commit refs/heads/master mark :1 committer 1110341295 +0000 data 25 import from baz patch-364 M 644 inline README data 656 Release notes for Bazaar-NG (pre-0) mbp@sourcefrog.net, November 2004, London * There is little locking or transaction control here; if you interrupt it the tree may be arbitrarily broken. This will be fixed. * Don't use this for critical data; at the very least keep separate regular snapshots of your tree. Dependencies ------------ This is mostly developed on Linux (Ubuntu); it should work on Unix, Windows, or OS X with relatively little trouble. bzr requires a fairly recent Python, say after 2.2. You can improve performance by installing Fredrik Lundh's amazing cElementTree__ module. __ http://effbot.org/zone/celementtree.htm M 644 inline bzr.py data 20900 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. """ # not currently working: # bzr check # Run internal consistency checks. # bzr info # Show some information about this branch. __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob from ElementTree import Element, ElementTree, SubElement import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Is ElementTree really all that much better for our purposes? ## Perhaps using the standard MiniDOM would be enough? ###################################################################### # check status def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_inventory(inventory_id): """Return inventory in XML by hash""" Branch('.').get_inventory(inventory_hash).write_xml(sys.stdout) def cmd_get_revision_inventory(revision_id): """Output inventory for a revision.""" b = Branch('.') b.get_revision_inventory(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files. Fails if the files are already added. """ Branch('.').add(file_list, verbose=verbose) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): b = Branch('.') print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') print 'revision number:', b.revno() print 'number of versioned files:', len(b.read_working_inventory()) def cmd_remove(file_list, verbose=False): Branch('.').remove(file_list, verbose=verbose) def cmd_file_id(filename): i = Branch('.').read_working_inventory().path2id(filename) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_log(): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log() def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print uuid() def cmd_commit(message, verbose=False): Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_doctest(): """Run internal doctest suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option import bzr, doctest, bzrlib.store bzrlib.trace.verbose = False doctest.testmod(bzr) doctest.testmod(bzrlib.store) doctest.testmod(bzrlib.inventory) doctest.testmod(bzrlib.branch) doctest.testmod(bzrlib.osutils) doctest.testmod(bzrlib.tree) # more strenuous tests; import bzrlib.tests doctest.testmod(bzrlib.tests) ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'ls': ['revision', 'verbose'], 'status': ['all'], 'log': ['show-ids'], 'remove': ['verbose'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('bzr --help'.split()) ([], {'help': True}) >>> parse_args('bzr --version'.split()) ([], {'version': True}) >>> parse_args('bzr status --all'.split()) (['status'], {'all': True}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? it = iter(argv[1:]) while it: a = it.next() if a[0] == '-': if a[1] == '-': mutter(" got option %r" % a) optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if not it: bailout('option %r needs an argument' % a) opts[optname] = optargfn(it.next()) mutter(" option argument %r" % opts[optname]) else: # takes no option argument opts[optname] = True elif a[:1] == '-': bailout('unknown short option %r' % a) else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. for ap in argform: argname = ap[:-1] if ap[-1] == '?': assert 0 elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. try: t = bzrlib.trace._tracefile t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % format_date(time.time())) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.gethostname())) t.write(' arguments: %r\n' % argv) starttime = os.times()[4] import platform t.write(' platform: %s\n' % platform.platform()) t.write(' python: %s\n' % platform.python_version()) ret = run_bzr(argv) times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum" % times[:4]) mutter(" %.3f elapsed" % (times[4] - starttime)) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') M 644 inline bzrlib/__init__.py data 1016 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr library""" from inventory import Inventory, InventoryEntry from branch import Branch, ScratchBranch from osutils import format_date from tree import Tree from diff import diff_trees BZRDIR = ".bzr" DEFAULT_IGNORE = ['.*', '*~', '#*#', '*.tmp', '*.o', '*.a'] M 644 inline bzrlib/branch.py data 25483 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind from store import ImmutableStore from revision import Revision from errors import bailout from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. :todo: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. :todo: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. :todo: Keep the on-disk branch locked while the object exists. :todo: mkdir() method. """ def __init__(self, base, init=False): """Create new branch object at a particular location. :param base: Base directory for the branch. :param init: If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ self.base = os.path.realpath(base) if init: self._make_control() else: if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def _rel(self, name): """Return filename relative to branch top""" return os.path.join(self.base, name) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch""" return file(self.controlfilename(file_or_path), mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # read in binary mode to detect newline wierdness. fmt = self.controlfile('branch-format', 'rb').read() if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() inv = Inventory.read_xml(self.controlfile('inventory', 'r')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ inv.write_xml(self.controlfile('inventory', 'w')) mutter('wrote inventory to %s' % quotefn(self.controlfilename('inventory'))) inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. This puts the files in the Added state, so that they will be recorded by the next commit. :todo: Perhaps have an option to add the ids even if the files do not (yet) exist. :todo: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. :todo: Option to specify file id. :todo: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self._rel(f)) if isfile(fullpath): kind = 'file' elif isdir(fullpath): kind = 'directory' else: bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if len(fp) > 1: parent_name = joinpath(fp[:-1]) mutter("lookup parent %r" % parent_name) parent_id = inv.path2id(parent_name) if parent_id == None: bailout("cannot add: parent %r is not versioned" % joinpath(fp[:-1])) else: parent_id = None file_id = _gen_file_id(fp[-1]) inv.add(InventoryEntry(file_id, fp[-1], kind=kind, parent_id=parent_id)) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r parent_id={%s}" % (f, file_id, kind, parent_id)) self._write_inventory(inv) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on :todo: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True :todo: Do something useful with directories. :todo: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: show_status('D', inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. :param timestamp: if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self._rel(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = _gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'R' else: state = 'M' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() mutter("building commit log message") rev = Revision(timestamp=timestamp, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) mutter("append to revision-history") self.controlfile('revision-history', 'at').write(rev_id + '\n') mutter("done!") def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. :todo: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: bailout("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, utc=False): """Write out human-readable log of commits to this branch :param utc: If true, show dates in universal time, not local time.""" revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, utc)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l revno += 1 precursor = p def show_status(branch, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() :todo: Get state for single files. :todo: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = branch.basis_tree() old_inv = old.inventory new = branch.working_tree() new_inv = new.inventory for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("wierd file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files = []): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def _gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" assert '/' not in name while name[0] == '.': name = name[1:] s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/check.py data 3362 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ###################################################################### # consistency checks def check(): """Consistency check of tree.""" assert_in_tree() mutter("checking tree") check_patches_exist() check_patch_chaining() check_patch_uniqueness() check_inventory() mutter("tree looks OK") ## TODO: Check that previous-inventory and previous-manifest ## are the same as those stored in the previous changeset. ## TODO: Check all patches present in patch directory are ## mentioned in patch history; having an orphaned patch only gives ## a warning. ## TODO: Check cached data is consistent with data reconstructed ## from scratch. ## TODO: Check no control files are versioned. ## TODO: Check that the before-hash of each file in a later ## revision matches the after-hash in the previous revision to ## touch it. def check_inventory(): mutter("checking inventory file and ids...") seen_ids = Set() seen_names = Set() for l in controlfile('inventory').readlines(): parts = l.split() if len(parts) != 2: bailout("malformed inventory line: " + `l`) file_id, name = parts if file_id in seen_ids: bailout("duplicated file id " + file_id) seen_ids.add(file_id) if name in seen_names: bailout("duplicated file name in inventory: " + quotefn(name)) seen_names.add(name) if is_control_file(name): raise BzrError("control file %s present in inventory" % quotefn(name)) def check_patches_exist(): """Check constraint of current version: all patches exist""" mutter("checking all patches are present...") for pid in revision_history(): read_patch_header(pid) def check_patch_chaining(): """Check ancestry of patches and history file is consistent""" mutter("checking patch chaining...") prev = None for pid in revision_history(): log_prev = read_patch_header(pid).precursor if log_prev != prev: bailout("inconsistent precursor links on " + pid) prev = pid def check_patch_uniqueness(): """Make sure no patch is listed twice in the history. This should be implied by having correct ancestry but I'll check it anyhow.""" mutter("checking history for duplicates...") seen = Set() for pid in revision_history(): if pid in seen: bailout("patch " + pid + " appears twice in history") seen.add(pid) M 644 inline bzrlib/diff.py data 5290 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set from trace import mutter def diff_trees(old_tree, new_tree): """Compute diff between two trees. They may be in different branches and may be working or historical trees. Yields a sequence of (state, id, old_name, new_name, kind). Each filename and each id is listed only once. """ ## TODO: Compare files before diffing; only mention those that have changed ## TODO: Set nice names in the headers, maybe include diffstat ## TODO: Perhaps make this a generator rather than using ## a callback object? ## TODO: Allow specifying a list of files to compare, rather than ## doing the whole tree? (Not urgent.) ## TODO: Allow diffing any two inventories, not just the ## current one against one. We mgiht need to specify two ## stores to look for the files if diffing two branches. That ## might imply this shouldn't be primarily a Branch method. ## XXX: This doesn't report on unknown files; that can be done ## from a separate method. old_it = old_tree.list_files() new_it = new_tree.list_files() def next(it): try: return it.next() except StopIteration: return None old_item = next(old_it) new_item = next(new_it) # We step through the two sorted iterators in parallel, trying to # keep them lined up. while (old_item != None) or (new_item != None): # OK, we still have some remaining on both, but they may be # out of step. if old_item != None: old_name, old_class, old_kind, old_id = old_item else: old_name = None if new_item != None: new_name, new_class, new_kind, new_id = new_item else: new_name = None mutter(" diff pairwise %r" % (old_item,)) mutter(" %r" % (new_item,)) if old_item: # can't handle the old tree being a WorkingTree assert old_class == 'V' if new_item and (new_class != 'V'): yield new_class, None, None, new_name, new_kind new_item = next(new_it) elif (not new_item) or (old_item and (old_name < new_name)): mutter(" extra entry in old-tree sequence") if new_tree.has_id(old_id): # will be mentioned as renamed under new name pass else: yield 'D', old_id, old_name, None, old_kind old_item = next(old_it) elif (not old_item) or (new_item and (new_name < old_name)): mutter(" extra entry in new-tree sequence") if old_tree.has_id(new_id): yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind else: yield 'A', new_id, None, new_name, new_kind new_item = next(new_it) elif old_id != new_id: assert old_name == new_name # both trees have a file of this name, but it is not the # same file. in other words, the old filename has been # overwritten by either a newly-added or a renamed file. # (should we return something about the overwritten file?) if old_tree.has_id(new_id): # renaming, overlying a deleted file yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind else: yield 'A', new_id, None, new_name, new_kind new_item = next(new_it) old_item = next(old_it) else: assert old_id == new_id assert old_name == new_name assert old_kind == new_kind if old_kind == 'directory': yield '.', new_id, old_name, new_name, new_kind elif old_tree.get_file_size(old_id) != new_tree.get_file_size(old_id): mutter(" file size has changed, must be different") yield 'M', new_id, old_name, new_name, new_kind elif old_tree.get_file_sha1(old_id) == new_tree.get_file_sha1(old_id): mutter(" SHA1 indicates they're identical") ## assert compare_files(old_tree.get_file(i), new_tree.get_file(i)) yield '.', new_id, old_name, new_name, new_kind else: mutter(" quick compare shows different") yield 'M', new_id, old_name, new_name, new_kind new_item = next(new_it) old_item = next(old_it) M 644 inline bzrlib/errors.py data 1170 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " ###################################################################### # exceptions class BzrError(StandardError): pass class BzrCheckError(BzrError): pass def bailout(msg, explanation=[]): ex = BzrError(msg, explanation) import trace trace._tracefile.write('* raising %s\n' % ex) raise ex M 644 inline bzrlib/inventory.py data 14825 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Inventories map files to their name in a revision.""" __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os.path, types from sets import Set from xml import XMLMixin from ElementTree import ElementTree, Element from errors import bailout from osutils import uuid, quotefn, splitpath, joinpath, appendpath from trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') >>> i.add(InventoryEntry('123', 'src', kind='directory')) >>> i.add(InventoryEntry('2323', 'hello.c', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id=None)) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', parent_id='123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', parent_id='123')) >>> i.add(InventoryEntry('2325', 'wibble', parent_id='123', kind='directory')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', parent_id='2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' :todo: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ def __init__(self, file_id, name, kind='file', text_id=None, parent_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c') >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c') Traceback (most recent call last): BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", []) """ if len(splitpath(name)) != 1: bailout('InventoryEntry name is not a simple filename: %r' % name) self.file_id = file_id self.name = name assert kind in ['file', 'directory'] self.kind = kind self.text_id = text_id self.parent_id = parent_id self.text_sha1 = None self.text_size = None def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.text_id, self.parent_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size is not None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1', 'parent_id']: v = getattr(self, f) if v is not None: e.set(f, v) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind')) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') self.parent_id = elt.get('parent_id') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __cmp__(self, other): if self is other: return 0 if not isinstance(other, InventoryEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.name, other.name) \ or cmp(self.text_sha1, other.text_sha1) \ or cmp(self.text_size, other.text_size) \ or cmp(self.text_id, other.text_id) \ or cmp(self.parent_id, other.parent_id) \ or cmp(self.kind, other.kind) class Inventory(XMLMixin): """Inventory of versioned files in a tree. An Inventory acts like a set of InventoryEntry items. You can also look files up by their file_id or name. May be read from and written to a metadata file in a tree. To manipulate the inventory (for example to add a file), it is read in, modified, and then written back out. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c')) >>> inv['123-123'].name 'hello.c' >>> for file_id in inv: print file_id ... 123-123 May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ ## TODO: Clear up handling of files in subdirectories; we probably ## do want to be able to just look them up by name but this ## probably means gradually walking down the path, looking up as we go. ## TODO: Make sure only canonical filenames are stored. ## TODO: Do something sensible about the possible collisions on ## case-losing filesystems. Perhaps we should just always forbid ## such collisions. ## _tree should probably just be stored as ## InventoryEntry._children on each directory. def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. """ self._byid = dict() # _tree is indexed by parent_id; at each level a map from name # to ie. The None entry is the root. self._tree = {None: {}} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, parent_id=None): """Return (path, entry) pairs, in order by name.""" kids = self._tree[parent_id].items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(parent_id=ie.file_id): yield joinpath([name, cn]), cie def directories(self, include_root=True): """Return (path, entry) pairs for all directories. """ if include_root: yield '', None for path, entry in self.iter_entries(): if entry.kind == 'directory': yield path, entry def children(self, parent_id): """Return entries that are direct children of parent_id.""" return self._tree[parent_id] # TODO: return all paths and entries def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c')) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c')) >>> inv['123123'].name 'hello.c' """ return self._byid[file_id] def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self: bailout("inventory already contains entry with id {%s}" % entry.file_id) if entry.parent_id != None: if entry.parent_id not in self: bailout("parent_id %s of new entry not found in inventory" % entry.parent_id) if self._tree[entry.parent_id].has_key(entry.name): bailout("%s is already versioned" % appendpath(self.id2path(entry.parent_id), entry.name)) self._byid[entry.file_id] = entry self._tree[entry.parent_id][entry.name] = entry if entry.kind == 'directory': self._tree[entry.file_id] = {} def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c')) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self._tree[ie.parent_id][ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in self._tree[file_id].values(): del self[cie.file_id] del self._tree[file_id] del self._byid[file_id] del self._tree[ie.parent_id][ie.name] def id_set(self): return Set(self._byid) def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c')) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __cmp__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo')) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo')) >>> i1 == i2 True """ if self is other: return 0 if not isinstance(other, Inventory): return NotImplemented if self.id_set() ^ other.id_set(): return 1 for file_id in self._byid: c = cmp(self[file_id], other[file_id]) if c: return c return 0 def id2path(self, file_id): """Return as a list the path to file_id.""" p = [] while file_id != None: ie = self[file_id] p = [ie.name] + p file_id = ie.parent_id return joinpath(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. """ assert isinstance(name, types.StringTypes) parent_id = None for f in splitpath(name): try: cie = self._tree[parent_id][f] assert cie.name == f parent_id = cie.file_id except KeyError: # or raise an error? return None return parent_id def get_child(self, parent_id, child_name): return self._tree[parent_id].get(child_name) def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): assert isinstance(file_id, str) return self._byid.has_key(file_id) if __name__ == '__main__': import doctest, inventory doctest.testmod(inventory) M 644 inline bzrlib/osutils.py data 6436 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, types, re, time, types from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE from errors import bailout def make_readonly(filename): """Make a filename read-only.""" # TODO: probably needs to be fixed for windows mod = os.stat(filename).st_mode mod = mod & 0777555 os.chmod(filename, mod) def make_writable(filename): mod = os.stat(filename).st_mode mod = mod | 0200 os.chmod(filename, mod) _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])') def quotefn(f): """Return shell-quoted filename""" ## We could be a bit more terse by using double-quotes etc f = _QUOTE_RE.sub(r'\\\1', f) if f[0] == '~': f[0:1] = r'\~' return f def file_kind(f): mode = os.lstat(f)[ST_MODE] if S_ISREG(mode): return 'file' elif S_ISDIR(mode): return 'directory' else: bailout("can't handle file kind of %r" % fp) def isdir(f): """True if f is an accessible directory.""" try: return S_ISDIR(os.lstat(f)[ST_MODE]) except OSError: return False def isfile(f): """True if f is a regular file.""" try: return S_ISREG(os.lstat(f)[ST_MODE]) except OSError: return False def pumpfile(fromfile, tofile): """Copy contents of one file to another.""" tofile.write(fromfile.read()) def uuid(): """Return a new UUID""" ## XXX: Could alternatively read /proc/sys/kernel/random/uuid on ## Linux, but we need something portable for other systems; ## preferably an implementation in Python. bailout('uuids not allowed!') return chomp(os.popen('uuidgen').readline()) def chomp(s): if s and (s[-1] == '\n'): return s[:-1] else: return s def sha_file(f): import sha ## TODO: Maybe read in chunks to handle big files if hasattr(f, 'tell'): assert f.tell() == 0 s = sha.new() s.update(f.read()) return s.hexdigest() def sha_string(f): import sha s = sha.new() s.update(f) return s.hexdigest() def username(): """Return email-style username. Something similar to 'Martin Pool ' :todo: Check it's reasonably well-formed. :todo: Allow taking it from a dotfile to help people on windows who can't easily set variables. :todo: Cope without pwd module, which is only on unix. """ e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: return e import socket try: import pwd uid = os.getuid() w = pwd.getpwuid(uid) realname, junk = w.pw_gecos.split(',', 1) return '%s <%s@%s>' % (realname, w.pw_name, socket.getfqdn()) except ImportError: pass import getpass, socket return '<%s@%s>' % (getpass.getuser(), socket.getfqdn()) def user_email(): """Return just the email component of a username.""" e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: import re m = re.search(r'[\w+.-]+@[\w+.-]+', e) if not m: bailout('%r is not a reasonable email address' % e) return m.group(0) import getpass, socket return '%s@%s' % (getpass.getuser(), socket.getfqdn()) def compare_files(a, b): """Returns true if equal in contents""" # TODO: don't read the whole thing in one go. result = a.read() == b.read() return result def format_date(t, inutc=False): ## TODO: Perhaps a global option to use either universal or local time? ## Or perhaps just let people set $TZ? import time assert isinstance(t, float) if inutc: tt = time.gmtime(t) zonename = 'UTC' offset = 0 else: tt = time.localtime(t) if time.daylight: zonename = time.tzname[1] offset = - time.altzone else: zonename = time.tzname[0] offset = - time.timezone return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt) + ' ' + zonename + ' ' + '%+03d%02d' % (offset / 3600, (offset / 60) % 60)) def compact_date(when): return time.strftime('%Y%m%d%H%M%S', time.gmtime(when)) def filesize(f): """Return size of given open file.""" return os.fstat(f.fileno())[ST_SIZE] if hasattr(os, 'urandom'): # python 2.4 and later rand_bytes = os.urandom else: # FIXME: No good on non-Linux _rand_file = file('/dev/urandom', 'rb') rand_bytes = _rand_file.read ## TODO: We could later have path objects that remember their list ## decomposition (might be too tricksy though.) def splitpath(p): """Turn string into list of parts. >>> splitpath('a') ['a'] >>> splitpath('a/b') ['a', 'b'] >>> splitpath('a/./b') ['a', 'b'] >>> splitpath('a/.b') ['a', '.b'] >>> splitpath('a/../b') Traceback (most recent call last): ... BzrError: ("sorry, '..' not allowed in path", []) """ assert isinstance(p, types.StringTypes) ps = [f for f in p.split('/') if f != '.'] for f in ps: if f == '..': bailout("sorry, %r not allowed in path" % f) return ps def joinpath(p): assert isinstance(p, list) for f in p: if (f == '..') or (f is None) or (f == ''): bailout("sorry, %r not allowed in path" % f) return '/'.join(p) def appendpath(p1, p2): if p1 == '': return p2 else: return p1 + '/' + p2 def extern_command(cmd, ignore_errors = False): mutter('external command: %s' % `cmd`) if os.system(cmd): if not ignore_errors: bailout('command failed') M 644 inline bzrlib/revision.py data 2420 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from xml import XMLMixin from ElementTree import Element, ElementTree, SubElement class Revision(XMLMixin): """Single revision on a branch. Revisions may know their revision_hash, but only once they've been written out. This is not stored because you cannot write the hash into the file it describes. :todo: Perhaps make predecessor be a child element, not an attribute? """ def __init__(self, **args): self.inventory_id = None self.revision_id = None self.timestamp = None self.message = None self.__dict__.update(args) def __repr__(self): if self.revision_id: return "" % self.revision_id def to_element(self): root = Element('changeset', committer = self.committer, timestamp = '%f' % self.timestamp, revision_id = self.revision_id, inventory_id = self.inventory_id) if self.precursor: root.set('precursor', self.precursor) root.text = '\n' msg = SubElement(root, 'message') msg.text = self.message msg.tail = '\n' return root def from_element(cls, root): cs = cls(committer = root.get('committer'), timestamp = float(root.get('timestamp')), precursor = root.get('precursor'), revision_id = root.get('revision_id'), inventory_id = root.get('inventory_id')) cs.message = root.findtext('message') # text of return cs from_element = classmethod(from_element) M 644 inline bzrlib/store.py data 4395 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Stores are the main data-storage mechanism for Bazaar-NG. A store is a simple write-once container indexed by a universally unique ID, which is typically the SHA-1 of the content.""" __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import os, tempfile, types, osutils from StringIO import StringIO from trace import mutter ###################################################################### # stores class StoreError(Exception): pass class ImmutableStore: """Store that holds files indexed by unique names. Files can be added, but not modified once they are in. Typically the hash is used as the name, or something else known to be unique, such as a UUID. >>> st = ImmutableScratchStore() >>> st.add(StringIO('hello'), 'aa') >>> 'aa' in st True >>> 'foo' in st False You are not allowed to add an id that is already present. Entries can be retrieved as files, which may then be read. >>> st.add(StringIO('goodbye'), '123123') >>> st['123123'].read() 'goodbye' :todo: Atomic add by writing to a temporary file and renaming. :todo: Perhaps automatically transform to/from XML in a method? Would just need to tell the constructor what class to use... :todo: Even within a simple disk store like this, we could gzip the files. But since many are less than one disk block, that might not help a lot. """ def __init__(self, basedir): """ImmutableStore constructor.""" self._basedir = basedir def _path(self, id): return os.path.join(self._basedir, id) def __repr__(self): return "%s(%r)" % (self.__class__.__name__, self._basedir) def add(self, f, fileid): """Add contents of a file into the store. :param f: An open file, or file-like object.""" # FIXME: Only works on smallish files # TODO: Can be optimized by copying at the same time as # computing the sum. mutter("add store entry %r" % (fileid)) if isinstance(f, types.StringTypes): content = f else: content = f.read() if fileid not in self: filename = self._path(fileid) f = file(filename, 'wb') f.write(content) f.flush() os.fsync(f.fileno()) f.close() osutils.make_readonly(filename) def __contains__(self, fileid): """""" return os.access(self._path(fileid), os.R_OK) def __iter__(self): return iter(os.listdir(self._basedir)) def __getitem__(self, fileid): """Returns a file reading from a particular entry.""" return file(self._path(fileid), 'rb') def delete_all(self): for fileid in self: self.delete(fileid) def delete(self, fileid): """Remove nominated store entry. Most stores will be add-only.""" filename = self._path(fileid) ## osutils.make_writable(filename) os.remove(filename) def destroy(self): """Remove store; only allowed if it is empty.""" os.rmdir(self._basedir) mutter("%r destroyed" % self) class ImmutableScratchStore(ImmutableStore): """Self-destructing test subclass of ImmutableStore. The Store only exists for the lifetime of the Python object. Obviously you should not put anything precious in it. """ def __init__(self): ImmutableStore.__init__(self, tempfile.mkdtemp()) def __del__(self): self.delete_all() self.destroy() M 644 inline bzrlib/tests.py data 4720 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # XXX: We might prefer these to be in a text file rather than Python # source, but that only works in doctest from Python 2.4 and later, # which is not present in Warty. r""" Bazaar-NG test cases ******************** These are run by ``bzr.doctest``. >>> import bzr, bzrlib, os >>> bzr.cmd_rocks() it sure does! Hey, nice place to begin. The basic object is a Branch. We have a special helper class ScratchBranch that automatically makes a directory and cleans itself up, but is in other respects identical. ScratchBranches are initially empty: >>> b = bzr.ScratchBranch() >>> b.show_status() New files in that directory are, it is initially unknown: >>> file(b.base + '/hello.c', 'wt').write('int main() {}') >>> b.show_status() ? hello.c That's not quite true; some files (like editor backups) are ignored by default: >>> file(b.base + '/hello.c~', 'wt').write('int main() {}') >>> b.show_status() ? hello.c >>> list(b.unknowns()) ['hello.c'] The ``add`` command marks a file to be added in the next revision: >>> b.add('hello.c') >>> b.show_status() A hello.c You can also add files that otherwise would be ignored. The ignore patterns only apply to files that would be otherwise unknown, so they have no effect once it's added. >>> b.add('hello.c~') >>> b.show_status() A hello.c A hello.c~ It is an error to add a file that isn't present in the working copy: >>> b.add('nothere') Traceback (most recent call last): ... BzrError: ('cannot add: not a regular file or directory: nothere', []) If we add a file and then change our mind, we can either revert it or remove the file. If we revert, we are left with the working copy (in either I or ? state). If we remove, the working copy is gone. Let's do that to the backup, presumably added accidentally. >>> b.remove('hello.c~') >>> b.show_status() A hello.c Now to commit, creating a new revision. (Fake the date and name for reproducibility.) >>> b.commit('start hello world', timestamp=0, committer='foo@nowhere') >>> b.show_status() >>> b.show_status(show_all=True) . hello.c I hello.c~ We can look back at history >>> r = b.get_revision(b.lookup_revision(1)) >>> r.message 'start hello world' >>> b.write_log(utc=True) ---------------------------------------- revno: 1 committer: foo@nowhere timestamp: Thu 1970-01-01 00:00:00 UTC +0000 message: start hello world (The other fields will be a bit unpredictable, depending on who ran this test and when.) As of 2005-02-21, we can also add subdirectories to the revision! >>> os.mkdir(b.base + "/lib") >>> b.show_status() ? lib/ >>> b.add('lib') >>> b.show_status() A lib/ >>> b.commit('add subdir') >>> b.show_status() >>> b.show_status(show_all=True) . hello.c I hello.c~ . lib/ and we can also add files within subdirectories: >>> file(b.base + '/lib/hello', 'w').write('hello!\n') >>> b.show_status() ? lib/hello Tests for adding subdirectories, etc. >>> b = bzrlib.branch.ScratchBranch() >>> os.mkdir(b._rel('d1')) >>> os.mkdir(b._rel('d2')) >>> os.mkdir(b._rel('d2/d3')) >>> list(b.working_tree().unknowns()) ['d1', 'd2'] Create some files, but they're not seen as unknown yet: >>> file(b._rel('d1/f1'), 'w').close() >>> file(b._rel('d2/f2'), 'w').close() >>> file(b._rel('d2/f3'), 'w').close() >>> [v[0] for v in b.inventory.directories()] [''] >>> list(b.working_tree().unknowns()) ['d1', 'd2'] Adding a directory, and we see the file underneath: >>> b.add('d1') >>> [v[0] for v in b.inventory.directories()] ['', 'd1'] >>> list(b.working_tree().unknowns()) ['d1/f1', 'd2'] >>> # d2 comes first because it's in the top directory >>> b.add('d2') >>> b.commit('add some stuff') >>> list(b.working_tree().unknowns()) ['d1/f1', 'd2/d3', 'd2/f2', 'd2/f3'] """ M 644 inline bzrlib/textui.py data 1017 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def show_status(state, kind, name): if kind == 'directory': kind_ch = '/' else: assert kind == 'file' kind_ch = '' assert len(state) == 1 print state + ' ' + name + kind_ch M 644 inline bzrlib/trace.py data 1875 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys ###################################################################### # messages and logging # Messages are always written to here, so that we have some # information if something goes wrong. In a future version this # file will be removed on successful completion. _tracefile = file('.bzr.log', 'at') ## TODO: If --verbose is given then write to both stderr and ## _tracefile; perhaps replace _tracefile with a tee thing. # used to have % (os.environ['USER'], time.time(), os.getpid()), 'w') # If false, notes also go to stdout; should replace this with --silent # at some point. silent = False verbose = False def mutter(msg): _tracefile.write(msg) _tracefile.write('\n') _tracefile.flush() if verbose: sys.stderr.write('- ' + msg + '\n') def note(msg): b = '* ' + str(msg) + '\n' if not silent: sys.stderr.write(b) _tracefile.write(b) _tracefile.flush() def log_error(msg): sys.stderr.write(msg) _tracefile.write(msg) _tracefile.flush() M 644 inline bzrlib/tree.py data 11894 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tree classes, representing directory at point in time. """ from sets import Set import os.path, os, fnmatch from inventory import Inventory from trace import mutter, note from osutils import pumpfile, compare_files, filesize, quotefn, sha_file, \ joinpath, splitpath, appendpath, isdir, isfile, file_kind from errors import bailout import branch from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE import bzrlib class Tree: """Abstract file tree. There are several subclasses: * `WorkingTree` exists as files on disk editable by the user. * `RevisionTree` is a tree as recorded at some point in the past. * `EmptyTree` Trees contain an `Inventory` object, and also know how to retrieve file texts mentioned in the inventory, either from a working directory or from a store. It is possible for trees to contain files that are not described in their inventory or vice versa; for this use `filenames()`. Trees can be compared, etc, regardless of whether they are working trees or versioned trees. """ def get_file(self, file_id): """Return an open file-like object for given file id.""" raise NotImplementedError() def has_filename(self, filename): """True if the tree has given filename.""" raise NotImplementedError() def has_id(self, file_id): return self.inventory.has_id(file_id) def id_set(self): """Return set of all ids in this tree.""" return self.inventory.id_set() def id2path(self, file_id): return self.inventory.id2path(file_id) def _get_inventory(self): return self._inventory inventory = property(_get_inventory, doc="Inventory of this Tree") def _check_retrieved(self, ie, f): # TODO: Test this check by damaging the store? if ie.text_size is not None: fs = filesize(f) if fs != ie.text_size: bailout("mismatched size for file %r in %r" % (ie.file_id, self._store), ["inventory expects %d bytes" % ie.text_size, "file is actually %d bytes" % fs, "store is probably damaged/corrupt"]) f_hash = sha_file(f) f.seek(0) if ie.text_sha1 != f_hash: bailout("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store), ["inventory expects %s" % ie.text_sha1, "file is actually %s" % f_hash, "store is probably damaged/corrupt"]) def export(self, dest): """Export this tree to a new directory. `dest` should not exist, and will be created holding the contents of this tree. :todo: To handle subdirectories we need to create the directories first. :note: If the export fails, the destination directory will be left in a half-assed state. """ os.mkdir(dest) mutter('export version %r' % self) inv = self.inventory for dp, ie in inv.iter_entries(): kind = ie.kind fullpath = appendpath(dest, dp) if kind == 'directory': os.mkdir(fullpath) elif kind == 'file': pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb')) else: bailout("don't know how to export {%s} of kind %r", fid, kind) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) class WorkingTree(Tree): """Working copy tree. The inventory is held in the `Branch` working-inventory, and the files are in a directory on disk. It is possible for a `WorkingTree` to have a filename which is not listed in the Inventory and vice versa. """ def __init__(self, basedir, inv): self._inventory = inv self.basedir = basedir self.path2id = inv.path2id def __repr__(self): return "<%s of %s>" % (self.__class__.__name__, self.basedir) def _rel(self, filename): return os.path.join(self.basedir, filename) def has_filename(self, filename): return os.path.exists(self._rel(filename)) def get_file(self, file_id): return file(self._get_store_filename(file_id), 'rb') def _get_store_filename(self, file_id): return self._rel(self.id2path(file_id)) def get_file_size(self, file_id): return os.stat(self._get_store_filename(file_id))[ST_SIZE] def get_file_sha1(self, file_id): f = self.get_file(file_id) return sha_file(f) def file_class(self, filename): if self.path2id(filename): return 'V' elif self.is_ignored(filename): return 'I' else: return '?' def file_kind(self, filename): if isfile(self._rel(filename)): return 'file' elif isdir(self._rel(filename)): return 'directory' else: return 'unknown' def list_files(self): """Recursively list all files as (path, class, kind, id). Lists, but does not descend into unversioned directories. This does not include files that have been deleted in this tree. Skips the control directory. """ inv = self.inventory def descend(from_dir, from_dir_id, dp): ls = os.listdir(dp) ls.sort() for f in ls: if bzrlib.BZRDIR == f: continue # path within tree fp = appendpath(from_dir, f) # absolute path fap = appendpath(dp, f) f_ie = inv.get_child(from_dir_id, f) if f_ie: c = 'V' elif self.is_ignored(fp): c = 'I' else: c = '?' fk = file_kind(fap) if f_ie: if f_ie.kind != fk: bailout("file %r entered as kind %r id %r, now of kind %r" % (fap, f_ie.kind, f_ie.file_id, fk)) yield fp, c, fk, (f_ie and f_ie.file_id) if fk != 'directory': continue if c != 'V': # don't descend unversioned directories continue for ff in descend(fp, f_ie.file_id, fap): yield ff for f in descend('', None, self.basedir): yield f def unknowns(self, path='', dir_id=None): """Yield names of unknown files in this WorkingTree. If there are any unknown directories then only the directory is returned, not all its children. But if there are unknown files under a versioned subdirectory, they are returned. Currently returned depth-first, sorted by name within directories. """ for fpath, fclass, fkind, fid in self.list_files(): if fclass == '?': yield fpath def ignored_files(self): for fpath, fclass, fkind, fid in self.list_files(): if fclass == 'I': yield fpath def is_ignored(self, filename): """Check whether the filename matches an ignore pattern.""" ## TODO: Take them from a file, not hardcoded ## TODO: Use extended zsh-style globs maybe? ## TODO: Use '**' to match directories? ## TODO: Patterns without / should match in subdirectories? for i in bzrlib.DEFAULT_IGNORE: if fnmatch.fnmatchcase(filename, i): return True return False class RevisionTree(Tree): """Tree viewing a previous revision. File text can be retrieved from the text store. :todo: Some kind of `__repr__` method, but a good one probably means knowing the branch and revision number, or at least passing a description to the constructor. """ def __init__(self, store, inv): self._store = store self._inventory = inv def get_file(self, file_id): ie = self._inventory[file_id] f = self._store[ie.text_id] mutter(" get fileid{%s} from %r" % (file_id, self)) fs = filesize(f) if ie.text_size is None: note("warning: no text size recorded on %r" % ie) self._check_retrieved(ie, f) return f def get_file_size(self, file_id): return self._inventory[file_id].text_size def get_file_sha1(self, file_id): ie = self._inventory[file_id] return ie.text_sha1 def has_filename(self, filename): return bool(self.inventory.path2id(filename)) def list_files(self): # The only files returned by this are those from the version for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id class EmptyTree(Tree): def __init__(self): self._inventory = Inventory() def has_filename(self, filename): return False def list_files(self): if False: # just to make it a generator yield None ###################################################################### # diff # TODO: Merge these two functions into a single one that can operate # on either a whole tree or a set of files. # TODO: Return the diff in order by filename, not by category or in # random order. Can probably be done by lock-stepping through the # filenames from both trees. def file_status(filename, old_tree, new_tree): """Return single-letter status, old and new names for a file. The complexity here is in deciding how to represent renames; many complex cases are possible. """ old_inv = old_tree.inventory new_inv = new_tree.inventory new_id = new_inv.path2id(filename) old_id = old_inv.path2id(filename) if not new_id and not old_id: # easy: doesn't exist in either; not versioned at all if new_tree.is_ignored(filename): return 'I', None, None else: return '?', None, None elif new_id: # There is now a file of this name, great. pass else: # There is no longer a file of this name, but we can describe # what happened to the file that used to have # this name. There are two possibilities: either it was # deleted entirely, or renamed. assert old_id if new_inv.has_id(old_id): return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id) else: return 'D', old_inv.id2path(old_id), None # if the file_id is new in this revision, it is added if new_id and not old_inv.has_id(new_id): return 'A' # if there used to be a file of this name, but that ID has now # disappeared, it is deleted if old_id and not new_inv.has_id(old_id): return 'D' return 'wtf?' M 644 inline bzrlib/xml.py data 1439 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """XML externalization support.""" __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " try: from cElementTree import Element, ElementTree, SubElement except ImportError: from ElementTree import Element, ElementTree, SubElement import os, time from trace import mutter class XMLMixin: def to_element(self): raise Exception("XMLMixin.to_element must be overridden in concrete classes") def write_xml(self, f): ElementTree(self.to_element()).write(f, 'utf-8') f.write('\n') def read_xml(cls, f): return cls.from_element(ElementTree().parse(f)) read_xml = classmethod(read_xml) commit refs/heads/master mark :2 committer 1110341369 +0000 data 45 add python bytecode to default ignore pattern from :1 M 644 inline bzrlib/__init__.py data 1028 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr library""" from inventory import Inventory, InventoryEntry from branch import Branch, ScratchBranch from osutils import format_date from tree import Tree from diff import diff_trees BZRDIR = ".bzr" DEFAULT_IGNORE = ['.*', '*~', '#*#', '*.tmp', '*.o', '*.a', '*.py[oc]'] commit refs/heads/master mark :3 committer 1110341397 +0000 data 36 add {arch} to default ignore pattern from :2 M 644 inline bzrlib/__init__.py data 1056 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr library""" from inventory import Inventory, InventoryEntry from branch import Branch, ScratchBranch from osutils import format_date from tree import Tree from diff import diff_trees BZRDIR = ".bzr" DEFAULT_IGNORE = ['.*', '*~', '#*#', '*.tmp', '*.o', '*.a', '*.py[oc]', '{arch}'] commit refs/heads/master mark :4 committer 1110341702 +0000 data 81 match ignore patterns against only the last path component unless a path is given from :3 M 644 inline bzrlib/tree.py data 12102 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tree classes, representing directory at point in time. """ from sets import Set import os.path, os, fnmatch from inventory import Inventory from trace import mutter, note from osutils import pumpfile, compare_files, filesize, quotefn, sha_file, \ joinpath, splitpath, appendpath, isdir, isfile, file_kind from errors import bailout import branch from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE import bzrlib class Tree: """Abstract file tree. There are several subclasses: * `WorkingTree` exists as files on disk editable by the user. * `RevisionTree` is a tree as recorded at some point in the past. * `EmptyTree` Trees contain an `Inventory` object, and also know how to retrieve file texts mentioned in the inventory, either from a working directory or from a store. It is possible for trees to contain files that are not described in their inventory or vice versa; for this use `filenames()`. Trees can be compared, etc, regardless of whether they are working trees or versioned trees. """ def get_file(self, file_id): """Return an open file-like object for given file id.""" raise NotImplementedError() def has_filename(self, filename): """True if the tree has given filename.""" raise NotImplementedError() def has_id(self, file_id): return self.inventory.has_id(file_id) def id_set(self): """Return set of all ids in this tree.""" return self.inventory.id_set() def id2path(self, file_id): return self.inventory.id2path(file_id) def _get_inventory(self): return self._inventory inventory = property(_get_inventory, doc="Inventory of this Tree") def _check_retrieved(self, ie, f): # TODO: Test this check by damaging the store? if ie.text_size is not None: fs = filesize(f) if fs != ie.text_size: bailout("mismatched size for file %r in %r" % (ie.file_id, self._store), ["inventory expects %d bytes" % ie.text_size, "file is actually %d bytes" % fs, "store is probably damaged/corrupt"]) f_hash = sha_file(f) f.seek(0) if ie.text_sha1 != f_hash: bailout("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store), ["inventory expects %s" % ie.text_sha1, "file is actually %s" % f_hash, "store is probably damaged/corrupt"]) def export(self, dest): """Export this tree to a new directory. `dest` should not exist, and will be created holding the contents of this tree. :todo: To handle subdirectories we need to create the directories first. :note: If the export fails, the destination directory will be left in a half-assed state. """ os.mkdir(dest) mutter('export version %r' % self) inv = self.inventory for dp, ie in inv.iter_entries(): kind = ie.kind fullpath = appendpath(dest, dp) if kind == 'directory': os.mkdir(fullpath) elif kind == 'file': pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb')) else: bailout("don't know how to export {%s} of kind %r", fid, kind) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) class WorkingTree(Tree): """Working copy tree. The inventory is held in the `Branch` working-inventory, and the files are in a directory on disk. It is possible for a `WorkingTree` to have a filename which is not listed in the Inventory and vice versa. """ def __init__(self, basedir, inv): self._inventory = inv self.basedir = basedir self.path2id = inv.path2id def __repr__(self): return "<%s of %s>" % (self.__class__.__name__, self.basedir) def _rel(self, filename): return os.path.join(self.basedir, filename) def has_filename(self, filename): return os.path.exists(self._rel(filename)) def get_file(self, file_id): return file(self._get_store_filename(file_id), 'rb') def _get_store_filename(self, file_id): return self._rel(self.id2path(file_id)) def get_file_size(self, file_id): return os.stat(self._get_store_filename(file_id))[ST_SIZE] def get_file_sha1(self, file_id): f = self.get_file(file_id) return sha_file(f) def file_class(self, filename): if self.path2id(filename): return 'V' elif self.is_ignored(filename): return 'I' else: return '?' def file_kind(self, filename): if isfile(self._rel(filename)): return 'file' elif isdir(self._rel(filename)): return 'directory' else: return 'unknown' def list_files(self): """Recursively list all files as (path, class, kind, id). Lists, but does not descend into unversioned directories. This does not include files that have been deleted in this tree. Skips the control directory. """ inv = self.inventory def descend(from_dir, from_dir_id, dp): ls = os.listdir(dp) ls.sort() for f in ls: if bzrlib.BZRDIR == f: continue # path within tree fp = appendpath(from_dir, f) # absolute path fap = appendpath(dp, f) f_ie = inv.get_child(from_dir_id, f) if f_ie: c = 'V' elif self.is_ignored(fp): c = 'I' else: c = '?' fk = file_kind(fap) if f_ie: if f_ie.kind != fk: bailout("file %r entered as kind %r id %r, now of kind %r" % (fap, f_ie.kind, f_ie.file_id, fk)) yield fp, c, fk, (f_ie and f_ie.file_id) if fk != 'directory': continue if c != 'V': # don't descend unversioned directories continue for ff in descend(fp, f_ie.file_id, fap): yield ff for f in descend('', None, self.basedir): yield f def unknowns(self, path='', dir_id=None): """Yield names of unknown files in this WorkingTree. If there are any unknown directories then only the directory is returned, not all its children. But if there are unknown files under a versioned subdirectory, they are returned. Currently returned depth-first, sorted by name within directories. """ for fpath, fclass, fkind, fid in self.list_files(): if fclass == '?': yield fpath def ignored_files(self): for fpath, fclass, fkind, fid in self.list_files(): if fclass == 'I': yield fpath def is_ignored(self, filename): """Check whether the filename matches an ignore pattern. Patterns containing '/' need to match the whole path; others match against only the last component.""" ## TODO: Take them from a file, not hardcoded ## TODO: Use extended zsh-style globs maybe? ## TODO: Use '**' to match directories? for pat in bzrlib.DEFAULT_IGNORE: if '/' in pat: if fnmatch.fnmatchcase(filename, pat): return True else: if fnmatch.fnmatchcase(splitpath(filename)[-1], pat): return True return False class RevisionTree(Tree): """Tree viewing a previous revision. File text can be retrieved from the text store. :todo: Some kind of `__repr__` method, but a good one probably means knowing the branch and revision number, or at least passing a description to the constructor. """ def __init__(self, store, inv): self._store = store self._inventory = inv def get_file(self, file_id): ie = self._inventory[file_id] f = self._store[ie.text_id] mutter(" get fileid{%s} from %r" % (file_id, self)) fs = filesize(f) if ie.text_size is None: note("warning: no text size recorded on %r" % ie) self._check_retrieved(ie, f) return f def get_file_size(self, file_id): return self._inventory[file_id].text_size def get_file_sha1(self, file_id): ie = self._inventory[file_id] return ie.text_sha1 def has_filename(self, filename): return bool(self.inventory.path2id(filename)) def list_files(self): # The only files returned by this are those from the version for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id class EmptyTree(Tree): def __init__(self): self._inventory = Inventory() def has_filename(self, filename): return False def list_files(self): if False: # just to make it a generator yield None ###################################################################### # diff # TODO: Merge these two functions into a single one that can operate # on either a whole tree or a set of files. # TODO: Return the diff in order by filename, not by category or in # random order. Can probably be done by lock-stepping through the # filenames from both trees. def file_status(filename, old_tree, new_tree): """Return single-letter status, old and new names for a file. The complexity here is in deciding how to represent renames; many complex cases are possible. """ old_inv = old_tree.inventory new_inv = new_tree.inventory new_id = new_inv.path2id(filename) old_id = old_inv.path2id(filename) if not new_id and not old_id: # easy: doesn't exist in either; not versioned at all if new_tree.is_ignored(filename): return 'I', None, None else: return '?', None, None elif new_id: # There is now a file of this name, great. pass else: # There is no longer a file of this name, but we can describe # what happened to the file that used to have # this name. There are two possibilities: either it was # deleted entirely, or renamed. assert old_id if new_inv.has_id(old_id): return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id) else: return 'D', old_inv.id2path(old_id), None # if the file_id is new in this revision, it is added if new_id and not old_inv.has_id(new_id): return 'A' # if there used to be a file of this name, but that ID has now # disappeared, it is deleted if old_id and not new_inv.has_id(old_id): return 'D' return 'wtf?' commit refs/heads/master mark :5 committer 1110343575 +0000 data 114 .bzrignore support - new WorkingTree.get_file_byname() to read .bzrignore - split out WorkingTree.get_ignore_list from :4 M 644 inline bzrlib/__init__.py data 1089 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr library""" from inventory import Inventory, InventoryEntry from branch import Branch, ScratchBranch from osutils import format_date from tree import Tree from diff import diff_trees BZRDIR = ".bzr" DEFAULT_IGNORE = ['.*', '*~', '#*#', '*.tmp', '*.o', '*.a', '*.py[oc]', '{arch}'] IGNORE_FILENAME = ".bzrignore" M 644 inline bzrlib/tree.py data 12371 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tree classes, representing directory at point in time. """ from sets import Set import os.path, os, fnmatch from inventory import Inventory from trace import mutter, note from osutils import pumpfile, compare_files, filesize, quotefn, sha_file, \ joinpath, splitpath, appendpath, isdir, isfile, file_kind from errors import bailout import branch from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE import bzrlib class Tree: """Abstract file tree. There are several subclasses: * `WorkingTree` exists as files on disk editable by the user. * `RevisionTree` is a tree as recorded at some point in the past. * `EmptyTree` Trees contain an `Inventory` object, and also know how to retrieve file texts mentioned in the inventory, either from a working directory or from a store. It is possible for trees to contain files that are not described in their inventory or vice versa; for this use `filenames()`. Trees can be compared, etc, regardless of whether they are working trees or versioned trees. """ def has_filename(self, filename): """True if the tree has given filename.""" raise NotImplementedError() def has_id(self, file_id): return self.inventory.has_id(file_id) def id_set(self): """Return set of all ids in this tree.""" return self.inventory.id_set() def id2path(self, file_id): return self.inventory.id2path(file_id) def _get_inventory(self): return self._inventory inventory = property(_get_inventory, doc="Inventory of this Tree") def _check_retrieved(self, ie, f): # TODO: Test this check by damaging the store? if ie.text_size is not None: fs = filesize(f) if fs != ie.text_size: bailout("mismatched size for file %r in %r" % (ie.file_id, self._store), ["inventory expects %d bytes" % ie.text_size, "file is actually %d bytes" % fs, "store is probably damaged/corrupt"]) f_hash = sha_file(f) f.seek(0) if ie.text_sha1 != f_hash: bailout("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store), ["inventory expects %s" % ie.text_sha1, "file is actually %s" % f_hash, "store is probably damaged/corrupt"]) def export(self, dest): """Export this tree to a new directory. `dest` should not exist, and will be created holding the contents of this tree. :todo: To handle subdirectories we need to create the directories first. :note: If the export fails, the destination directory will be left in a half-assed state. """ os.mkdir(dest) mutter('export version %r' % self) inv = self.inventory for dp, ie in inv.iter_entries(): kind = ie.kind fullpath = appendpath(dest, dp) if kind == 'directory': os.mkdir(fullpath) elif kind == 'file': pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb')) else: bailout("don't know how to export {%s} of kind %r", fid, kind) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) class WorkingTree(Tree): """Working copy tree. The inventory is held in the `Branch` working-inventory, and the files are in a directory on disk. It is possible for a `WorkingTree` to have a filename which is not listed in the Inventory and vice versa. """ def __init__(self, basedir, inv): self._inventory = inv self.basedir = basedir self.path2id = inv.path2id def __repr__(self): return "<%s of %s>" % (self.__class__.__name__, self.basedir) def _rel(self, filename): return os.path.join(self.basedir, filename) def has_filename(self, filename): return os.path.exists(self._rel(filename)) def get_file(self, file_id): return self.get_file_byname(self.id2path(file_id)) def get_file_byname(self, filename): return file(self._rel(filename), 'rb') def _get_store_filename(self, file_id): return self._rel(self.id2path(file_id)) def get_file_size(self, file_id): return os.stat(self._get_store_filename(file_id))[ST_SIZE] def get_file_sha1(self, file_id): f = self.get_file(file_id) return sha_file(f) def file_class(self, filename): if self.path2id(filename): return 'V' elif self.is_ignored(filename): return 'I' else: return '?' def file_kind(self, filename): if isfile(self._rel(filename)): return 'file' elif isdir(self._rel(filename)): return 'directory' else: return 'unknown' def list_files(self): """Recursively list all files as (path, class, kind, id). Lists, but does not descend into unversioned directories. This does not include files that have been deleted in this tree. Skips the control directory. """ inv = self.inventory def descend(from_dir, from_dir_id, dp): ls = os.listdir(dp) ls.sort() for f in ls: if bzrlib.BZRDIR == f: continue # path within tree fp = appendpath(from_dir, f) # absolute path fap = appendpath(dp, f) f_ie = inv.get_child(from_dir_id, f) if f_ie: c = 'V' elif self.is_ignored(fp): c = 'I' else: c = '?' fk = file_kind(fap) if f_ie: if f_ie.kind != fk: bailout("file %r entered as kind %r id %r, now of kind %r" % (fap, f_ie.kind, f_ie.file_id, fk)) yield fp, c, fk, (f_ie and f_ie.file_id) if fk != 'directory': continue if c != 'V': # don't descend unversioned directories continue for ff in descend(fp, f_ie.file_id, fap): yield ff for f in descend('', None, self.basedir): yield f def unknowns(self, path='', dir_id=None): """Yield names of unknown files in this WorkingTree. If there are any unknown directories then only the directory is returned, not all its children. But if there are unknown files under a versioned subdirectory, they are returned. Currently returned depth-first, sorted by name within directories. """ for fpath, fclass, fkind, fid in self.list_files(): if fclass == '?': yield fpath def ignored_files(self): for fpath, fclass, fkind, fid in self.list_files(): if fclass == 'I': yield fpath def get_ignore_list(self): """Return list of ignore patterns.""" if self.has_filename(bzrlib.IGNORE_FILENAME): f = self.get_file_byname(bzrlib.IGNORE_FILENAME) return [line.rstrip("\n\r") for line in f.readlines()] else: return bzrlib.DEFAULT_IGNORE def is_ignored(self, filename): """Check whether the filename matches an ignore pattern. Patterns containing '/' need to match the whole path; others match against only the last component.""" ## TODO: Take them from a file, not hardcoded ## TODO: Use extended zsh-style globs maybe? ## TODO: Use '**' to match directories? for pat in self.get_ignore_list(): if '/' in pat: if fnmatch.fnmatchcase(filename, pat): return True else: if fnmatch.fnmatchcase(splitpath(filename)[-1], pat): return True return False class RevisionTree(Tree): """Tree viewing a previous revision. File text can be retrieved from the text store. :todo: Some kind of `__repr__` method, but a good one probably means knowing the branch and revision number, or at least passing a description to the constructor. """ def __init__(self, store, inv): self._store = store self._inventory = inv def get_file(self, file_id): ie = self._inventory[file_id] f = self._store[ie.text_id] mutter(" get fileid{%s} from %r" % (file_id, self)) fs = filesize(f) if ie.text_size is None: note("warning: no text size recorded on %r" % ie) self._check_retrieved(ie, f) return f def get_file_size(self, file_id): return self._inventory[file_id].text_size def get_file_sha1(self, file_id): ie = self._inventory[file_id] return ie.text_sha1 def has_filename(self, filename): return bool(self.inventory.path2id(filename)) def list_files(self): # The only files returned by this are those from the version for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id class EmptyTree(Tree): def __init__(self): self._inventory = Inventory() def has_filename(self, filename): return False def list_files(self): if False: # just to make it a generator yield None ###################################################################### # diff # TODO: Merge these two functions into a single one that can operate # on either a whole tree or a set of files. # TODO: Return the diff in order by filename, not by category or in # random order. Can probably be done by lock-stepping through the # filenames from both trees. def file_status(filename, old_tree, new_tree): """Return single-letter status, old and new names for a file. The complexity here is in deciding how to represent renames; many complex cases are possible. """ old_inv = old_tree.inventory new_inv = new_tree.inventory new_id = new_inv.path2id(filename) old_id = old_inv.path2id(filename) if not new_id and not old_id: # easy: doesn't exist in either; not versioned at all if new_tree.is_ignored(filename): return 'I', None, None else: return '?', None, None elif new_id: # There is now a file of this name, great. pass else: # There is no longer a file of this name, but we can describe # what happened to the file that used to have # this name. There are two possibilities: either it was # deleted entirely, or renamed. assert old_id if new_inv.has_id(old_id): return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id) else: return 'D', old_inv.id2path(old_id), None # if the file_id is new in this revision, it is added if new_id and not old_inv.has_id(new_id): return 'A' # if there used to be a file of this name, but that ID has now # disappeared, it is deleted if old_id and not new_inv.has_id(old_id): return 'D' return 'wtf?' commit refs/heads/master mark :6 committer 1110343865 +0000 data 25 import all docs from arch from :5 M 644 inline doc/adoption.txt data 4504 **************** Driving adoption **************** Getting adoption means persuading people that it's a good choice. I think the key is to have something that key project leaders see as worth using. Imagine what it would take to get tridge, havoc, or akpm to switch. Or not even to switch, but to even just try it out. * Simple operations must be simple. * The project and the implementation must not have bad smells. * Given their current understanding of the problem, there must be at one feature that's clearly better than what they're currently using. What holds it back now? * Too complex on initial impression * Bad smell from having so many forks/wrappers/kooky opinions * Some of the more exotic features can only be appreciated on familiarity * It doesn't actually achieve by default a lot of the advantages that it ought to: for example it still blocks on the network Good features at the moment * Archive storage is clean; probably makes a favorable impression on people who look at it * Relatively few dependencies (if you don't look too closely at hackerlab, etc) From `Ben Collins-Sussman`__ __ http://www.red-bean.com/sussman/svn-anti-fud.html If you're learning about Subversion and thinking of using it in your group or company, please approach it the way you'd approach any new product: with caution. This isn't to say that Subversion is unreliable... but that doesn't mean you shouldn't use some common sense either. Don't blindly jump into the deep end without a test-drive. No user wants a new product forced upon them, and if you're going to be responsible for administering the system, you better have some familiarity with it before rolling it out to everyone. Find a smallish project, and set it up as a "pilot" for Subversion. Ask for enthusiastic volunteers to test-drive the experiment. In the end, if Subversion turns out to be a good fit, you'll have much happier developers (who have been part of the process from the start) and you'll be ready to support a larger installation as well. [...] When Subversion hit "alpha" it was already being used by dozens of private developers and shops for real work. Any other project probably would have called the product "1.0" at that point, but we deliberately decided to delay that label as long as possible. Because we're talking managing people's irreplaceable data, the project was extremely conservative about labeling something 1.0. We were aware that many people were waiting for that label before using Subversion, and had very specific expectations about the meaning of that label. So we stuck to that standard. All it takes is one high-profile case of data loss to destroy an SCM's reputation. `John S. Yates, Jr.`__: __ http://lists.gnu.org/archive/html/gnu-arch-users/2004-10/msg00370.html First let me say that I have nothing but increasing respect for Tom's skills and accomplishments. That said I see Gnu Arch as really just emerging from a period of prototype development. If the project really wants to take over the world, and especially supplant projects with momentum and commercial customers (e.g. Subversion, BitKeeper, etc) then I would warn against two mistakes I have experienced repeatedly in my career: 1) Deferring to a tiny installed base instead of focusing on eliminating barriers to adoption 2) Believing that great technology will be irresistible no matter how it is presented Appearances matter. Expectations matter. Standards (official or de facto) matter. There is also a `reply from Tom`__. __ http://lists.gnu.org/archive/html/gnu-arch-users/2004-10/msg00430.html Clear wins ---------- To convince people to use Baz, there has to be some feature they can clearly understand which will be much better under Baz. It must be something they do today. * Offline support * Almost no network delays * Atomic changes (svn already has this) * Correct repeated merges * Read-only mirroring archives (not really important) My model is that people will consider changing if 1. it's at least as good as cvs/svn 2. AND there are no big concerns about implementation/safety 3. AND there is at least one feature which is easy to use and a big win This gets you to some people at least trying it. Will people migrate big projects to it? Maybe, if it looks safe, it fixes there problem, and it doesn't look like something substantially better is on the horizon. M 644 inline doc/bitkeeper.txt data 898 Bitkeeper compared to Arch ========================== BK has a default GUI, which is good, but it's ugly and not all that friendly to the new user. It does pay attention to allowing quick keyboard navigation. The tool itself is also a bit unfriendly in terms of emitting a lot of noise messages and having lots of wierd commands. Bitkeeper always requires both per-file and per-changeset commands. It seems bad to always require this; at most per-file messages should be optional. Are they really the best option? Claimed to have extremely space-efficient storage, keeping 19 years of history in slightly more than twice the size of the working copy. Hashes: does Baz always have these even without signing? It really should. Fine-grained event triggers, pre- and post-event. Can remotely find status of a tree, e.g. parent, number of comitters, versioned files, extras, modified, etc. M 644 inline doc/changelogs.txt data 1320 ********** ChangeLogs ********** Changelogs have an interesting relation to version control systems. They are, for some projects, the primary way of examining history. They provide a pattern and standard for writing commit history, which may encourage people to enter more details (function names, etc) in a systematic way than they otherwise might. The ChangeLog is available offline, in tarfiles, or even when the project has been switched into a different VCS. GNU ChangeLog format is only the most important, not the only such file. Debian uses a different format for packages. At a higher level, At the same time the information there is some redundancy: both the ChangeLog and the VCS want to hold the description of what has been done, including descriptive text, names of files changed, etc. Some people say__ that ChangeLogs are mostly needed because of the limitations of CVS (and originally RCS): if it was easy to read the VC history when disconnected and at local speed, and if there are atomic commits then they might be needed rather less. __ http://gcc.gnu.org/ml/gcc/2004-06/msg00270.html There should be hooks called to create the log message template and before committing it. These might, for example, populate it with space for per-file comments, or check that it is in the right format. M 644 inline doc/cherry-picking.txt data 1632 ************** Cherry picking ************** Bazaar-NG allows you to try to 'cherry pick' changes from one branch to another. That is to say that you can merge across only the changes from a particular commit, without getting anything else from that branch. However, because of dependencies in the source, it's not always very practical to pick changes out of a widely-diverged branch. The general principle is that it's easier to keep things separate and combine them later than to mix things up and try to separate them. Therefore, we suggest using branches for each line of development that might want to separately merge. Create a branch for each major feature; create branches for code that is ready to go upstream and for code that is very experimental. Branches are cheap. As changes pass through these branches they accumulate interesting metadata. Design ------ There is little that a tool can do to help with merges once you start cherry-picking changes; we need to rely on the merge tool detecting that some changes have already been applied. We should note that the changes have already come across, but I don't see many places where that will be terribly helpful. Sometimes a succession of cherry picks will be equivalent to merging the whole history up to some point, in which case we can fall back to a proper merge. When choosing a merge ancestor I think knowing non-contiguous cherry-picked changes will not help. Cherry-picking is equivalent to putting the merge ancestor just before the changeset(s) to be picked in. So we can do this regardless of whether we have the whole history of the tree. M 644 inline doc/cmdref.txt data 25271 *********************** Reference for Bazaar-NG *********************** :Author: Martin Pool :Copyright: Copyright 2004, 2005 Martin Pool; GNU GPL v2 .. contents:: Generalities ------------ Note that this document refers to many features which are not implemented yet. Some may never be implemented. The top level command is ``bzr``. Everything else is invoked as a subcommand of that, as is common in version control systems. A source directory containing some files to be versioned is called a *tree*; this contains a number of *files* and possibly some subdirectories. The point of bzr is to keep track of the *versions* (or *revisions*) of the tree at various points in time. A sequence of versions of a tree form a *branch*. All branches begin with the empty directory, called the null revision. Two branches may have some revisions in common and then *diverge* later -- which is why they're called branches. We can distinguish the *working copy* of the files, which is available for modification, from their previous versions. Each file has a unique *file-id*, which remains the same for the life of the file, even when files are renamed. Files also have a *kind*, which can currently be *file* or *directory*, and which cannot change for any file-id. The difference between two revisions is a *changeset*. Changesets include the textual changes to files, the affected file-ids, the date of the change, the author, the name of the branch. Changesets have a globally unique identifier. A changeset committed to a branch is *local* to that branch; otherwise it is foreign. (Note that a changeset may be local to several different branches if it was created prior to their divergence.) Changesets may include a note that they *incorporate* other changesets; this is used when changes are merged from one branch into another. After changes have been merged but before they are committed they are listed as *pending merges*; when they are committed they are listed within their changeset. All files in a versioned directory can be divided into four classes: Versioned Anything assigned a file-id. Changes to these files are tracked. Control Anything under ``.bzr/``. Should not be edited by users. Ignored Anything matching an ignore pattern. As the name suggests, ignored by Bazaar. Unknown Everything else. Generally ignored, except that they are reported as 'unknown' and can be added by a recursive add_. We say a tree, or a file, is *unmodified* if it is the same as in the most recent version, or *modified* otherwise. *Committing* creates a new revision equal to the current state of the tree. Immediately after committing, all files are unmodified, since they are the same as the just-committed state. File states ----------- A file is in exactly one of these *states*. These are identified by single characters which should be familiar to people used to svn/cvs. ?, Unknown Not versioned, ignored, or control. Typically new files that should be versioned or ignored but have not been marked as either yet. ., Up-to-date This versioned file is the same as in the last revision on this branch. (The file had the same name and text and properties in the previous revision.) A, Added This file will be added by the next commit. (The file ID was not present in the previous revision.) M, Modified This file has been changed from the previous revision and will be recorded in the next commit. (The file had the same name but different text in the previous revision.) Directories have no text and so can never be in this state. D, Deleted This file has been removed from the inventory or deleted in the working copy and will be removed in the next revision. R, Renamed This file had a different name in the previous revision; the next commit will record the new name. The file text may also have been modified. These are only summaries, not complete descriptions. For example the 'D' state does not distinguish between a file removed by the remove_ command and one whose text has been deleted. (There should perhaps be a variation of the ``info`` command, or a ``file-info`` that shows enough details.) A directory may be up-to-date even if some files inside it have different states. .. _pinned: pinned.html Global options -------------- These can be applied to many or all commands. --help Show help, either globally or for the selected command. -v, --verbose Show progress/explanatory information. This is good if for example using a slow network and you want to see that something is happening. --debug Show way too much detail about files being opened, locking, etc. Intended for --silent Show nothing but errors. (Perhaps unnecessary; you could just redirect stdout to /dev/null.) --version Show version and quit. --dry-run Show what would be done, but don't make any permanent changes. --directory DIR Operate in given directory rather than cwd. (Perhaps this should be ``-d``, but that might be better saved for ``--delete``. Perhaps -b?) --recurse, -R When a directory name is given, operate not only on that directory but also any files or directories contained within it. (XXX: Should this be on by default? It will often be what people want, but also possibly more surprising. If it is, we will want a ``--no-recurse``.) --force Relax safety checks. --format=FORMAT Choose output format; options might include XML, YAML, text. --id-only Only update the inventory, don't touch the working copy. (May need a better name.) --show-ids List file ids, as well as names. init ---- Create bzr control files in the current directory:: bzr init Use this, followed by add_ and then commit_ to import or start a new project. add --- Add one or more files or directories to the branch:: bzr add FILE... Each added file is assigned a new file-id, and will be in the Added state. They will be recorded in the next commit that covers that file, and then in the Up-to-date state. +-------------+-------------------------------------------------+ |File state |Action | +=============+=================================================+ |? |New file id given to file; now in A state | +-------------+-------------------------------------------------+ |I |"Adding previously ignored file"; now in A | +-------------+-------------------------------------------------+ |Z, ZM |Add file with new ID; now in A state. | | |(To get the old ID back, use revert_ on that | | |file.) | +-------------+-------------------------------------------------+ |U, A, M, R, |Warning "already added"; no change | |RM | | +-------------+-------------------------------------------------+ |!, D |Error "no such file"; no change | +-------------+-------------------------------------------------+ |# |Error "cannot add control file"; no change | +-------------+-------------------------------------------------+ --id ID Add file with given ID rather than a random UUID. Error if this id is assigned to any other file. (not implemented yet) --recurse Add directory and all Unknown children, recursively. This includes U children of previously-added subdirectories. (not implemented yet) This command will add any new source files, except for those matching ignore rules:: $ bzr add --recurse . (Note: I hope this might satisfy people who are fond of arch name-based tagging, and who dislike individually adding & removing files. All they need to do is set up the appropriate ignore patterns, then 'add -R .' and they're done.) remove ------ Make a file no longer be versioned, and record its deletion from the inventory:: $ bzr remove FILE... This does not remove the working copy. If you wish to both remove the working copy and unregister the file, you can simply delete it using regular ``rm``. This is the opposite of add_. :State table: +--------------+--------------------------------------------------+ | File state | Action | +==============+==================================================+ | M, R, RM, . | File to D state. | +--------------+--------------------------------------------------+ | A | File back to I or ? state | +--------------+--------------------------------------------------+ | I, ?, Z, ZM | Error, no change | +--------------+--------------------------------------------------+ | ! | Change to D | +--------------+--------------------------------------------------+ | D | Warning "already deleted" | +--------------+--------------------------------------------------+ | # | "Cannot remove control file" | +--------------+--------------------------------------------------+ diff ---- Show all changes compared to the pristine as a text changeset. --full Include details for even binary files, uuencoded. Makes the diff long but lossless. export ------ :: bzr export TO-DIR Copy the tree, but leave out Bazaar-NG control files. This includes copying uncommitted changes. This is equivalent to copying the branch and then deleting the control directory (except more efficient). Should this also delete any other control files like ``.bzrignore``? status ------ Show which files are added/deleted/modified/unknown/missing/etc, as in subversion. Given the ``--check`` option, returns non-zero if there are any uncommitted changes. info ---- Display various information items about the branch. Can be given various options to pull out particular fields for easier use in scripts. Should include: * branch name * parent * number of revisions * number of files that are versioned/modified/deleted/added/unknown * number of versioned directories * branch format version * number of people who have made commits * date of last commit delta ----- Compute a two-way non-history-sensitive delta from one branch or version to another. Basically a smart diff between the two. (Perhaps this should just be a ``--format`` option to diff_?) merge ----- Merge changes in from another branch, and leave them uncommitted in this tree:: merge [FROM-BRANCH] This makes a note of the revisions which were merged. A range of revisions may be specified to cherry-pick changes from that branch, or to merge changes only up to a certain point. Merge refuses to run if there are uncommitted changes unless forced, so that merged changes don't get mixed up with your own changes. You can use a merge rather than an update to accomplish any of several things: * Merge in a patch, but modify it to either suit your taste or fix textual or semantic conflicts. * Collapse several merged patches into a single changeset. A feature may go through many revisions when being developed on its own branch, but you might want to hide that detail when it merges onto a main branch. --revision RANGE Merge only selected revisions, rather than everything that's not present yet. Before a merge is committed, it may be reversed with the revert_ command. sync ---- :: sync [OTHER-BRANCH] --revision RANGE Pull only selected revisions. Synchronize mirrored branches. A mirror branch is a branch that strictly follows a parent branch, without any changes being committed to it. This is useful in several ways: * Moving a backup of a branch onto another machine to protect against disk failure or laptop theft. * Making the complete history of a upstream branch available for offline use. The result is the same as copying the whole branch, but it is more efficient for remote branches because only newly-added changesets are moved. The result is similar to rsync except it respects Bzr locking. The same command can be used for push mirrors (changesets are moved from this branch to the other) or pull mirrors (vice versa). Bzr automatically determines which to do by looking at which branch has more patches. (Perhaps it would be clearer to have separate *push* and *pull* commands?) This command can only be used when the history of one branch is a subset of the other. If you commit different changes to both branches, then ``sync`` will say that the branches have diverged and it will refuse to run. This command also refuses to run if the destination branch has any uncommitted changes. Uncommitted changes on the origin are not copied. Method: * Get the ordered list of change ids on both branches. * One list should be a prefix of the other; if not, fail. The shorter list will be the destination branch. * Check the destination has no uncommitted changes. * For each change present only in the origin, download it to the destination, add to the changeset history and update the tree and control files. commit ------ :: commit MESSAGE [WHAT] Commit changes from the working copy into the branch. By default commits everything, but can be given a list of files directories or files. Partial commits (not implemented yet) should ideally be allowed even when the partial thing you want to commit is an add, delete or rename. Partial commits are **not** allowed if patches have been merged in this change. Method: * check preconditions * get log, if not present or given on command line * re-check preconditions * calculate diff from pristine to working copy * store this diff, plus headers, into the patches directory * add the diff to the list of applied patches * update pristine tree Commit can optionally run some pre-commit and post-commit hooks, passing them the delta which will be applied; this might mail the delta or apply it to an export and run a test case there. :State table: +-------------+-------------------------------------------------+ |File state |After commit | +=============+=================================================+ |?, I, # |Unchanged | +-------------+-------------------------------------------------+ |A, M, R, RM |Recorded, U | +-------------+-------------------------------------------------+ |D |Gone | +-------------+-------------------------------------------------+ |Z, ZM |Recorded, working copy remains as I or ? | +-------------+-------------------------------------------------+ |! |Error "file is missing"; must be deleted or | | |reverted before commit | +-------------+-------------------------------------------------+ lint ---- Check for problems in the working copy. +-------------+-------------------------------------------------+ |File state |Lint output | +=============+=================================================+ |? |"Unknown file, please add/ignore/remove" | +-------------+-------------------------------------------------+ |Z, ZM |"Zombie file, please add/ignore/remove/revert" | +-------------+-------------------------------------------------+ |! |"File is missing, please recreate, delete or | | |revert." | +-------------+-------------------------------------------------+ |A, M, D, R, |No output | |RM, #, I | | +-------------+-------------------------------------------------+ check ----- Run various consistency/sanity checks on the branch. These might include: * Make sure all patches named by the inventory exist. * No patch IDs are duplicated. * Make sure hash of each patch is correct. * Starting at zero, apply each patch and make sure it does not conflict. * All files named in inventory are reasonable. * No file IDs are duplicated in inventory. * Possibly many more. * No patches are in both ``patch-history`` and ``merged-patches``. * The patches in the bag are exactly those listed in ``patch-history``. Maybe add a separate option to say that you believe the tree is clean. backout ------- Reverse the changes made by previous changesets:: bzr backout REVISION bzr backout REV1 REV2 If a single revision is given, that single changeset is backed out. If two revisions are given, all changes in that range are backed out. The change that is reversed need not be the most recently committed change, but if there are revisions after the ones to be reverse which depend on them this command may cause conflicts. This undoes the changes but remembers that it was once applied, so it will not be merged again. Anyone who pulls from later versions of this tree will also have that patch reversed. You can backout a backout patch, etc, which will restore the previous changes. This leaves the changeset prepared but not committed; after doing this you should commit it if you want. You can also backout only the parts of a changeset touching a particular file or subdirectory:: bzr reverse foo.c@31 revert ------ Undo changes to the working copy of files or directories, and discard any pending merge notes:: $ bzr revert [FILE...] If no files are specified, the entire tree is reverted, which is equivalent to specifying the top-level directory. In either case, the list of pending merges is also cleared. This does not affect history, only the working copy. A corollary is that if no changes have been made to the working copy, this does nothing. If a file was Modified, it is returned to the last committed state, reversing changes to the working copy. If the file has been Added, it is un-added but the working copy is not removed, so it returns to either the Unknown or Ignored state. If the file has been Deleted, it is resurrected and returns to the Up-to-date state. If the file is Unknown, Ignored, or a Control file then it is not changed and a warning is issued:: bzr: warning: cannot revert ignored file foo.c If the file has been Renamed, it is returned to its original name and any textual changes are reversed. This may cause an error if the rename clashes with an existing file:: bzr: error: cannot revert foo.c to old name bar.c: file exists If a directory is listed, by default only changes to the directory itself are undone. Ideally this would not lose any changes, but rather just set them aside, so that the revert command would be undoable. One way is to follow Arch and write out the discarded changes to a changeset file which can be either re-applied or discarded at a later date. This very nicely allows for arbitrarily nested undo. A simpler intermediate mechanism would be to just move the discarded files to GNU-style tilde backups. --merges Clear the list of pending merges. If files are specified, their text is also reverted, otherwise no files are changed. --id-only Don't touch the working text, only the inventory. +-------------+-------------------------------------------------+ | File state | Actions | +=============+=================================================+ | A | Returned to ? or I state, working text unchanged| +-------------+-------------------------------------------------+ | D | Working copy restored, returned to U | +-------------+-------------------------------------------------+ | Z | Returned to U | +-------------+-------------------------------------------------+ | ZM | Working copy restored to previous | | | version, returned to U | +-------------+-------------------------------------------------+ | R | Moved back to old | | | name. | +-------------+-------------------------------------------------+ | RM | Moved back to old name and restored to previous | | | text. | +-------------+-------------------------------------------------+ |I, #, ? | "Cannot revert" | +-------------+-------------------------------------------------+ log --- Show a log of changes:: bzr log By default shows all changes on this branch. The default format shows merged changes indented under the change that merged them. Alternatively changes can be sorted by date disregarding merges, which shows the order in which they were written not the order they were merged. Changes which were merged are flagged as such. Such an order is needed for a GNU-style ChangeLog. The option is comparable to choosing a threaded or unthreaded display in an email client, and should perhaps have those names. Another option is to show just short descriptions of the merged changes, similar to arch. The log can be filtered in any of these ways: * Logs touching a particular file or directory. * Changes touching a particular file-id, regardless of what name it had in the past. * Changes by a particular author. * Changes from a particular branch name (not necessarily the same branch). Another option is to also include diffs (which may make it quite large). ignore ------ Mark a file pattern to be ignored and not versioned:: bzr ignore PATTERN The pattern should be quoted on Unix to protect it against shell expansion. The pattern is appended to ``.bzr-ignore``. This file is created if it does not already exist, and added if it is not already versioned. bzr prints a message showing the pattern added so that people can see where to go to remove it:: $ bzr ignore \*.pyc bzr: notice: created .bzr-ignored bzr: notice: added pattern '*.pyc' to .bzr-ignore $ bzr status A .bzr-ignore If the wrong pattern is added it can be removed by either editing ``.bzr-ignore`` or by reverting__ that file. __ revert_ is -- Test various predicates against a branch, similar to the Unix shell ``test`` command:: bzr is TEST [ARGS] Takes a third-level command: ``clean`` Tree has no uncommitted changes. ``in-tree`` Pwd is in a versioned tree. ``in-control-dir`` Pwd is inside a Bazaar-NG control directory (and therefore should not be modified directly). ``tree-top`` Pwd is the top of a working tree. protect ------- (Proposed idea, may not be implemented or may need a better name.) Sets a voluntary write-protect flag on a branch, to protect against accidental changes:: bzr protect bzr unprotect This is typically used on branches functioning as tags, which should not normally be committed to or updated. There may, in the future, be a mechanism to allow only particular users to protect/unprotect a branch. uncommit -------- (It is not certain this command will be implemented.) This command removes the most recent revision from the branch:: bzr uncommit This does not affect the working copy, which can be fixed up and a new commit run. The new commit will have a different revision ID. Removal of the revision will not propagate to any other branches. The command may be repeated to successively remove more and more revisions. This command should perhaps have a more obviously dangerous name, since it can lose information or cause confusion. By default the revision is removed from the history, but its text is left in the store, which allows some chance of recovery. You cannot partially uncommit; you can however uncommit the whole revision and then re-commit just part of it. :Use cases: `Wrong commit message`_ .. _`Wrong commit message`: use-cases.html#wrong-commit-message find ---- Finds files, versions, etc in a branch:: bzr find [OPERATORS] The behaviour is similar to regular unix *find*, but this understands about bzr versioning. Eventually this may gain all the and/or/grouping options of Unix find, but not yet. This does not have the quirky syntax of unix find, but rather just specifies commands as regular words. Operators: * ``directory`` * ``file`` * ``unknown`` * ``ignored`` By default the operators are anded together; there is also an ``or`` operator. If no action is specified, just prints the file name. Other actions: ``print`` Print just the filename. ``printf`` Print various fields about the object, using a formating system. ``exec`` Execute a command on the file. M 644 inline doc/common-format.txt data 1092 *********************** Common changeset format *********************** It might be useful to have a common changeset format for interchange between projects. It is not clear that you would be able to capture everything that every tool produces, but perhaps a lot could be done. Scenarios: (in order of difficulty?) * dump/load * convert repo from one tool to another * one-way sync from exotic tool into read-only CVS or Svn repo * two way sync between writable archives Perforce's RevML and ``vcp`` may be some use but apparently don't go all the way. Things like darcs and monotone may have such a different model that it may be hard to map them. Two-way sync may require keeping external state and is probably pretty hard. Apparently tlord tried to do something about this before but it stalled. Eventually perhaps we could arrange for someone to export key bitkeeper archives in this format, allowing all tools to read them in. Colin__ points the existing mailing list about this which is just a bunch of spam. __ http://web.verbum.org/blog/freesoftware/fsrc-responses M 644 inline doc/compared-aegis.txt data 4275 **************** Ideas from Aegis **************** * Very mature -- in use for 14 years, many large projects, etc. * "Essential process can be learned in a day" -- which is still kind of a long time; i'd like it to be well under an hour. * Good process integration; show who is supposed to be working on what and how far through they are. * Very poor Windows support. * Distributed repositories. (??) * Very focussed on security -- can reproduce any previous revision; availability/integrity/confidentiality; uses Unix permissions and seteuid() to prevent users changing the database. * Does not itself track history -- assumes this will be done by some other tool such as RCS operating on the baseline. * I think every individual project needs a single baseline. (??) * The baseline is always working and always releasable -- to the extent that you have scripts which can enforce this. * Integration step is somewhat similar to that used by distributed systems. It seems that you could build some features of aegis as policy macros on top of Bazaar-NG. * The baseline also contains object files built from current source, which can be used to pre-populate working directories. Also people only need a copy of the source files they're changing. I think the economics of this have changed a bit. Also tends to assume all developers are on the same Unix host, which is no longer generally true. * Only one review/integration can be in process at a time. * One process difference is that developers produce all changes; reviewers/integrators can only accept or reject them. This is different from the integrator-makes-right model of many distributed tools. (Though the integrator still has the choice to reject, but they have the option of fixing it too.) * Can automatically append Signed-off-by field. Interesting idea. I wonder if we should have a metadata facility to include licence data? * Much of the functionality of Aegis is to prevent people doing things they could otherwise do. That can be useful in enforcing a healthy process but bazaar-ng is not the place for it. * Can serialize an (in-progress) changeset to text, and then mail from one place to another. * Branches are an extension of the 'change' concept; they can be merged into the parent in the same way that a change can be. Merging branches onto the mainline seems to hit a similar problem to that of centralized branches. If someone else has committed, you need to make a new changeset reconciling all their changes, commit that to the child branch, then commit everything to the parent. This suggests a different way to do shared branches in Bazaar-NG: You can push to the parent if you incorporate either directly or by merger all changes on the parent. That means that everything someone has committed to the parent is present in some way in your branch. By extension, it is safe to transform the parent text into your branch without losing anything. We can therefore remotely record that changeset to the parent. This is more or less what Aegis does. The problem with this in the Bazaar-NG model is that then the new commit will be only in the parent, and not on the child. So if you run ``bzr log`` on the child, you won't see what you just committed. We can't apply it to both because the predecessor is different. (darcs could do that, but it has a looser patch history than I want.) Overall, the process model is good for a particular type of organization. It would be good to build this on top of Bazaar-NG. To support that we need: - patches are submitted, rather than being directly written in - arbitrary levels of branching/review - users can submit changes to branches they are not directly allowed to write to - branches can be cleanly removed when they're no longer necessary - strong audit trail Interestingly, the BitKeeper model which is criticized__ by Greg Hudson is similar to that of Aegis: a single integrator (or small team?) who ultimately decide what gets into the main tree. __ http://web.mit.edu/ghudson/thoughts/bitkeeper.whynot The Aegis workflow can probably be `emulated in bzr `_. M 644 inline doc/compared-codeville.txt data 2608 Codeville ********* Documentation on how this actually works is pretty scarce to say the least. I *think* I understand their merge algorithm though, and it's pretty clever. Basically we do a two-way merge between annotated forms of the two files: that is, with each line marked with the revision in which it last changed. (I am simplifying here by speaking of lines and changes, but I don't think it causes any essential problem.) Now we walk through each file, line by line. If the change that introduced the line state in branch A is already merged into branch B, then we can just take B. Now is this actually better? It may be better in several ways: * Do not need to choose just a single ancestor, but rather can take advantage of all possible previous changes. * Can handle OTHER containing changes which have been merged into THIS, but have then been overwritten. * Can handle cherrypicks(!) by remembering which lines came in from that cherrypick; then we don't need to merge them again. Some questions: * Do we actually need to store the annotations, or can we just infer it at the time we do the merge? * Can this be accomodated in something like an SCCS weave format? I think something like a weave may work, in as much as it is basically a concatenation of annotations, but I don't know if it represents merges well enough to cope. Can this handle binaries or type-specific merges, and if so how? Unmergeable binaries are easy: just get the user to pick one. Things like XML are harder; we probably need to punt out to a type-specific three-way merge. Of course this approach does not forbid also offering a 3-way merge. ---- I suppose this could be accomodated by an annotation cache on top of plain history storage, or by using a storage format such as a weave that can efficiently produce annotation information. That is to say there is nothing inherently necessary about remembering the line history at the point when it is committed, except that it might be more efficient to do this once and remember it than to ---- There is another interesting approach that can be used even in a tool that does not inherently remember annotations: Given two files to merge, find all regions of difference. For each such, try to find a common ancestor having the same content for the region. Subdivide the region if necessary. This naive approach is probably infeasible, since it would mean checking every possible predecessor. ---- So the conclusion is that this is very cool, but it does not require a fundamental change of model and can be implemented later. M 644 inline doc/compared-cvsnt.txt data 816 ***** CVSNT ***** http://www.cvsnt.com/cvspro/ * Remembers last-merge-point__ between two branches, and uses this to do smarter incremental merges. __ http://www.cvsnt.org/wiki/MergePoint http://www.cvsnt.org/wiki/CvsntAdvantages * SSPI authentication on Windows, and built-in Putty SSH code. * Branch ACLs. * Remote user administration using cvs passwd command. * Repository browsing via cvs ls command (update: now provided in standard CVS since version 1.12.9) * LockServer on a second port replaces filesystem-based locks & provides file level locking Various other improvements relative to CVS, particularly for Windows users. Worth looking at, but nothing apparently earth-shaking. Interesting that the advanced merge support is basically that it remembers what was pulled across. M 644 inline doc/compared-opencm.txt data 1220 ****************** Compared to OpenCM ****************** http://opencm.org/ Not very stable; apparently inactive. * Separated working copy & repository model. * Files have long-lived identifiers. * Files and revisions identified by the SHA-1 hash of their content (as in monotone); explicitly makes it easier to be sure we have the right one and prevents some tricks. * Binding of objects to external names done on the client, so you can version e.g. database objects, instead of files. * Directories are inferred by having files that exist under them; empty directories are a special case with an object of type DIR. This is a bit ugly. I might rather have files given a name only relative to their parent directory. So renaming a directory will only update the entry for the directory, and everything will move with it. * Access-control rules about who can write to a central server. * Their design is somewhat similar to ours and used a lot of disk space -- enough to be a significant problem. * Assigning human names to branches proved problematic -- i think this is a good reason to rely on the filesystem/URL space, which people already know how to manage and deal with. M 644 inline doc/compared-prcs.txt data 1483 ******************** Comparison with PRCS ******************** http://prcs.sourceforge.net/ * No network support; no Windows or Mac support. * Long-lived file IDs, generated from . Files can be reintroduced after they are deleted. * Named branches. * Custom keyword replacement: can specify keywords such as the release number. Commands to update keywords, and to strip them out. Separate ``rekey`` command can be used after a checkin to bring keywords up to date. * Users are allowed to edit the manifest file, which also contains some system-maintained data. This apparently works OK but I think it makes people uncomfotable. * ``populate`` command adds all unversioned files and unregister files which are no longer present. * Merge algorithm is similar to the best-parent three-way merge we're planning on using; in particular it remembers merged parents to help with later choices. * ``$Format$`` feature useful in generating arbitrary tags inside text, example__:: /* $Format: "static char* version = \"$ProjectVersion$\";"$ */ static char* version = "x.x"; transformed to:: /* $Format: "static char* version = \"$ProjectVersion$\";"$ */ static char* version = "1.2"; Also a good bundle of other keywords. * ``prcs execute`` is like find-xargs; perhaps easier to have a find/exec pattern. __ http://svn.haxx.se/dev/archive-2004-12/1065.shtml Should have a look at their storage format; probably pretty efficient. M 644 inline doc/compared-teamware.txt data 1123 Compared to Sun Teamware ************************ (**note**: I have never used Sun Teamware, so this document is just based on public documentation and information from other people. Corrections would be appreciated.) http://docs.sun.com/app/docs/doc/806-3573 Needless to say bazaar-ng is currently far less mature, and some of the advantages listed below don't work in the current pre-1.0 code. But they are accounted for in the design. TeamWare has file locking. Distributed systems can't easily support file locking because you can't prevent people diverging. But perhaps we can have scripts or a higher-level tool to communicate that changes to a particular file are in train. (One possibility: there is a development branch where that file has been changed, but not yet checked in. Or if Bob can see Alice's work area, he can see that a particular file has been fetched read/write.) parent/child workspaces. Advantages of bazaar-ng: * Free / open source software * Prior tree revisions are always exactly reproducible (atomic changesets, etc) * Can get whole-tree diff * Versioned, mergeable renames. M 644 inline doc/compression.txt data 2453 Compression *********** At the moment bzr just stores the full text of every file state ever. (Files unmodified from one revision are stored only once.) This is simple to write, and adequate for even reasonably large trees with many versions. Disk is very cheap. Eventually we might like something more compressed, but this is neither an interesting nor urgent problem. (Not "interesting" in the sense that doing it is just a matter of coding; there is no theoretical problem or risk.) There are various possibilities: * Store history of each file in RCS, relying on RCS to do line-by-line delta compression. (Does not handle binaries very well.) OpenCMS paper has a horror story about using RCS for file storage. * Store full copies of each file in a container with gzip compression, which should fairly efficiently eliminate unchanged areas. This works on binaries, and gives compression of file text as a side benefit. (Note that ``.zip`` will *not* do for this, because every file is compressed independently.) The OpenCMS paper notes that RCS storage is only 20% more efficient than gzip'd storage of individual file versions. * Store history of each file in SCCS; allows quick retrieval of any previous state and may give more efficient storage than RCS. Allows for divergent branches within a single file. * Store xdeltas between consecutive file states. * Store xdeltas according to a spanning delta algorithm; this probably requires files are stored with some kind of sequence number so that we can predict related version names. * Store in something like XDFS. * Any of the above, but with the final storage in some kind of database: psql, sqlite, mysql, whatever is convenient. It should be something that is safe across system crashes, which rules out tdb at the moment. ---- These properties are seen as desirable in darcs and arch: * Passive HTTP downloads: without requiring any server-side intelligence, a client can get updates from one version to the next by requesting a set of self-contained files. The number of files necessary to do this must not be unfeasibly large, and the size of each of those files should be proportionate to the amount of actual change. In other words the data downloaded should be of comparable size to the actual edits between the trees. * Write-once storage: once a file is written to the repository, it is not modified. M 644 inline doc/config-specs.txt data 1788 ************ Config specs ************ A *config spec* is a set of instructions for assembling a source tree from separately maintained branches. This is like CVS's "modules" format. Config specs are very commonly used in Clearcase, and the main way of getting a new checkout. Scenarios: * One module is shared by several projects, and it needs to be present in a subdirectory of their trees to build. * There are different commit or stability rules for different parts of a tree. The simplest form is just a specification of branch sources, and where they should be assembled into the tree. Example:: . http://foo.net/fooapp ./libbaz http://foo.net/libbaz Normally the entire branch will be placed at this point, but sometimes subdirectory should be selected. Selecting just a single file rather than subdirectory might be problematic because we wouldn't have anywhere to put the control files. Each branch will have a .bzr directory at the top level. Changes on each branch can be pushed back independently. The tool needs to support (and be tested with) nested checkouts. Commands should normally only apply to a single tree, but there might be an option to descend into nested trees. One very important use case is a naive user checking out a tree that happens to use a configspec. This is transparent in cvs, but not at all so in Arch. The ``svn:externals`` mechanism in Subversion works like this. One interesting idea from Aaron is that all branches should be like this: the latest-patch pointer can be generalized to in fact name several patches that should be assembled in subdirectories. A simplest branch names just one, assembled to the base directory. You might want to specify the parent branches for each one as well.... M 644 inline doc/conflicts.txt data 1387 Types of conflict ***************** The following types of conflict can be encountered during a merge, or a merge-like operation (such as resyncing local changes with upstream): Aaron's list: * Parent directory missing when attempting to add a file * Attempt to create directory that already exists * Some patch hunks failed to apply * While changing permissions, the "old" permissions in the changeset do not match the file. * While replacing contents of a file, the old contents of a file do not match the changeset * Attempt to remove non-empty directory: This only occurs when the directory has contents that it didn't have the last time it was deleted. It's pretty easy to handle adds, renames and deletes for exact patching. There's no need to rm -rf directories as tla does. * Attempt to create a symlink when there is already a file/dir/etc with that name * Attempt to apply a patch to a file that does not exist * Attempt to remove a file that does not exist * Attempt to rename a file that does not exist * Attempt to change permissions of a file that does not exist * Can't determine filename during three-way merging * Can't determine file parent directory during three-way merging * Can't determine file permissions during three-way merging Aaaron says: I have no doubt there are more. For example, attempt to change permissions of a symlink. M 644 inline doc/costs.txt data 701 Costs ===== User thought is most expensive. User time spent waiting is next. Developer time for the version control system is also relatively expensive. Machine resources are cheap. In particular; disk is generally very cheap; an ordinary programmer earns enough to buy hundreds of GB of disk per day. However, it is sometimes limited, as on a laptop. Therefore it is generally OK to trade off disk space for anything else, but it should be possible to be compact. Network round trips are very bad. Consider scalability also; avoid anything worse than O(n). Try to avoid anything that needs e.g. to hold the whole tree in memory at any time, or to hold the entire history of the project. M 644 inline doc/darcs.txt data 1113 Darcs compared to Arch ====================== Simpler to use; perhaps harder to completely understand. Always local; always fast. Patch commution is slow and perhaps doesn't clearly do what people want. Too slow! Can't reliably get back to any previous point. Explicitly not addressing source archive/librarian function. Loads everything into memory. Written in Haskell. Breaking commits into hunks at commit time is interesting, but I think not totally necessary. Sometimes it won't break hunks where you want it. A really simple pre-commit check hook is remarkably useful. http://www.scannedinavian.org/DarcsWiki/DifferencesFromArch Token replace ------------- Very cute; possibly handy; not absolutely necessary in most places. Somewhat limited by the requirement that it be reversible. This is one of very few cases where it does seem necessary that we store deltas, rather than tree states. But that seems to cause other problems in terms of being able to reliably sign revisions. This can perhaps be inferred by a smart 3-way merge tool. Certainly you could have it do sub-line merges. M 644 inline doc/deadly-sins.txt data 1585 ************************** Deadly sins in tool design ************************** http://gcc.gnu.org/wiki/DeadlySins They don't directly apply, but many do correspond. The "Deadly Sins" from P. J. Brown's *Writing Interactive Compilers and Interpreters*, Wiley 1979. We've committed them all at least once in GCC. The deadly sins are: 1. to code before you think. 2. to assume the user has all the knowledge the compiler writer has. 3. to not write proper documentation. 4. to ignore language standards. 5. to treat error diagnosis as an afterthought. 6. to equate the unlikely with the impossible. 7. to make the encoding of the compiler dependent on its data formats. 8. to use numbers for objects that are not numbers. 9. to pretend you are catering to everyone at the same time. 10. to have no strategy for processing break-ins. (A break-in is when you interrupt an interactive compiler, and then possibly continue it later. This is meaningful in an environment in which the compiler is run dynamically, such as many LISP and some BASIC environments. It is not meaningful for typical uses of C/C++ (although there was at least one interactive C environment, from Sabre).) (Perhaps this corresponds to handling user interrupts during operation -- they should not leave anything in an inconsistent state.) 11. to rate the beauty of mathematics above the usability of your compiler. 12. to let any error go undetected. 13. to leave users to find the errors in your compiler. M 644 inline doc/design.txt data 41787 **************** Bazaar-NG design **************** :Author: Martin Pool :Date: December 2004, Noosa. .. sectnum:: .. contents:: Abstract -------- *Bazaar-NG should be a joy to use.* What if we started from scratch and tried to take the best features from darcs, svn, arch, quilt, and bk? Don't get the sum of all features; rather get the minimum features that make it a joy to use. Choose simplicity, in both interface and model. Do not multiply entities beyond necessity. *Make it work; make it correct; make it fast* -- Ritchie(?) Design model ------------ * Unify archives and branches; one archive holds one branch. If you want to publish multiple branches, just put up multiple directories. * Explicitly add/remove files only; no names or tagline tagging. If someone wants to do heuristic detection of renames that's fine, but it's not in the core model. Quilt indicates an interesting approach: patches themselves are the thing we're trying to build. We don't just want a record of what happened, but we want to build up a good description of the change that will be implied when it's integrated. This implies that we want to be able to change history quite a lot before merging upstream; or at least change our description of what will go up. Principles ---------- * Unix design philosophy (via Peter Miller), tempered by modern expectations: - least unnecessary output - little dependence on *specific* external tools - short command lines - least overlap with cooperating tools * `Worse is better`__ __ http://www.jwz.org/doc/worse-is-better.html - *Simplicity: the design must be simple, both in implementation and interface. It is more important for the implementation to be simple than the interface. Simplicity is the most important consideration in a design.* - *Correctness: the design must be correct in all observable aspects. It is slightly better to be simple than correct.* - *Consistency: the design must not be overly inconsistent. Consistency can be sacrificed for simplicity in some cases, but it is better to drop those parts of the design that deal with less common circumstances than to introduce either implementational complexity or inconsistency.* - *Completeness: the design must cover as many important situations as is practical. All reasonably expected cases should be covered. Completeness can be sacrificed in favor of any other quality. In fact, completeness must sacrificed whenever implementation simplicity is jeopardized. Consistency can be sacrificed to achieve completeness if simplicity is retained; especially worthless is consistency of interface.* * Try to get a reasonably tasteful balance between having something that works out of the box but also has composable parts. Provide mechanism rather than policy but not to excess. * Files have ids to let us detect renames without having to walk the whole path. If there are conflicts in ids they can in principle be resolved. There might be a ``merge --by-name`` to allow you to force two trees into agreement on IDs. If the merge sees two files with the same name and text then it should conclude that the files merged. It would be nice if there were some way to make repeated imports of the same tree give the same ids, but I don't think there is a safe feasible way. Sometimes files start out the same but really should diverge; boilerplate files are one example. * Archives are just directories; if you can read/write the files in them you can do what you need. This works even over http/sftp/etc. Or at least this should work for read-only access; perhaps for writing it is reasonable to require a svn+ssh style server invoked over a socket. Of course people should not edit the files in there by hand but in an emergency it should be possible. * Storing archives in plain directories means making some special effort to make sure they can be rolled back if the commit is interrupted any time. On truly malicious filesystems (NFS) this may be quite difficult, but at a minimum it should be possible to roll back whatever was uncommitted and get to a reasonable state. It should also be reasonably possible to mirror branches using rsync, which may transfer files in arbitrary order and cannot handle files changing while in flight. Recovering from an interrupted commit may require a special ``bzr fix`` command, which should write the results to a new branch to avoid losing anything. * Branches carry enough information to recreate any previous state of the branch (including its ancestors). This does not necessarily mean holding the complete text of all those patches, but we do store at least a globally unique identifier so that we can retrieve them. * Commands should correspond to svn or cvs as much as possible: add, get, copy, commit, diff, status, log, merge. * We have all the power of mirroring, but without needing to introduce special concepts or commands. If you want somebody's branch available offline just copy it and keep updating to pull in their changes; if you never make any changes the updates will always succeed. * It is useful to be able to easily undo a previous change by committing the opposite. I had previously imagined requiring all patches to be stored in a reversible form but it's enough to just do backwards three-way merges. * Patches have globally unique IDs which uniquely identify them. * As a general principle we separate identification (which must be globally unique) from naming (which must be meaningful to users). Arch fuses them, which makes the human names long and prevents them ever being reused. Monotone doesn't have human-friendly names. * Users are identified by something like an email address; ``user@domain``. This need not actually be a working email address; the point is just to piggyback on domain names to get human-readable globally unique names. * Everything will be designed from the beginning to be safe and reasonable on Windows and Unix. * History is append-only. Patches are recorded along with the time at which they were committed; if time steps backwards then we give a warning (but probably commit anyhow.) This means we can reliably reproduce the state of the branch at any previous point, just by backing out patches until we get back there. This is also true at a physical level as much as possible; once a patch is committed we do not overwrite it. This should make it less likely that a failure will corrupt past history. However, we may need some indexes which are updated rather than replaced; they should probably be atomically updated. * Storage should be reasonably transparent, as much as possible. (ie don't use SQLite or BDB.) At the same time it should be reasonably efficient on a wide range of systems (ie don't require reiserfs to work well.) Programmers who look behind the covers should feel comfortable that their data is safe, and hopefully pleased that the design is elegant. * Unrecognized files cause a warning when you try to commit, but you can still commit. (Same behavior as CVS/Subversion; less discipline than Arch.) If you wish, you can change this to fail rather than just warn; this can be done as tree policy or as an option (eg ``commit --strict``) * Files may be ignored by a glob; this can be applied globally (across the whole tree) or for a particular directory. As a special convenience there is ``bzr ignore``. * If branches move location (e.g. to a new host or a different directory), then everyone who uses them needs to know the new URL by some out-of-band method. * All operations on a branch or pair of branches can be done entirely with the information stored in those branches. Bazaar-NG never needs to go and look at another branch, so we don't need unique branch names or to remember the location of branches. * Store SHA-1 hashes of all patches, also store hashes of the tree state in each revision. (We need some defined way to make a hash of a tree of files; for a start we can just cat them together in order by filename.) Hashes are stored in such a way that we can switch hash algorithms later if needed if SHA-1 is insecure. * You can also sign the hashes of patches or trees. * All branches carry all the patches leading up to their current state, so you can recreate any previous state of that branch, including the branches leading up to it. * A branch has an append-only history of patches committed on this branch, and also an append-only history of patches that have been merged in. * A commit log message file is present in .bzr-log all the time; you can add notes to it as you go along. Some commands automatically add information to this file, such as when merging or reversing changes. The first line of the message is used as the summary. * Commands that make changes to the working copy will by default baulk if you have any uncommitted changes. Such commands include ``merge`` and ``reverse``. This is done for two reasons: to avoid losing your changes in the case where the merge causes problems, and to try to keep merges relatively pure. You can force it if you wish. (*pull* is possibly a special case; perhaps it should set aside local changes, update, and then reapply them/remerge them?) * Within a branch, you can refer to commits by their sequence number; it's nice and friendly for the common case of looking at your commits in order. * You can generate a changelog any time by looking at only local files. Automatically including a changelog in every commit is redundant and so can be eliminated. Of course if you want to manually maintain a changelog you can do that too. * At the very least we should have ``undo`` as a reversible ``revert``. It might be even better to have a totally general undo which will undo any operation; this is possible by keeping a journal of all changes. * Perhaps eventually move to storing changesets in single text files, containing file diffs and also information on renames, etc. The format should be similar to that of ``tla show-changeset``, but lossless. * Pristines are kept in the control directory; pristines are relatively expensive to recreate so we might want to keep more than one. (Robert says that keeping pristines under there can cause trouble with people running recursive commands across the source tree, so there should probably be some other way to do it. If pristines are identified by their hash then we can have a revlib without needing unique branch names.) * Can probably still have cacherevs for revisions; ideally autogenerated in some sensible way. We know the tree checksum for each revision and can make sure we cached the right thing. * Bazaar-NG should ideally combine the best merging features of Bitkeeper and Arch: both cherry-picking and arbitrary merging within a graph. The metaphor of a bazaar or souk is appropriate: many independent agents, exchanging selected patches at will. * Code should be structured as a library plus a command-line client; the library could be called from any other client. Therefore communication with the user should go through a layer, the library should not arbitrarily exit() or abort(), etc. * Any of these details are open to change. If you disagree, write and say so, sooner rather than later. There will be a day in the future where we commit to compatibility, but that is a while off. * Timestamps obviously need to be in UTC to be meaningful on the network. I guess they should be displayed in localtime by default and you can change that by setting $TZ or perhaps some option like ``--utc``. It might be cool to also capture the local time as an indicator of what the committer was doing. * Should probably have some kind of progress indicator like --showdots that is easy to ignore when run from a program (especially an editor); that probably means avoiding tricks with carriage return. (That might be a problem on Windows too.) * What date should be present on restored files? We don't remember the date of individual files, but we could set the date for the entire commit. * One important layer is concerned with reproducing a previous revision from a given branch; either the whole thing or just a particular file or subdirectory. This is used in many different places. We can potentially plug in different storage mechanisms that can do this; either a very transparent and simple file-based store as in darcs and arch, or perhaps a more tricky/fast database-based system. Entities and terminology ------------------------ The name of the project is *Bazaar-NG*; the top-level command is ``bzr``. Branch '''''' Development in Bazaar-NG takes places on branches. A branch records the progress of a *tree* through various *revisions* by the accumulation of a series of *patches*. We can point to a branch by specifying its *location*. At first this will be just a local directory name but it might grow to allow remote URLs with various schemes. Branches have a *name* which is for human convenience only; changesets are permanently labelled with the name of the branch on which they originated. Branch names complement change descriptions by providing a broader context for the purpose of the change. Typically the branch name will be the same as the last component of the directory or path. There is no higher-level grouping than branches. (Nothing that corresponds to repositories in CVS or Subversion, or archives/categories/versions in Arch.) Of course it may be a good practice to keep your branches organized into directories for each project, just as you might do with tarballs or cvs working directories. Bazaar-NG makes forking branches very easy and common. Revision '''''''' The tree in a branch at a particular moment, after applying all the patches up to that point. File id ''''''' A UUID for a versioned file, assigned by ``bzr add``. Delta ''''' A smart diff, containing: * unidiff hunks for textual changes * for each affected file, the file id and the name of that file before and after the delta (they will be the same if the file was not renamed) * in future, possibly other information describing symlinks, permissions, etc A delta can be generated by comparing two trees without needing any additional input. Although deltas have some diff context that would allow fuzzy application they are (almost?) always exactly applied to the correct predecessor. Changeset ''''''''' (also known as a patch) A changeset represents a commit to a particular branch; it incorporates a *delta* plus some header information such as the name of the committer, the date of the commit, and the commit message. Tree '''' A tree of files and directories. A branch minus the Bazaar-NG control files. Syntax ------ Branches '''''''' Branches are identified by their directory name or URL:: bzr branch http://kernel.org/bzr/linux/linux-2.6 bzr branch ./linux-2.6 ./linux-2.6-mbp-partitions Branches have human-specified names used for tracing patches to their origin. By default this is the last component of the directory name. Revisions ''''''''' Revisions within a branch may be identified by their sequence number on that branch, or by a tag name:: bzr branch ./linux-2.6@43 ./linux-2.6-old bzr branch ./linux-2.6@rel6.8.1 ./linux-2.6.8.1 You may also use the UUID of the patch or by the hash of that revision, though sane humans should never (need to) use these:: bzr log ./linux-2.6@uuid:6eaa1c41-34b8-4e0e-8819-acb5dfcabb78 bzr log ./linux-2.6@hash:4bf00930372cce9716411b266d2e03494f7fe7aa Revision ranges are given as two revisions separated by a colon (same as Svn): bzr merge ../distcc-doc@4:10 Authors ''''''' Authors are identified by their email address, taken from ``$EMAIL`` or ``$BZR_EMAIL``. Tree inventory -------------- When a revision is committed, Bazaar-NG records an "inventory" which essentially says which version of each file should be assembled into which location in the tree. It also includes the SHA-1 hash and the size of each file. Merging ------- Merges are carried out in Bazaar-NG by a three-way merge of trees. Users can choose to merge all changes from another branch, or a particular subset of changes. In either case Bazaar-NG chooses an appropriate common base appropriately, although there should perhaps also be an option to specify a different base. I have not solved all the merge problems here. I do think that this design preserves as much information as possible about the history of the code and so gives a good foundation for smart merging. The basic merge operation is a 3-way diff: we have three files *BASE*, *OTHER* and *MINE* and want to produce a result. There are many different tools that could be used to resolve this interactively or automatically. There are some cases where the best base is not a state that ever occurred on the two branches. One such case is when there are two branches that have both tracked an upstream branch but have never previously synced with each other. In this case we suggest that people manually specify the base:: bzr merge --base linus-2.6 my-2.6 Merges most commonly happen on files, but can also occur on metadata. For example we may need to resolve a conflict between file ids to decide what name a file should have, or conversely which id it should have. When merging an entire branch, the base is chosen as the last revision in which the trees manifests were identical. If merging only selected revisions from a branch (ie cherry picking) then the base is set just before the revisions to be merged. A three-way merge operates on three inputs: THIS, OTHER, and a BASE. Any regions which have been changed in only one of THIS and OTHER, or changed the same way in both will be carried across automatically. Regions which differ in all three trees are conflicts and must be manually resolved. The merge does not depend upon any states the trees may have passed through in between the revisions that are merged. After the merge, the destination tree incorporates all the patches from the branch region that was merged in. Sending patches by email ------------------------ Patches can be sent to someone else by email, just by posting the string representation of the changeset. Could also post the GPG signature. The changeset cannot itself contain its uniquely-identifying hash. Therefore I suppose it needs some kind of super-header which says what the patch id is; this can be verified by comparing it to the hash of the actual changeset. This in turn applies that the text must be exactly preserved in email, so possibly we need some kind of quoted-printable or ascii-armoured form. Another approach would be to not use the hash as the id, but rather something else which allows us to check the patch is actually what it claims to be. For example giving a GPG key id and a UUID would do just as well, and *would* allow the id to be included within the patch, as would giving an arch-style revision ID, assuming we can either map the userid to a GPG key and/or check against a trusted archive. There are two ways to apply such a received patch. Ideally it tells us a revision of our branch from which it was based, probably by specifying the content hash. We can use that as the base, make a branch there, apply the patch perfectly, and then merge that branch back in through a 3-way merge. This gives a clean reconciliation of changes in the patch against any local changes in the branch since the base. If we do not have the base for the patch we can try apply it using a similar mechanism to regular patch, which might cause conflicts. Or maybe it is not worth special-casing this; we could just require people to have the right basis to accept a patch. Rewriting history ----------------- History is generally append-only; once something is committed it cannot be undone. We need this to make several important guarantees about being able to reconstruct previous versions, about patches being consistent, and so on and on. However, pragmatically, there are a few cases where people will insist on being able to fudge it. We need to accommodate those as best we can, within the limits of causality. In other words, what is physically and logically possible should not be arbitrarily forbidden by the software (though it might be enormously discouraged). The basic transaction is a changeset/patch/commit. There is little value and hellish complexity in introducing meta-changesets or trying to update already-committed changes. Wrong commit message '''''''''''''''''''' *Oops, I pressed Save too soon, and the commit message is wrong.* This happens all the time. If no other branch has taken that change, there is no harm in fixing the message. Noticing the problem right away is probably a very common case. Therefore, you can change the descriptive text (but not any other metadata) of a changeset in your tree. This will not propagate to anyone else who has already accepted the change. Nothing will break, but they'll still see the original (incorrect/incomplete) commit. Committed confidential information '''''''''''''''''''''''''''''''''' If you just added a file you didn't mean to add then you can simply commit a second changeset to remove it again. However, sometimes people will accidentally commit sensitive/confidential information, and they need to remove it from the history. If anyone else has already taken the changeset we can't prevent them seeing or keeping the information. You need to find them and ask them nicely to remove it as well. Similarly, if you've mirrored your branch elsewhere you need to fix it up by hand. This additional manual work is a feature because it gives you some protection against accidentally destroying the wrong thing. A similar but related case is accidentally committing an enormous file; you don't want it to hang around in the archive for ever. (In fact, it would need to be stored twice, once for the original commit and again for a reversible remove changeset.) Here is our suggestion for how to fix this: make a second branch from just before the undesired commit, typically by specifying a timestamp. If there are any later commits that need to be preserved, they can be merged in too. Possibly that will cause conflicts if they depended on the removed changeset, and those changes then need to be resolved. History truncation ------------------ (I don't think we should implement this soon, if at all, but people might want to know it's possible.) Bazaar-NG relies on each branch being able to recreate any of its predecessor states. This is needed to do really intelligent merging. However, you might eventually get sick of keeping all the history around forever. Therefore, we can set a history horizon, ignoring all patches before that point. The patches are still recorded as being merged but we do not keep the text of the patches. Perhaps we add them to a special list. Merges with a tree that have no history in common since the horizon will be somewhat harder. A development path ------------------ **See also work-log.txt for what I'm currently doing.** * Start by still using Arch changeset format, do-changeset and delta commands, possibly also for merge. * Don't do any merges automatically at first but rather just build some trees and let the user run dirdiff or something. * Don't handle renames at first. * Don't worry about actually being distributed yet; just work between local directories. There are no conceptual problems with accessing remote directories. Compared to others ------------------ * History cannot be rewritten, aside from a couple of special pragmatic cases. * Allows cherry-picking, which is not possible on bk or most others. * Allows merges within an arbitrary graph (rather than a line, star or tree), which can be done by bk but not by arch or others. * History-sensitive merges allow safe repeated merges and mutual merges between parallel lines. * Patches are labelled with the history of branches they traversed to their current location, which is previously unique to Arch. * Would aim to be almost as small and simple as Quilt. * Does not need archives to be registered. * Like darcs and bk, remembers the last archive you pulled from and uses this as the default. Also as a bonus remembers all branches you previously pulled and their name, so that it is as if they were registered. * Because patches do not change when they move around (as in Darcs), they can be cryptographically signed. * Recognizes that textually non-conflicting merges may not be a correct merge and may not work, and so should not be auto-committed. The developer must have a chance to intervene after the merge and before a commit. (I think Monotone is wrong on this.) Best practices -------------- We recommend that people using Bazaar-NG follow these practices and protocols: * Develop independent features in separate branches. It's easier to keep them separate and merge later than to mix things together and then try to separate them. Although cherry picking is possible, it's generally harder than keeping the code separate in the first place. * Although you can merge in a graph, it can be easier to understand things if you keep them roughly sorted into a star of downstream and upstream branches. * Merge off your laptop/workstation into a personal stable tree at regular changes; this protects against accidentally losing your development branch for any reason. * Try to have relatively "pure" merges: a single changeset that merges changes should make only those merges and any edits needed to fix them up. * You can use reStructuredText (like this document) for commit messages to allow nicer formatting and automatic detection of URLs, email addreses, lists, etc. Nothing requires this. Mechanics --------- Patch format '''''''''''' A patch (i.e. commit to a branch) exists at three levels: * the hash of the patch, which is used as its globally-unique name * the headers of the patch, including: - the human-readable name of the branch to which the changeset was committed - free-form comments about the changeset - the email address and name of the user who committed the changeset - the date when the changeset was committed to the branch - the UUIDs of any patches merged by this change - the hash of the before and after trees - the IDs of any files affected by the change, and their names before and after the change, and their hash before and after the change * the actual text of the patch, which may include - unidiff hunks - xdeltas (in reversible pairs?) - complete files for adds/deletes, or for binaries At the simplest level a branch knows just the IDs of all of the patches committed to it. More usually it will also have all their logs or all their text. Using the IDs, it can retrieve the patches when necessary from a shared or external store. By this means we can have many checkouts, each of which looks like it holds all of its history, without actually using a lot of space. When pulling down a remote branch by default everything will be mirrored, but there might be an option to only get the inventory or only the logs. Keeping the relatively small header separate from the text makes it easy to get only the header information from a remote machine. One might also when offline like to see only the logs but not necessarily have the text. Only the basic policy (keep everything everywhere) needs to be done in the first release of course. The headers need to be stored in some format that allows moderately structured data. Ideally it would be both human readable and accessible from various languages. In the prototype I think I'll use Python data format, but that's probably not good in the long term. It may be better to use XML (tasteless though that is) or perhaps YAML or RFC-2822 style. Python data is probably not secure in the face of untrusted patches. The date should probably be shown in ISO form (unoptimal though that is in some ways.) Unresolved questions and other ideas ------------------------------------ Pulling in inexact matches '''''''''''''''''''''''''' If ``update`` pulls in patches noninteractively onto the history, then there are some issues with patches that do not exactly match. Some consequences: * You may pull in a patch which causes your tree to semantically break. This might be avoided by having a test case which is checked before committing. * The patch may fuzzily apply; this is OK. If we pull in a patch from elsewhere then we will have a signature on the patch but not a signature for the whole cacherev. Have pristines/working directory by default? '''''''''''''''''''''''''''''''''''''''''''' It seems a bit redundant to have two copies of the current version of each file in every repository, even ones in which you'll never edit. Some fixes are possible: * don't create working copy files * hard link working copies into pristine directory (can detect corruption by having SHA-1 sums for all pristine files) I think it's reasonable to have Directory name '''''''''''''' We have a single metadata directory at the top-level of the tree: ``.bzr``. There is no value in having it non-hidden, because it can't be seen from subdirectories anyhow. Apparently three-letter names after a dot are fine on Windows -- it works for ``.svn``. File encodings '''''''''''''' Unicode, line endings, etc. Ignore this for now? Case-insensitive file names? Maybe follow Darcs in forbidding files that differ only in case. Always use 3-way merge '''''''''''''''''''''' I think using .rej files and fuzzy patches is confusing/unhelpful. I would like to use 3-way merges between appropriate coordinates as the fundamental mechanism for all 'merge'-type operations. Is there any case where .rej files are more useful? Why would you ever want that? Some people seem to `prefer them`__ in Arch. __ http://wiki.gnuarch.org/moin.cgi/Process_20_2a_2erej_20files I guess when cherry-picking you might not be able to find an appropriate ancestor for diff3? I think you can; anyhow wiggle can transform rejects into diff3-style conflicts so why not do that? Miles says there that he prefers .rej files to conflict markers because they give better results for complex conflicts. Perhaps we should just always produce both and let people use whatever they want. Another suggestion is the *rej_* tool, which helps fix up simple rejects: There are four basic rejects fixable via rej. 1) missing context at the top or bottom of the hunk 2) different context in the middle of the hunk 3) slightly different lines removed by the hunk than exist in the file 4) Large hunks that might apply if they were broken up into smaller ones. .. _rej: ftp://ftp.suse.com/pub/people/mason/rej/ Mirroring ''''''''' One reason people say they like archives is that all new work in that archive will be automatically mirrored off your laptop, if it's already set up to mirror that archive. Control files out of tree ''''''''''''''''''''''''' Some people would like to have absolutely no control files in their tree. This is conceptually easy as long as we can find both the control files and working directory when a command is run. As a first step, the ``.bzr`` directory can be replaced by a symlink, which will prevent recursive commands looking into it. Another approach is to put all actual source in a subdirectory of the tree, so that you never need to see the directory unless you look above the ceiling. If this is not enough, we might ask them to have an environment variable point to the control files, or have a map somewhere associating working directories with their control files. Unfortunately both of those seem likely to come loose and whip around dangerously. Representation of changesets '''''''''''''''''''''''''''' Using patches is nice for plain text files. In general we want the old and new names to correspond, but these are only for decoration; the file id determines where the patch really goes. * Should they be reversible? * How to represent binary diffs? * How to represent adds/removes? * How to zip up multiple changes into a single bundle? Reversibility is very important. We do not need it for regular merges, since we can always recover the previous state. We do need it for application of isolated patches, since we may not be able to recover the prior state. It might also help when building a previous tree state. Of course we can have an option to show deletes or to make the diff reversible even if it normally is not. It is very nice that plain diffs can be concatenated into a single text file. This is not easily possible with binary files, xdeltas, etc. Of course it is uncommon to display binary deltas directly or mail them, but if mailing is really required we could use base64 or MIME. Perhaps it would be reasonable to just store xdeltas between versions. Perhaps each changeset body should be a tar or zip holding the patches, though in simpler form than Arch. (Since these are free choices, perhaps stick closely to what Arch does?) Continuations ''''''''''''' Do we need the generalized continuations currently present in Arch, or will a more restricted type do? One use case for arch continuation tags is to make a release branch which contains only tags from the development branch. Maybe want darcs-style tags which just label the tree at various points; more familiar to users perhaps? :: bzr fork http://samba.org/bzr/samba/main ./my-samba 1. creates directory my-samba 2. copies contents of samba main branch 3. the parent becomes samba-main 4. parent is the default place you'll pull from & push to Is there a difference between "contains stuff from samba-main" and "is branched from samba-main"? File split/merge '''''''''''''''' Is there any sense in having a command to copy a file, or to rejoin several files with different IDs? Joining might be useful when the same tree is imported several times, or the same new-file operation is done in different trees. Time skew ''''''''' Local clocks can be wrong when they record a commit. This means that changes may be irrevocably recorded with the wrong time, and that in turn means that later changes may seem to come from before earlier changes. We can give a warning at the later time, but short of refusing the commit there is not much we can do about it. Annotate/blame/praise --------------------- ``cvs annotate`` is pretty useful for understanding the history of development. At the same time it is not quite trivial to implement, so I plan to make sure all the necessary data is easily accessible and then defer actually writing it. Possibly the most complicated part is something to read in a diff and find which lines came from where. What we need is a way to easily follow back through the history of a file, this is easily done by walking back along the branch. Since we have revision numbers within a branch we have a short label which can be put against each line; we can also put a key at the bottom with some fields from each revision showing the committer and comment. For the case of merge commits people might be interested to know which merged patch brought in a change. We cannot do this completely accurately since we don't know what the person did during the manual resolution of the merge, but by looking for identical lines we can probably get very close. We can at the very least tell people the hash of all patches that were merged in so they can go and have a look at them. Performance ----------- I think nothing here requires loading the whole tree into memory, as Darcs does. We can detect renames and then diff files one by one. Because patches cannot change or be removed once they are committed or merged, we do not need to diff the patch-log, which is a problem in Arch. We do need to hold the whole list of patches in memory at various points but that should be at most perhaps 100,000 commits. We do need to pull down all patches since forever but that's not too unreasonable. Most heavy lifting can be done by GNU diff, patch and diff3, which are hopefully fast. Patches should be reasonably proportionate to the actual size of changes, not to the total size of the tree -- we should only list the hash and id for files that were touched by the change. This implies that generating the manifest for any given revision means walking from the start of history to that revision. Of course we can cache that manifest without necessarily caching the whole revision. * The dominant effect on performance in many cases will be network round-trips; as Tom says "every one is like punching your user in the face." The network protocol can/should try to avoid them. However, here's an even lazier idea: by making it possible to use rsync for moving trees around, we get an insanely pipelined protocol *for free*. It's not always suitable (as when committing to a central tree), but it will often work. Cool! Safely using rsync probably requires user intervention to make sure that the tree is idle at the time the command runs; otherwise the ordering of files arriving makes it really hard to know that we have a consistent state. I guess we can just ignore patches that are missing... Hashing ------- It might be nice to present hashes in BubbleBabble or some similar form to make it a bit easier on humans who have to see them. This can of course be translated to and from binary. On the other hand there is something in favour of regular strings that can be easily verified with other tools. We can have a Henson Mode in which it never trusts that files with the same hash are identical but always checks it. Of course if SHA-1 is broken then GPG will probably be broken too... Comparison: binary: 20 bytes bubblebabble > xizif-segim-vipyz-dyzak-gatas-sifet-dynir-gegon-borad-cetit-tixux 65 bytes base64: > qvTGHdzF6KLavt4PO0gs2a6pQ00= 28 bytes hex: > aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d 40 bytes Hex is probably the most reasonable tradeoff. File metadata ------------- I don't want to get into general versioning of file metadata like permissions, at least in the first version; it's hard to say what should be propagated and what should not be. This is a source code control system. It may be useful to carry some very restricted bits, like *read only* or *executable*; I think these are harmless. The only case where people generally want to remember permissions and ownership is when versioning ``/etc``, which is quite a special case. Perhaps this should be deferred to a special script such as the ``cvs-conf`` package. Faster comparisons ------------------ There are many cases where we need to compare trees; perhaps the most common is just diffing the tree to see what changed. For small to medium trees it is OK to just diff everything in the tree, and we can do just this in the first version. This runs into trouble for kernel-sized trees, where reading every Fear of forking --------------- There is some fear that distributed version control (many branches) will encourage projects to fork. I don't think this is necessarily true of Bazaar. A fundamental principle of Bazaar is that is not the tool's place to make you run a project a particular way. The tool enables you to do what you want. The documentation and community might suggest some practices that have been useful for other projects, but the choice is up to you. There are principles for running open source projects that are useful regardless of tool, and Bazaar supports them. They include encouraging new contributors, building community, managing a good release schedule and so on, but I won't enumerate them all here (and I don't claim to know them all.) Bazaar reduces some pressures that can lead to forking. There need not be fights about who gets commit access: everyone can have a branch and they can contribute their changes. Radical new development can occur on one branch while stabilization occurs on another and a new feature or port on a third. Both creating the branches and merging between them should be easier in the Bazaar than with existing systems. (Though of course there may be technical difficulties that no tool can totally remove.) Sometimes there really is a time for a fork, for various reasons: irreconcilable differences on technical direction or personality. If that happens, Bazaar makes the break less total: the projects can still merge patches, share bug fixes and features, and even eventually reunite. Why a new project? ------------------ A key goal is simplicity and user-friendliness; this is easier to build into a new tool than to fix in an existing tool. Nevertheless we want to provide a smooth upgrade path from Arch, CVS, and other systems. References ---------- * http://www.dwheeler.com/essays/scm.html Good analysis; should try to address everything there in a way he will like. .. Local variables: .. mode: indented-text .. End: .. Would like to use rst-mode, but it's too slow on a document of this .. size. M 644 inline doc/extra-commands.txt data 695 ************************ Bazaar-NG Extra Commands ************************ :Copyright: Copyright 2005 Canonical Ltd. .. contents:: This document lists some "extra" commands that are intended for developer, debugging, or advanced-hacker use. You should not need to know about them for normal operations. inventory List files in the working copy, in an XML format including the filename, id and content hash. unknowns List unknown files. uuid Emit a new UUID. add-to-store Add given files to the full-text store, indexed by their hash. doctest Run test suite. revno Print number of revisions on this branch (or equivalently, the latest revision number.) M 644 inline doc/faq.txt data 231 Bazaar-NG frequently asked questions ************************************ *The XML is unreadable!* Try running it through a pretty-printer like this:: xmllint --format *.xml *There's only two questions here?* Yes. M 644 inline doc/formats.txt data 7920 ***************** Bazaar-NG formats ***************** .. contents:: Since branches are working directories there is just a single directory format. There is one metadata directory called ``.bzr`` at the top of each tree. Control files inside ``.bzr`` are never touched by patches and should not normally be edited by the user. These files are designed so that repository-level operations are ACID without depending on atomic operations spanning multiple files. There are two particular cases: aborting a transaction in the middle, and contention from multiple processes. We also need to be careful to flush files to disk at appropriate points; even this may not be totally safe if the filesystem does not guarantee ordering between multiple file changes, so we need to be sure to roll back. The design must also be such that the directory can simply be copied and that hardlinked directories will work. (So we must always replace files, never just append.) A cache is kept under here of easily-accessible information about previous revisions. This should be under a single directory so that it can be easily identified, excluded from backups, removed, etc. This might contain pristine tree from previous revisions, manifests and inventories, etc. It might also contain working directories when building a commit, etc. Call this maybe ``cache`` or ``tmp``. I wonder if we should use .zip files for revisions and cacherevs rather than tar files so that random access is easier/more efficient. There is a Python library ``zipfile``. Signing XML files ***************** bzr relies on storing hashes or GPG signatures of various XML files. There can be multiple equivalent representations of the same XML tree, but these will have different byte-by-byte hashes. Once signed files are written out, they must be stored byte-for-byte and never re-encoded or renormalized, because that would break their hash or signature. Branch metadata *************** All inside ``.bzr`` ``README`` Tells people not to touch anything here. ``branch-format`` Identifies the parent as a Bazaar-NG branch; contains the overall branch metadata format as a string. ``pristine-directory`` Identifies that this is a pristine directory and may not be committed to. ``patches/`` Directory containing all patches applied to this branch, one per file. Patches are stored as compressed deltas. We also store the hash of the delta, hash of the before and after manifests, and optionally a GPG signature. ``cache/`` Contains various cached data that can be destroyed and will be recreated. (It should not be modified.) ``cache/pristine/`` Contains cached full trees for selected previous revisions, used when generating diffs, etc. ``cache/inventory/`` Contains cached inventories of previous revisions. ``cache/snapshot/`` Contains tarballs of cached revisions of the tree, named by their revision id. These can also be removed, but ``patch-history`` File containing the UUIDs of all patches taken in this branch, in the order they were taken. Each commit adds exactly one line to this file; lines are never removed or reordered. ``merged-patches`` List of foreign patches that have been merged into this branch. Must have no entries in common with ``patch-history``. Commits that include merges add to this file; lines are never removed or reordered. ``pending-merge-patches`` List of foreign patches that have been merged and are waiting to be committed. ``branch-name`` User-qualified name of the branch, for the purpose of describing the origin of patches, e.g. ``mbp@sourcefrog.net/distcc--main``. ``friends`` List of branches from which we have pulled; file containing a list of pairs of branch-name and location. ``parent`` Default pull/push target. ``pending-inventory`` Mapping from UUIDs to file name in the current working directory. ``branch-lock`` Lock held while modifying the branch, to protect against clashing updates. Locking ******* Is locking a good strategy? Perhaps somekind of read-copy-update or seq-lock based mechanism would work better? If we do use a locking algorithm, is it OK to rely on filesystem locking or do we need our own mechanism? I think most hosts should have reasonable ``flock()`` or equivalent, even on NFS. One risk is that on NFS it is easy to have broken locking and not know it, so it might be better to have something that will fail safe. Filesystem locks go away if the machine crashes or the process is terminated; this can be a feature in that we do not need to deal with stale locks but also a feature in that the lock itself does not indicate cleanup may be needed. robertc points out that tla converged on renaming a directory as a mechanism: this is one thing which is known to be atomic on almost all filesystems. Apparently renaming files, creating directories, making symlinks etc are not good enough. Delta ***** XML document plus a bag of patches, expressing the difference between two revisions. May be a partial delta. * list of entries * entry * parent directory (if any) * before-name or null if new * after-name or null if deleted * uuid * type (dir, file, symlink, ...) * patch type (patch, full-text, xdelta, ...) * patch filename (?) Inventory ********* XML document; series of entries. (Quite similar to the svn ``entries`` file; perhaps should even have that name.) Stored identified by its hash. An inventory is stored for recorded revisions, also a ``pending-inventory`` for a working directory. Revision ******** XML document. Stored identified by its hash. committer RFC-2822-style name of the committer. Should match the key used to sign the revision. comment multi-line free-form text; whitespace and line breaks preserved timestamp As floating-point seconds since epoch. precursor ID of the previous revision on this branch. May be absent (null) if this is the start of a new branch. branch name Name of the branch to which this was originally committed. (I'm not totally satisfied that this is the right way to do it; the results will be a bit wierd when a series of revisions pass through variously named branches.) inventory_hash Acts as a pointer to the inventory for this revision. merged-branches Revision ids of complete branches merged into this revision. If a revision is listed, that revision and transitively its predecessor and all other merged-branches are merged. This is empty except where cherry-picks have occurred. merged-patches Revision ids of cherry-picked patches. Patches whose branches are merged need not be listed here. Listing a revision ID implies that only the change of that particular revision from its predecessor has been merged in. This is empty except where cherry-picks have occurred. The transitive closure avoids Arch's problem of needing to list a large number of previous revisions. As ddaa writes: Continuation revisions (created by tla tag or baz branch) are associated to a patchlog whose New-patches header lists the revisions associated to all the patchlogs present in the tree. That was introduced as an optimisation so the set of patchlogs in any revision could be determined solely by examining the patchlogs of ancestor revisions in the same branch. This behaves well as long as the total count of patchlog is reasonably small or new branches are not very frequent. A continuation revision on $tree currently creates a patchlog of about 500K. This patchlog is present in all descendent of the revision, and all revisions that merges it. It may be useful at some times to keep a cache of all the branches, or all the revisions, present in the history of a branch, so that we do need to walk the whole history of the branch to build this list. M 644 inline doc/hashes.txt data 2560 Use of hashes in Bazaar-NG ************************** * http://infohost.nmt.edu/~val/review/hash.html * http://infohost.nmt.edu/~val/review/hash2.html The main attraction of hashes in bazaar-ng is as an easy way to get universally-unique IDs, or at least with a low chance of collision: The first paper is a bit paranoid; the second has some sensible advice: 1. Will compare-by-hash provide significant benefit -- save time, bandwidth, etc? 2. Is the system usable if hash collisions can be generated at will? 3. Can the hashes be regenerated with a different algorithm at any time? We should try to abide by these rules. I think they are possibly too paranoid -- a real break of SHA-1 would have much wider security implications -- but if a design that respects them is practical, it should be preferred. The first is probably true; the third is just a matter of making sure we allow for the choice of hash to be varied in the format. There are actually two variations on the second: 2a. Is the system safe if an attacker can generate hash collisions? 2b. Is the system safe if a user's own files contain collisions. Regardless of cryptographic weakness, SHA-1 is unlikely to "accidentally" collide, but it's possible that someone will intentionally generate collisions (in research on SHA) and then want to store them. It would be unfortunate if that did not work. An advantage of naming by hash is that it lets us store only a single copy of identical files, but we have already decided__ that disk space is pretty cheap. It is perhaps enough to have a single copy of files that do not change from one tree revision to the next. __ costs.html As far as an attacker: we will not automatically trust that ids from one branch have the same value in another. It is possible for a branch to contain "lies" about its history or contents, but that doesn't corrupt anything else. It may confuse or mislead someone who looks at the branch, but there is no substitute for human review anyhow. ------- The safest position may be to never rely on identifying content by hash. Rather, things which need a universally unique ID should get a UUID instead. This has a slight advantage that the id can be stored directly in the object it refers to, when that's useful. So a `Revision` holds a UUID for the `Inventory`. An inventory holds `InventoryEntry` objects, each with * file-id * filename (location in tree) * type (file, dir, etc) * text-id (uuid identifying the text) * text-sha1 * text-length (for catching bugs) * parent-file-id M 644 inline doc/index.txt data 6670 Bazaar-NG ********* .. These documents are formatted as ReStructuredText. You can .. .. convert them to HTML, PDF, etc using the ``python-docutils`` .. .. package. .. *Bazaar-NG* (``bzr``) is a project of `Canonical Ltd`__ to develop an open source distributed version control system that is powerful, friendly, and scalable. The project is at an early stage of development. __ http://canonical.com/ **Note:** These documents are in a very preliminary state, and so may be internally or externally inconsistent or redundant. Comments are still very welcome. Please send them to . Summary status -------------- (as of 2005-02-23) * Bazaar-NG can track the history of a single local branch, similar to RCS. The storage format should be reasonably close to what will be used for 1.0. * The following commands work: ``add``, ``remove``, ``commit``, ``log``, ``diff`` (whole tree against basis revision), ``status``, ``help``, ``export`` (any revision). * Subdirectories and files within them are now supported. User documentation ------------------ * `Project overview/introduction `__ * `Command reference `__ -- intended to be user documentation, and gives the best overview at the moment of what the system will feel like to use. Fairly complete. * `Quick reference `__ -- single page description of how to use, intended to check it's adequately simple. Incomplete. * `FAQ `__ -- mostly user-oriented FAQ. * `Demonstration/tutorial `__ Requirements and general design ------------------------------- * `Various purposes of a VCS `__ -- taking snapshots and helping with merges is not the whole story. * `Requirements `__ * `Costs `__ of various factors: time, disk, network, etc. * `Deadly sins `__ that gcc maintainers suggest we avoid. * `Overview of the whole design `__ and miscellaneous small design points. * `File formats `__ * `Random observations `__ that don't fit anywhere else yet. Design of particular features ----------------------------- * `Automatic generation of ChangeLogs `__ * `Cherry picking `__ -- merge just selected non-contiguous changes from a branch. * `Common changeset format `__ for interchange format between VCS. * `Compression `__ of file text for more efficient storage. * `Config specs `__ assemble a tree from several places. * `Conflicts `_ that can occur during merge-like operations. * `Recovering from interrupted operations `__ * `Inventory command `__ * `Branch joins `__ represent that all the changes from one branch are integrated into another. * `Kill a version `__ to fix a broken commit or wrong message, or to remove confidential information from the history. * `Hash collisions `__ and weaknesses, and the security implications thereof. * `Layers `__ within the design * `Library interface `__ for Python. * `Merge `__ * `Mirroring `__ * `Optional edit command `__: sometimes people want to make the working copy read-only, or not present at all. * `Partial commits `__ * `Patch pools `__ to efficiently store related branches. * `Revision syntax `__ -- ``hello.c@12``, etc. * `Roll-up commits `__ -- a single revision incorporates the changes from several others. * `Scalability `__ * `Security `__ * `Shared branches `__ maintained by more than one person * `Supportability `__ -- how to handle any bugs or problems in the field. * `Place tags on revisions for easy reference `__ * `Detecting unchanged files `__ * `Merging previously-unrelated branches `__ * `Usability principles `__ (very small at the moment) * ``__ * ``__ * ``__ Modelling/controlling flow of patches. * ``__ -- Discussion of using YAML_ as a storage or transmission format. .. _YAML: http://www.yaml.org/ Comparisons to other systems ---------------------------- * `Taxonomy `__: basic questions a VCS must answer. * `Bitkeeper `__, the proprietary system used by some kernel developers. * `Aegis `__, a tool focussed on enforcing process and workflow. * `Codeville `__ has an intruiging but scarcely-documented merge algorithm. * `CVSNT `__, with more Windows support and some merge enhancements. * `OpenCM `__, another hash-based tool with a good whitepaper. * `PRCS `__, a non-distributed inventory-based tool. * `GNU Arch `__, with many pros and cons. * `Darcs `__, a merge-focussed tool with good usability. * `Quilt `__ -- Andrew Morton's patch scripts, popular with kernel maintainers. * `Monotone `__, Graydon Hoare's hash-based distributed system. * `SVK `__ -- distributed operation stacked on Subversion. * `Sun Teamware `__ Project management and organization ----------------------------------- * `Development news `__ * `Notes on how to get a VCS adopted `__ * `Testing plan `__ -- very sketchy. * `Thanks `__ to various people * `Roadmap `__: High-level order for implementing features. * ``__: current tasks. * `Extra commands `__ for internal/developer/debugger use. * `Choice of Python as a development language `__ Download -------- There are no releases yet. The Bazaar-NG development code may be downloaded using GNU Arch by the following steps:: tla register-archive http://sourcefrog.net/arch/mbp@sourcefrog.net--2004 tla get mbp@sourcefrog.net--2004/bazaar-ng--0 bazaar-ng (By contrast, when Bazaar-NG network operations are complete, a single much shorter command will do.) Note that the current code is under heavy development and so is not guaranteed to do anything useful whatsoever. M 644 inline doc/interrupted.txt data 4293 Interrupted operations ********************** Problem: interrupted operations =============================== Many version control systems tend to have trouble when operations are interrupted. This can happen in various ways: * user hits Ctrl-C * program hits a bug and aborts * machine crashes * network goes down * tree is naively copied (e.g. by cp/tar) while an operation is in progress We can reduce the window during which operations can be interrupted: most importantly, by receiving everything off the network into a staging area, so that network interruptions won't leave a job half complete. But it is not possible to totally avoid this, because the power can always fail. I think we can reasonably rely on flushing to stable storage at various points, and trust that such files will be accessible when we come back up. I think by using this and building from the bottom up there are never any broken pointers in the branch metadata: first we add the file versions, then the inventory, then the revision and signature, then link them into the revision history. The worst that can happen is that there will be some orphaned files if this is interrupted at any point. rsync is just impossible in the general case: it reads the files in a fairly unpredictable order, so what it copies may not be a tree that existed at any particular point in time. If people want to make backups or replicate using rsync they need to treat it like any other database and either * make a copy which will not be updated, and rsync from that * lock the database while rsyncing The operating system facilities are not sufficient to protect against all of these. We cannot satisfactorily commit a whole atomic transaction in one step. Operations might be updating either the metadata or the working copy. The working copy is in some ways more difficult: * Other processes are allowed to modify it from time to time in arbitrary ways. If they modify it while bazaar is working then they will lose, but we should at least try to make sure there is no corruption. * We can't atomically replace the whole working copy. We can (semi) atomically updated particular files. * If the working copy files are in a wierd state it is hard to know whether that occurred because bzr's work was interrupted or because the user changed them. (A reasonable user might run ``bzr revert`` if they notice something like this has happened, but it would be nice to avoid it.) We don't want to leave things in a broken state. Solution: write-ahead journaling? ================================= One possibly solution might be write-ahead journaling: Before beginning a change, write and flush to disk a description of what change will be made. Every bzr operation checks this journal; if there are any pending operations waiting then they are completed first, before proceeding with whatever the user wanted. (Perhaps this should be in a separate ``bzr recover``, but I think it's better to just do it, perhaps with a warning.) The descriptions written into the journal need to be simple enough that they can safely be re-run in a totally different context. They must not depend on any external resources which might have gone away. If we can do anything without depending on journalling we should. It may be that the only case where we cannot get by with just ordering is in updating the working copy; the user might get into a difficult situation where they have pulled in a change and only half the working copy has been updated. One solution would be to remove the working copy files, or mark them readonly, while this is in progress. We don't want people accidentally writing to a file that needs to be overwritten. Or perhaps, in this particular case, it is OK to leave them in pointing to an old state, and let people revert if they're sure they want the new one? Sounds dangerous. Aaron points out that this basically sounds like changesets. So before updating the history, we first calculate the changeset and write it out to stable storage as a single file. We then apply the changeset, possibly updating several files. Each command should check whether such an application was in progress. M 644 inline doc/intro.txt data 3379 Introduction to Bazaar ====================== Bazaar's goal is *to make a distributed version control system that open source developers will love to use*. Using Bazaar should feel good. .. epigraph:: Language designers want to design the perfect language. They want to be able to say, "My language is perfect. It can do everything." But it's just plain impossible to design a perfect language, because there are two ways to look at a language. One way is by looking at what can be done with that language. The other is by looking at how we feel using that language -- how we feel while programming. Because of the Turing completeness theory, everything one Turing-complete language can do can theoretically be done by another Turing-complete language, but at a different cost. You can do everything in assembler, but no one wants to program in assembler anymore. From the viewpoint of what you can do, therefore, languages do differ -- but the differences are limited. For example, Python and Ruby provide almost the same power to the programmer. Instead of emphasizing the what, I want to emphasize the how part: how we feel while programming. -- `Yukihiro Matsumoto`__ __ http://www.artima.com/intv/ruby.html Bazaar tries to make simple things simple, and complex things possible. In particular: * Distributed operation is easy: you can work while disconnected; you can fork any other project; you can contribute changes back easily. * The system is designed to scale to supporting very large trees with a lot of history. No operations require downloading the entire history of the project. * Changes can be "cherry-picked" out of branches as needed. Because of dependencies between History-sensitive merging ========================= Baz keeps track of what changes have been merged into a branch. You can repeatedly merge from one branch into another and Baz will pull across only the new changes since you last merged. Speed ===== For most users, the most important factor for performance is to avoid unnecessary network round trips. Baz tries hard to avoid ever downloading the same data twice. Remote archives are automatically cached on your local machine by default. If you have ever accessed a remote revision you should be able to get it again without going to the network. The cache policy may be tuned. Code history ============ One important function of a revision control system is to maintain a record of when, why, how and by whom changes were made. Baz requires that branches and archives be named. Unlike most other systems, Baz keeps a record for each changeset of which branches and archives it passed through on its way to its eventual destination. This allows people to go back later and see the context in which the patch was written or merged. Scalability =========== We regularly test Baz on projects with tens of thousands of commits, and tens of thousands of files. Merging ======= The basic method of integration is a three-way merge. Baz selects an appropriate basis version File renaming ============= Baz allows files and directories to be renamed in a project. Unlike Subversion, Baz will correctly merge changes spanning file renames. This is done by automatically assigning a unique ID to each file, which is persistent across renames. M 644 inline doc/inventory.txt data 312 Should be a simple command that shows something similar to 'svn status': what is not added but perhaps should be what is source, backup, precious, junk, etc Or a nice simple command like 'bk extras' to show what's not added but should be. baz inventory almost does this but it way too hard to understand M 644 inline doc/join-branches.txt data 3260 **************** Joining branches **************** (I think this is pretty brilliant. :-) Branches diverge when people create more than one changeset following on from a common ancestor:: A: 0 ------- 1 B: \------- 2 We also allow branches to reunite. This means that all the decisions taken on multiple branches have been reconciled and unified into a single successor:: A: 0 ------- 1 ----- 3 \ / B: \------ 2 ----/ The predecessor of 3 is 1, in the sense that it was created on that branch. We could have created the exact same state as a successor to 2, and we can move that state onto branch 2 without any loss of information. (One thing we can do here is just delete B. Because all of the work there has been merged onto A, this will not lose anything. We might do this if the purpose of B has been achieved, such as completing a feature or bug. But if the work is still in progress, we might keep it around. It makes little difference whether we decide to do new work in a branch called B or make a new one called C.) That is to say that 3 can be perfectly (trivially) merged onto B, with, say ``bzr push``, ``bzr pull`` or ``bzr update`` (whatever name works best). Perfectly merged means that we know there will be no conflicts or need for manual intervention, and that we can just directly store it without forming a roll-up changeset. I think we might also like the choice of merging A onto B, rather than pulling the changeset. That causes a new changeset to be created on B, noted as a successor of 2 and 3:: A: 0 ------- 1 ----- 3 ------+ \ / \ B: \------ 2 ----+---------- 4 One complication is that 3 is probably stored in A's history as a patch relative to 1; we can't just move this representation across. Instead, we need to recalculate the delta from 2 to 3 and store that. Despite that the delta is stored differently, the original signature on 3 should still be valid. So it must be a signature of the tree state, not the diff. Note from `Kernel Traffic discussion`__: __ http://www.kerneltraffic.org/kernel-traffic/kt20030323_210.txt But anyway, what made Bitkeeper suck less is the real DAG structure. Neither arch (http://arch.fifthvision.net/bin/view) nor subversion seem to have understood that and, as a result, don't and won't provide the same level of semantics. Zero hope for Linus to use them, ever. They're needed for any decently distributed development process. This in turn suggests that possibly deltas should be stored separately from the commits that create them. Commits name points of development; deltas describe how to get from one to the other. The separation is nice in allowing us to send just a delta when diffing trees or recording for undo. We might want to compute many deltas between different trees. Is this a problem? Does this ignore Tom's advice about the primacy of storing changesets? Splitting them is probably good, but then what manifest is stored in changesets? We don't want to store the manifest of the whole tree if we can avoid it. So I suppose the changeset just gives the hash of the manifest, and the manifest then can be stored separately, possibly delta-encoded. M 644 inline doc/kill-version.txt data 3560 Killing versions **************** Sometimes people really need to rewrite history. The canonical example is that they have accidentally checked confidential information into the wrong tree. There is a tension between two imperatives: * Accurately record all history. * Remove critical information. Of course we cannot control who may have made use of the information in the time it was public, but often these things are caught early enough. After history has been rewritten, anything that depends on that history may be inconsistent. We cannot consistently continue on a version that has had some revisions knocked out; therefore it seems simplest to kill the version over all. You can make a tag onto a new version from a previous consistent revision. Downstream users will need to switch on to the new version. We can mark the version as killed so that mirroring will delete the revisions and clients looking at the version can suggest switching. This should be an extremely infrequent operation so it is OK that it is simple as long as it fulfils the basic requirement. A related problem is that people sometimes commit the wrong log message, or (more seriously) the wrong files. It is common to notice immediately afterwards. Uncommit -------- I think this is the best and most useful option at the moment: the `uncommit command`_ undoes the last commit, and nothing else. It can be repeated to successively remove more and more history. .. _`uncommit command`: cmdref.html#uncommit Example:: % cd my-project % bzr add launch-codes.txt % bzr commit -m 'add launch codes' A launch-codes.txt recorded r23 [oops!] % bzr uncommit [now back to just before the commit command] % bzr status A launch-codes.txt % bzr revert launch-codes.txt % bzr status ? launch-codes.txt If anyone else has depended on the option then the branches will have diverged and that needs to be resolved somehow, probably by the other people starting a new branch with the correct commit. In particular uncommitting from a shared branch will break anyone who has seen that revision; possibly there needs to be a policy option on such branches to deny uncommit. If someone else on that shared branch did a the equivalent of a switch_ command then they would bring their changes onto the correct basis. Would it be reasonable to do that automatically? .. _switch: cmdref.html#switch UI proposals ------------ Aaron's proposal:: % cd my-project % baz add launch-codes.txt % baz commit -s 'add launch codes' recorded patch-4 % baz add good-code.c % baz commit -s 'add good code' recorded patch-5 [oops!] % baz undo patch-3 % baz pull patch-5 % baz commit --as-latest renamed old branch to my-project-old-1 % baz eradicate my-project-old-1 Long form:: % cd my-project % baz add launch-codes.txt % baz commit -s 'add launch codes' recorded patch-4 % baz add good-code.c % baz commit -s 'add good code' recorded patch-5 [oops!] % cd ../ % baz fork my-project--patch3 my-project-fixed % cd my-project-fixed % baz pull ../my-project patch5 % cd ../ % rm -rf my-project % mv my-project-fixed my-project Robert's proposal:: % cd my-project % baz add launch-codes.txt % baz commit -s 'add launch codes' recorded patch-4 % baz add good-code.c % baz commit -s 'add good code' recorded patch-5 [oops!] % baz branch patch-3 $(baz tree-version) % baz pull . patch-5 % baz eliminate patch-4 M 644 inline doc/layers.txt data 2867 * Write-once store of files identified by globally-unique names (e.g. hashes or UUIDs). Simplest option is to just dump them in a directory. Optionally do delta-compression between similar/related files; or build this on top of deltas. * Tree manifests/inventories which say which files should be assembled at particular points to build a tree. These too can be stored indexed by hash. - Reconstruct a revision by pulling out the manifest and then all the individual files. - Manipulate working copy of inventory by add/mv/remove/commands. * Retrieve deltas - Calculate diffs between two file versions, just by getting them from the store. - Retrieve deltas between any two revisions: requires looking for changes to the structure of the tree, and then text changes for anything inside it. - Deltas may be either stored or calculated on demand. They can be put in the store just indexed by the from and to manifest id. - Calculate diff between a previous revision and the working copy. * Commit and retrieve revisions - Revisions hold metadata (date, committer, comment, optional parents, merged patches), and a pointer to the inventory. - Stored in a write-once store. * Branch holds a linear history of revisions. - This is mostly redundant; we could just remember the current base revision and walk backwards from there. But it still seems possibly useful to hold; we can check that the two are always consistent. (This suggests that we actually *could* do ``switch`` if we really wanted to, by replacing the revision history and head revision. But I don't think it's a good idea.) - Can add a new revision to the end. - By indexing into this can translate between 0-based revision numbers and revision ids. - By walking through and looking at revision dates can find revisions in a particular date range. * Calculations on branch histories: - Find if one branch is a prefix of another. - Find the latest common ancestor of another. * Three-way merge between revisions - Resolve shape of directory - Then resolve textual conflicts * Pull/push changes when they perfectly match - Possible when the destination is a prefix of the source - Just move all revisions, manifests and texts across, and * Merge all changes from one branch into another * Signatures applied to revisions - There is a separable module for checking a signature: this is passed the claimed author, changeset, date. This needs to fetch an appropriate key, decide if it is trusted to correspond to that author, is not revoked, etc. - If it is unknown, untrusted, revoked, etc, that is reported. Depending on a paranoia level it may cause the operation to abort. M 644 inline doc/library-interface.txt data 326 ***************** Library interface ***************** Should have a pleasant library interface. * Be clear what is supposed to be in a stable public interface and what is not. (A common failing of Python programs.) * If a function changes the working directory (as may be necessary to run patch) it should restore it. M 644 inline doc/merge.txt data 758 Merging ======= There should be one merge command which does the right thing, and which is called 'merge'. The merge command pulls a changeset or a range of changesets into your tree. It knows what changes have already been integrated and avoids pulling them again. There should be some intelligence about working out what changes have already been merged. The tool intelligently chooses (or perhaps synthesizes) an ancestor and two trees to merge. These are then intelligently merged. Merge should refuse to run (unless forced) if there are any uncommitted changes in your tree beforehand. This has two purposes: if you mess up the merge you won't lose anything important; secondly this makes it more likely that the merge will be relatively pure. M 644 inline doc/mirroring.txt data 393 Mirroring ========= The basic idea of mirroring is good, but it's too hard to understand. Push mirrors and pull mirrors. Suggest that people commit to their local machine and then regularly mirror elsewhere as a backup. bk and darcs's design inherently avoids this; just make another repo and decide to use it that way. On the other hand they do not keep track of the progress of a patch. M 644 inline doc/monotone.txt data 595 ******************** Thoughts on Monotone ******************** Great project, but not perfect. My main feeling is that it exposes too much mechanism and in the wrong ways. For example, hashes are all very well for representing identity, but I don't want to see them; I just want to say "r43 of this branch". Cryptographic signing is good for making sure revisions are really what they claim to be, but I don't want to directly deal with it: I want it as a check that things are really happening as they should be. It should be barely seen and not heard unless/until something goes wrong. M 644 inline doc/news.txt data 4434 Bazaar-NG Development News ************************** 2005-02-20 ========== * Added documentation of `patch pools `_, explication of commands in terms of file state transitions, `uncommit `_, `journalling `_ through recording changesets. Feedback from Aaron on some of the design documentation. * Much cleaner ``diff`` and ``status`` commands, based on a new Tree class that abstracts access to either a current working copy or a historical revision. * ``remove`` command now both removes the working copy and updates the inventory, and has a new alias ``rm``. * Basic local versioning works well:: % bzr init % echo 'int main() {}' > hello.c % bzr add hello.c % bzr commit 'my test program' % bzr status U hello.c % echo 'int main() { return 0; }' > hello.c % bzr status M hello.c % bzr diff --- old/hello.c +++ new/hello.c @@ -1,1 +1,1 @@ -int main() {} +int main() { return 0; } % bzr commit 'fix return code' % bzr log ---------------------------------------- revno: 1 committer: mbp@sourcefrog.net timestamp: Wed 2005-02-16 11:39:13.003095 EST +1100 my test program ---------------------------------------- revno: 2 committer: mbp@sourcefrog.net timestamp: Wed 2005-02-16 11:43:13.010274 EST +1100 fix return code % bzr status U hello.c % bzr rm hello.c % ls % bzr status D hello.c % bzr commit 'this program sucks!' % bzr stauts bzr: error: unknown command 'stauts' exit 1 % bzr status % * Many internal/hacker commands: ``revno``, ``get-inventory``, ``get-file``, ... * Most of the implementation is split into reasonably clean objects: `Branch`, `Revision`, `Tree`, `Inventory`. I think these will give a good foundation for non-intrusive remote operations. They live in submodules of `bzrlib`. ====== ================================== 596 bzr.py 608 bzrlib/branch.py 39 bzrlib/errors.py 25 bzrlib/__init__.py 320 bzrlib/inventory.py 126 bzrlib/osutils.py 66 bzrlib/revision.py 173 bzrlib/store.py 107 bzrlib/tests.py 62 bzrlib/trace.py 239 bzrlib/tree.py 41 bzrlib/xml.py ------ ---------------------------------- 2402 total ====== ================================== * After feedback and reflection I have decided not to use `SHA-1 hashes`__ as the main identifier of texts, inventories, revisions, etc. It would probably be safe, but there is a certain amount of concern (or even FUD) about the security of these hashes. Since they cannot be trusted to be safe in the long term, it is better just to use something cheaper to compute, and equally unique in the absence of malice: UUIDs. __ hashes.html We now store the SHA-1 of files, but use it only as a check that the files were stored safely. At tridge's suggestion we also keep the size, which can catch some classes of bug or data corruption:: % bzr diff bzr: error: wrong SHA-1 for file '680757bf-2f6e-4ef3-9fb4-1b81ba04b73f' in ImmutableStore('/home/mbp/work/bazaar-ng/toy/.bzr/text-store') inventory expects d643a377c01d3e29f3e6e05b1618eb6833992dd0 file is actually 352586b84597ea8915ef9b1fb5c9c6c5cdd26d7b store is probably damaged/corrupt 2005-02-27 ========== Revisions are now named by something based on the username and date, plus some random bytes to make it unique. This may be a reasonable tradeoff against uniqueness and readability. This same id is used by default for revision inventories:: mbp@sourcefrog.net-20050220113441-34e486671a10f75297e03986 Keeping them separate is still a good thing, because the inventory may be large and we often want only the revision and not the inventory. It is possible for the revision to point to an older inventory if it has not changed (but this is not implemented yet.) Tridge pointed out some `possible performance problems`__ with the assumption that we can simply compare all files to check for changes. __ unchanged.html XML has newlines to make it a bit more readable. Nested subdirectories are now supported down to an arbitrary depth. bzr can successfully import and reproduce itself. ongoing ======= * Systematic command-line handling and option parsing, etc. M 644 inline doc/optional-edit.txt data 951 ****************************** Optional explicit edit command ****************************** The default is for each branch to have an editable working copy, which can be modified and committed. However, in some cases (following RCS and BK) people might prefer to have either a read-only working copy, or none at all. Reasons: - Branches which are never edited don't need it; removing it saves some time and space. In particular this applies to branches on a server which are meant to be branched from or committed too, not edited. - Files for which there is no working copy or a read-only copy are known not to be edited, and this can make some operations such as diffing a bit simpler. Since this is mostly an optimization and requires user knowledge it perhaps should not be added. Certainly not yet. Sketch:: $ bzr get --no-wc SOURCE TARGET $ bzr edit --remove $ bzr edit --read-only . $ bzr edit --write foo.c M 644 inline doc/partial-commit.txt data 880 Partial commit ************** In the first cut, commit covers the whole tree. Secondly, people might want to commit only particular files or subdirectories. Semantics of this in the case of renames/adds/deletes may be a bit complex, but I think it can be done. The basic point is to build an inventory which includes only the specified changes from the working copy. Beyond that, it can be very nice in darcs to commit only selected changes to a file. I think this should not be the default though; perhaps only do it with ``-i``. Even better (for some users) kick off a graphical tool to select the particular regions. Beyond a certain point it may become easier for the user to explicitly set aside some changes and commit others first. In any case, it is a good idea to run a test suite on the revision to be committed, to make sure there were no missed dependencies. M 644 inline doc/pool.txt data 2930 Patch pools *********** *Patch pools* are an optimization for efficient storage of related branches. They are not required for the first release. Revisions, inventories, and file states are identified in Bazaar-NG by universally unique hashes, and they are never modified once they are created. Objects which are common between branches may therefore be stored only once and referenced from each branch. Various strategies are available for doing this. Objects can be held hard-linked between related branches on filesystems that support hardlinks. This provides automatic reference counting as branches are deleted. It is common to have several branches of the same project on a machine, with many objects in common. These can be configured with each other on the pool path. The parent should be the default pool path when creating a new branch. Each user might also have a pool that acts as a cache of all remote revisions. Such a cache might use some kind of least-recently-used policy to limit its size. The user might nominate a series or hierarchy of pools to be searched for a patch; these might be progressively on the local machine, local network and remotely. A system like the supermirror might make good use of a pool that gradually accumulates all public objects in the world, and stores branches very cheaply. One complication is garbage collection. Naive implementations that store references from branches into the pool will not be able to detect objects that are no longer referenced by any active branch; as branches are created and deleted over time such objects will accumulate. This may not be a problem in many cases, given the relative abundance of disk compared to programmer time, and the relatively small number of long branches that are discarded. There are some partial solutions: * Keep a reference count for each object. There is no problem of circular references. However, keeping the count accurately requires that branches are never lost or deleted other than through the correct mechanism. * From time to time, building a new pool including only objects from active branches. * Keeping a pool that holds only patches known to be available from elsewhere, so the pool is only a cache and never the single source of a particular object. Such a pool can then be discarded at will and the objects will be re-fetched from their original source. This last point suggests that new objects should never be written solely into a pool, because of the risk that they might be accidentally lost. Using the parent on the default pool path allows for varying greed or laziness in fetching objects. By default, objects might be read only as necessary, and then stored in the local cache. If the user wants to keep the whole history available locally that could be specified with a ``--greedy`` option when making the branch, or through later pulling down the history. M 644 inline doc/purpose.txt data 2511 What is the purpose of version control? *************************************** There are several overlapping purposes: * Allowing concurrent development by several people. Primarily, helping resolve changes when they are integrated, but also making each person aware of what the others have been doing, allowing them to talk about changes, etc. * To allow different lines of development, with sensible reintegration. e.g. stable/development, or branches for experimental features. * To record a history of development, so that you can find out why a feature went in or who added it, or which releases might be affected by a bug. * In particular, as a *record of decisions* about the project made by the developers. * Help in writing a gloss on that history; not simply a record of events but an explanation of what happened and why. Such a record may need to be written at several levels: a very detailed explanation of changes to a function; a note that a bug was fixed and why; and then a brief NEWS file for the whole release. While past events can never change, the intepretation placed upon them may change, even after the event. For example, even after a release has gone out, one might want to go back and note that a bug was actually fixed. The system is helping the developers tell a story about the development of the project. * As an aid to thinking about the project. (Much as a personal journal is not merely a record or even analysis of events, but also a chance to reflect and to think of the future.) People can review diffs, and then write a description of what changed, and in doing so perhaps realize something else they should do, or realize they made a mistake. Making branches helps work out the order of feature integration and the stability of different lines of development. * As an 'undo' protection mechanism. This is one reason why version control can be useful on projects that have only a single developer and never branch. * Incidentally, as a backup mechanism. Version control systems, particularly distributed systems, tend to cause code to exist on several machines, which gives some protection against loss of any single copy. It's still a good idea to use a separate backup system as well. * As a file-sharing mechanism: even with just a single developer and line of development it can be useful to keep files synchronized between several machines, which may not always be connected. M 644 inline doc/python.txt data 4230 Choice of Python ---------------- This will be written in Python, at least for the first cut, just for ease of development -- I think I am at least 2-3 times faster than in C or C++, and bugs may be less severe. I am open to the idea of switching to C at some time in the future, but because that is enormously expensive I want to avoid it until it's clearly necessary. Python is also a good platform to handle cross-platform portability. Possible reasons to go to C: Audience acceptance If Linus says "I'd use it if it were written in C" that would be persuasive. I think the good developers we want to do not consider implementation language as a dominant factor. A few related but separate questions are important to them: modest dependencies, easy installation, presence in distributions (or as packages), active support, etc. A few queries show that Python is seen as relatively safe and acceptable even by people who don't actually use it. Speed Having scalable designs is much more important. Secondly, we will do most of the heavy lifting in external C programs in the first cut, and perhaps move these into native libraries later. (Subversion people had trouble in relying on GNU diff on legacy platforms and they had to integrate the code eventually.) Bindings to other languages If we have only a Python interpretation then it can be run as a shell script from emacs or similar tools. It can also be natively called from Python scripts, which would allow GUI bindings to almost every toolkit, and it can possibly be called from Java and .NET/Mono. By the time this is mature, it's possible that Python code will be able to cross-call Perl and other languages through Parrot. There should be enough options there to support a good infrastructure there of additional tools. If it was necessary to provide a C API that can perhaps be wrapped around a Python library. Reuse of tla code That may be useful, if there are substantial sections that approach or meet our goals for both design and implementation (e.g. being good to use from a library.) This does not necessarily mean doing the whole thing in C; we could call out to tla or could wrap particular bits into libraries. ---- Erik Bågfors: However, I think it's very important that a VCS can be wrapped in other languages so that it can be integrated in IDE's and have tools written for them. A library written in c would be simple to wrap in other languages and therefore could be used from for example monodevelop and friends. I really believe this is important for a VCS. I agree; this is a more important argument against Python than speed, where I think we can be entirely adequate just using smart design. But there are some partial answers: We can design bzr to be easily called as an external process -- not depending on interactive input, having systematically parsed output, --format=xml output, etc. This is the only mode CVS supports, and people have built many interesting tools on top of it, and it's still popular for svn and tla. For things like editor integration this is often the easiest way. Secondly, there is a good chance of calling into Python from other languages. There are projects like Jython, IronPython, Parrot and so on that may well fix this. Thirdly, we can present a Python library through a C interface; this might seem a bit wierd but I think it will work fine. Python is easily embeddable; this might be the best way for Windows IDE integration. Finally, if none of these work, then we can always recode in C, treating Python only as a prototype. I think working in Python I can develop it at least twice as fast as in C, particularly in this early phase where the design is still being worked out. Although all other things being equal it might be nice to be in pure C, but I don't think it's worth paying that price. One of the problems with darcs is that it's such a mess wrapping it. Yes. ---- Experiments to date on large trees show that even with little optimization, bzr is mostly disk-bound, and the CPU time usage is only a few seconds. That supports the position that Python performance will be adequate. M 644 inline doc/quickref.txt data 549 bzr init Prepare to record history in a new working directory. bzr add FILE... Make one or more files be versioned. This puts the file in the "added" state, and it will be added by the next commit. bzr commit [-s MESSAGE] [FILE...] Commit pending changes, with the given commit message. If no message is given you will be prompted to enter one. bzr diff [-r REV] [FILE...] bzr status [FILE...] Show a brief description of the status of a file. Status codes ------------ A Added D Deleted M Modified M 644 inline doc/quilt.txt data 253 Quilt ===== Descendent of Andrew Morton's patch scripts. Demonstrates that quite a powerful tool can be built in a relatively short time. Is largely not concerned with preserving a history record. Allows Pop patches off, similar to excluding them. M 644 inline doc/random.txt data 8240 I think Ruby's point is right: we need to think about how a tool *feels* as you're using it. Making regular commits gives a nice rhythm to to working; in some ways it's nicer to just commit single files with C-x v v than to build complex changesets. (See gmane.c.v-c.arch.devel post 19 Nov, Tom Lord.) * Would like to generate an activity report, to e.g. mail to your boss or post to your blog. "What did I change today, across all these specified branches?" * It is possibly nice that tla by default forbids you from committing if emacs autosave or lock files exist -- I find it confusing to commit somethin other than what is shown in the editor window because there are unsaved changes. However, grumbling about unknown files is annoying, and requiring people to edit regexps in the id-tagging-method file to fix it is totally unreasonable. Perhaps there should be a preference to abort on unknown files, or perhaps it should be possible to specify forbidden files. Perhaps this is related to a mechanism to detect conflicted files: should refuse to commit if there are any .rej files lying around. *Those who lose history are doomed to recreate it.* -- broked (on #gnu.arch.users) *A universal convention supplies all of maintainability, clarity, consistency, and a foundation for good programming habits too. What it doesn't do is insist that you follow it against your will. That's Python!* -- Tim Peters on comp.lang.python, 2001-06-16 (Bazaar provides mechanism and convention, but it is up to you whether you wish to follow or enforce that convention.) ---- jblack asks for A way to subtract merges, so that you can see the work you've done to a branch since conception. ---- :: now that is a neat idea: advertise branches over zeroconf should make lca fun :-) ---- http://thedailywtf.com/ShowPost.aspx?PostID=24281 Source control is necessary and useful, but in a team of one (or even two) people the setup overhead isn't always worth it--especially if you're going to join source control in a month, and you don't want to have to migrate everything out of your existing (in my case, skunkworks) system before you can use it. At least that was my experience--I putzed with CVS a bit and knew other source control systems pretty well, but in the day-to-day it wasn't worth the bother (granted, I was a bit offended at having to wait to use the mainline source control, but that's another matter). I think Bazaar-NG will have such low setup overhead (just ``init``, ``add``) that it can be easily used for even tiny projects. The ability to merge previously-unrelated trees means they can fold their project in later. ---- From tridge: * cope without $EMAIL better * notes at start of .bzr.log: * you can delete this * or include it in bug reports * should you be able to remove things from the default ignore list? * headers at start of diff, giving some comments, perhaps dates * is diff against /dev/null really OK? I think so. * separate remove/delete commands? * detect files which were removed and now in 'missing' state * should we actually compare files for 'status', or check mtime and size; reading every file in the samba source tree can take a long time. without this, doing a status on a large tree can be very slow. but relying on mtime/size is a bit dangerous. people really do work on trees which take a large chunk of memory and which will not stay in memory * status up-to-date files: not 'U', and don't list without --all * if status does compare file text, then it should be quick when checking just a single file * wrapper for svn that every time run logs - command - all inputs - time it took - sufficient to replay everything - record all files * status crashes if a file is missing * option for -p1 level on diff, etc. perhaps * commit without message should start $EDITOR * don't duplicate all files on commit * start importing tridge-junkcode * perhaps need xdelta storage sooner rather than later, to handle very large file ---- The first operation most people do with a new version-control system is *not* making their own project, but rather getting a checkout of an existing project, building it, and possibly submitting a patch. So those operations should be *extremely* easy. ---- * Way to check that a branch is fully merged, and no longer needed: should mean all its changes have been integrated upstream, no uncommitted changes or rejects or unknown files. * Filter revisions by containing a particular word (as for log). Perhaps have key-value fields that might be used for e.g. line-of-development or bug nr? * List difference in the revisions on one branch vs another. * Perhaps use a partially-readable but still hopefully unique ID for revisions/inventories? * Preview what will happen in a merge before it is applied * When a changeset deletes a file, should have the option to just make it unknown/ignored. Perhaps this is best handled by an interactive merge. If the file is unchanged locally and deleted remotely, it will by default be deleted (but the user has the option to reject the delete, or to make it just unversioned, or to save a copy.) If it is modified locall then the user still needs to choose between those options but there is no default (or perhaps the default is to reject the delete.) * interactive commit, prompting whether each hunk should be sent (as for darcs) * Write up something about detection of unmodified files * Preview a merge so as to get some idea what will happen: * What revisions will be merged (log entries, etc) * What files will be affected? * Are those simple updates, or have they been updated locally as well. * Any renames or metadata clashes? * Show diffs or conflict markers. * Do the merge, but write into a second directory. * "Show me all changesets that touch this file" Can be done by walking back through all revisions, and filtering out those where the file-id either gets a new name or a new text. * Way to commit backdated revisions or pretend to be something by someone else, for the benefit of import tools; in general allow everything taken from the current environment to be overridden. * Cope well when trying to checkout or update over a flaky connection. Passive HTTP possibly helps with this: we can fetch all the file texts first, then the inventory, and can even retry interrupted connections. * Use readline for reading log messages, and store a history of previous commit messages! ---- 20050218090900.GA2071@opteron.random Subject: Re: [darcs-users] Re: [BK] upgrade will be needed From: Andrea Arcangeli Newsgroups: gmane.linux.kernel Date: Fri, 18 Feb 2005 10:09:00 +0100 On Thu, Feb 17, 2005 at 06:24:53PM -0800, Tupshin Harper wrote: > small to medium sized ones). Last I checked, Arch was still too slow in > some areas, though that might have changed in recent months. Also, many IMHO someone needs to rewrite ARCH using the RCS or SCCS format for the backend and a single file for the changesets and with sane parameters conventions miming SVN. The internal algorithms of arch seems the most advanced possible. It's just the interface and the fs backend that's so bad and doesn't compress in the backups either. SVN bsddb doesn't compress either by default, but at least the new fsfs compresses pretty well, not as good as CVS, but not as badly as bsddb and arch either. I may be completely wrong, so take the above just as a humble suggestion. darcs scares me a bit because it's in haskell, I don't believe very much in functional languages for compute intensive stuff, ram utilization skyrockets sometime (I wouldn't like to need >1G of ram to manage the tree). Other languages like python or perl are much slower than C/C++ too but at least ram utilization can be normally dominated to sane levels with them and they can be greatly optimized easily with C/C++ extensions of the performance critical parts. M 644 inline doc/requirements.txt data 1030 ************ Requirements ************ gcc requirements ---------------- * http://lists.gnu.org/archive/html/gnu-arch-users/2004-06/msg00082.html * http://gcc.gnu.org/ml/gcc/2004-06/msg00264.html Some of these are: * Must preserve existing history from CVS; must be able to export back out to CVS in case they change their mind. * Support a single central blessed version. * Cheap branches and tagging. * Handle repeated merges; remember the last time something was merged. * Easy cloning. * Copy a named patch set (one or more changes) onto a particular branch. * Unix, Windows, Mac. * What patches changed a particular symbol? * annotate function that shows deleted lines. * Reliable backups. (bzr can do this simply by cloning, then mirror-sync.) * Scale to enormously wide and long history. * Easy to get back to last-known-good state. kernel ------ * http://www.kerneltraffic.org/kernel-traffic/kt20030323_210.txt Discussion of BitKeeper, strengths and weaknesses, and interoperation tools. M 644 inline doc/revision-syntax.txt data 2445 Syntax for identifying revisions ******************************** There seem to be two main options: use a separate ``-r`` option, or put a revision identifier within the filename. ``-r`` options are familiar from CVS and Subversion, and easy to specify, and cover almost all situations. Special identifiers in the filename are sometimes more concise, and allow for more complete specification of filenames in the presence of renames. They may be more complex to implement, and will cause some filenames to be either forbidden or to require escaping. ---- How to interpret revisions in the presence of renamed files? Suppose hello.c was renamed in r3; when we do ``bzr diff -r2 hello.c`` should we compare to * the file currently called ``hello.c``, as it was in revision 2, even if it then had a different name or * the file that was called ``hello.c`` in revision 2, even if that is a different file-id? Often only one will work, but it is possible to have situations where in revision 2 there was a file called ``hello.c`` but it is not the same one that now has that name. If we can have a simple syntax that accomodates either that would be good. One might be to mix revision numbers into the path, rather than a separate ``-r`` option. For example:: hello.c@2 @2/hello.c src/@2/hello.c The simplest case is to just append the number, but you can vary them. It might even be possible to chain them, getting the file id from one revision and its state from another: @70/hello.c@31 This means "the file that was called ``hello.c`` in revision 70, give me its state in revision 31." (If you wanted to do this across branches, I think you might need to get the file id and then look it up, etc.) Some more syntax: you can refer to a revision by its hash just by specifying that; we can distinguish them from anything else by their length:: hello.c@09fac8dbfd27bd9b4d23a00eb648aa751789536d (Is that really safe? Perhaps some punctuation is needed to distinguish it from a label. But then I think people should rarely or never use them; perhaps it need not be allowed at all.) You can also give a date, which is used to find the most recent revision no later than that date:: hello.c@{2004-12-01} hello.c@{yesterday} ./@{10:30}/ (Braces can be a bit scary for shell, but if there are no commas they are somewhat safe.) Or, once we have labels, you can use a label:: hello.c@release-2.2 M 644 inline doc/roadmap.txt data 1480 Roadmap for Bazaar-NG ********************* (This document contains only things still-to-do, in in approximate order. For a list of things already done, see the `development news`__.) __ news.html * get doctest going again now that we have predictable-order status commands * note at top of .bzr.log * basic local versioning working: init, add, remove, mv, status, log, diff, commit, revert * revert command * mv command * some commands should work on selected files, if any are given: * diff * status * log * commit * work properly when invoked from a subdirectory * give a file-id to tree root? * use stat information to detect unchanged files without reading the full text (or establish that this is not safe). * basic find command: find versioned, deleted, unknown, etc. * branch command * get a branch from an http server (remote Tree/Store/Branch proxy classes) * write experimental weave algorithm in Python to see how it works? * update command: pull in changes from another branch that is a strict superset of the destination * merge command: reconcile changes in this branch with those in another. punt on structural changes at first and only merge text, then work out a nice way to resolve structure. * ``vc-bzr.el`` emacs support * *go self-hosting at about this point*, with parallel commits to baz * ignore patterns from ``.bzrignore`` longer-term things: * Some kind of `compressed storage `_. M 644 inline doc/rollup.txt data 2640 Roll-up changesets ****************** Sometimes it is useful to collapse changes; sometimes it is useful to pull in patches while keeping their original identity. So (uniquely?) Bazaar-NG allows either option at the user's discretion. I'm not completely sure yet how this should be controlled: should it be an option at every stage, or should mergers generate roll-up changesets? I think possibly the second. Rolled-up changesets can be represented as so:: mbp, 2005-04-01 import new quantum-computing feature jrh, 2005-03-02 stub implementation for quantum computing jrh, 2005-03-03 plus operator jrh, 2005-03-04 regexps jrh, 2005-03-03 fix bugs, ready to go down to an arbitrary level of nesting. When bringing changes into a branch, we have the choice of either rolling them up into a single changeset or bringing them across individually. I'm not sure, but I think this may be best handled by having two separate commands: * *update* (or maybe *pull*) brings across revisions onto the current branch, adding them one-by-one into the revision history. This can only be executed if the destination branch is a prefix of the source branch and therefore we know the patches will apply perfectly. * *merge* pulls in one or more changes from another branch, and allows the user to edit the merged state or fix any conflicts. These changes are then all marked as rolled-up by a new commit. It is possible to look up those changes inside the commit, if the information is still accessible. In this model, Arch is missing the first command: you cannot put patches from one branch onto another -- and it would be hard to add it, since the unique branch id is part of the name of the patch name. Most other tools are missing the second: it is impossible to combine changesets and therefore the total history of any branch can grow very long; there is no way to make summaries within the tool itself. People might also want to retrospectively zip up several changes existing on a branch, but I think it may be better to make a new branch from before the changes and pull them across onto that. ---- One interesting application of this is in maintaining the ``NEWS`` file present in many software distributions. This summarizes the changes to the software in each release at a very high user-visible level. Although this is very much version-control data it is not normally known to the VC system, except as another versioned file. One way to handle this is to have a *release* branch, where the commit message for each release the NEWS notes for that release. M 644 inline doc/scalability.txt data 650 *********** Scalability *********** bzr needs to scale up very well: projects with tens of thousands of commits, tens of thousands of files, and tens of thousands of branches. We are concerned with both the big-O performance of the design, and the multiplicative factors of the implementation. Both is important. For example, darcs, svn and arch use more than one inode per working file (pristine, id file, etc). This is only a constant factor, but enough to more than double the space used by a typical tree. We would like to avoid it if we can. From a early stage in development the features which do work should be tested on large trees. M 644 inline doc/security.txt data 14662 ***************************** Security aspects of Bazaar-NG ***************************** * Good security is required. * Usability is required for good security. Being too strict "because it's the secure way" just means that people will disable you altogether, or start doing things that they know is wrong, because the right way of doing this may be secure, but [..] also very inconvenient. -- Linus Torvalds .. contents: Requirements ============ David Wheeler gives some good requirements__: Problem is, the people who develop SCM tools often don't think about what kind of security requirements they need to support. This mini-paper describes briefly the kinds of security requirements an SCM tool should support. __ http://www.dwheeler.com/essays/scm-security.html confidentiality_ Are only those who should be able to read information able to do so? integrity Are only those who should be able to write/change information able to do so? This includes not only limiting access rights for writing, but also protecting against repository corruption. availability Is the system available to those who need it? (I.E., is it resistant to denial-of-service attacks?) identification/authentication Does the system safely authenticate its users? If it uses tokens (like passwords), are they protected when stored and while being sent over a network, or are they exposed as cleartext? audit Are actions recorded? non-repudiation Can the system "prove" that a certain user/key did an action later? self-protection Does the system protect itself, and can its own data (like timestamps) be trusted? trusted paths Can the system make sure that its communication with users is protected? Attacker categories ------------------- * Unprivileged outsiders. (Almost always read-only, but people might want to allow them to write in some cases, e.g. for wikis.) * Non-malicious developers with privilege. * Malicious developers with privilege. * Attackers who have stolen a privileged developer's identity. Access control -------------- Dan Nicolaescu gives these examples of access control: - security related code that is still emabargoed, only select few are allowed to see it, it is not desirable to release this information to the public because a fix is still being worked on. It would be nice to be able to have this kind of code under the same version control system used for normal development for ease of use and easy merging, yet it is crucial to restrict access to a branches, files or directories to certain people. - feature freeze before a release. It would be good if the release manager could disable writing to the release branch, so that the last tests are run, and not have someone commit stuff by mistake. - documentation/translation writers don't need write access to the whole source code, just to the documentation directories. - For proprietary companies restricting access is even more important, for example only some engineers should access the latest development version of some code in order to keep some trade secrets, etc, etc. In Bazaar-NG, the basic unit of access control is the branch. If people are not supposed to read a branch, or know of its existence, put it somewhere where they can't see it. If people are allowed to read from but not write to a branch then set those permissions. The code can later be merged into a public branch if desired with no loss of function. We largely rely on lower-level security measures controlling who can get read or write access to a branch. If you have a branch that should be confidential, then put it on an appropriately-secured machine, with only people in a particular group allowed to read it. Not having separate repositories is probably a feature here -- unlike Subversion, no features depend on having branches be in the same repository. Each repository can have different group ownership. (The directories should usually be setgid.) It also makes it easier to see just what the access control is; there is only one object that can meaningfully have an ACL. The existence of a secret branch can be fairly well hidden from the world. When its changes are merged in, all that is visible is the name, date, and branch name of the commit, not anything about the location of the source branch. The documentation case I would handle by having a separate documentation branch, which could perhaps be checked out into a subdirectory when it is required. I think this is fairly common for larger projects even in CVS. Confidentiality --------------- As dwheeler points out, this can be important even for open source projects, such as when preparing a security patch. Mechanisms that send email should have an option to encrypt the mail. I can't think of anywhere encrypted archives would be useful. If you want to store it on an encrypted filesystem you can. If you want to store encrypted files you can do that too, though that will leak some information in the metadata and branch structure. Security in distributed systems ------------------------------- If I have a branch on my laptop, the software ultimately cannot prevent me doing anything to that branch -- physical access trumps software controls. We can, at most, try to prevent non-malicious mistakes. The purpose of the software here is to protect other people, whose machines I do not control. In particular, it should be hard for me to lie to them; the software should detect any false statements. In particular, these should be prevented: * Claiming to be someone else. * Attempting to rewrite history. Revocation ---------- Suppose Alice's code-signing key is stolen by an attacker Charles. Charles can sign changesets purporting to come from Alice. Alice needs to revoke that key; hopefully she has saved a copy of the key elsewhere and can use that to revoke it. Failing that she can mail everyone and ask them to delete it. This can propagate through the usual GPG mechanism, which is very nice. Alice also needs to make a new key and get it trusted. This revocation does not distinguish between changesets genuinely signed by Alice in the past, and changesets fraudulently signed by Charles. What can Alice do now? First of all, she needs to work out what changesets signed by her key can still be trusted. One good way to do this is to check against another branch signed by Bob. If Bob's key is safe, we know his copy of Alice's changesets are OK and the full tree at various points is OK. Then: * Go through her old changesets, check that they're OK -- perhaps restore from a trusted backup. Re-sign those changesets with a new key bound to the same email address. Publish the new signatures instead. (This seems to indicate it is a good idea to bind signatures to changeset by author name/address rather than by key ID.) * Roll-up all previous development into a new tree, then sign that. This means there is no safe access to the previous individual changes, but in some cases it may be OK. If a key is revoked at a particular time then perhaps we could still trust commits made before that time. I don't know if GPG revocations can support that. Old keys -------- Keys also expire, rather than being revoked. What does this mean? Ideally we would check that the date when a changeset claims to have been signed is within the validity period of the key. This requires more GPG integration than may at the moment be possible, but in theory we can do it. Also need to make sure that commits are in order by date, or at least reasonably close to being in order (to allow for some clock skew). One interesting case is when version is committed for which both the public and private keys have been lost. This will always be untrusted, but that should not prevent people continuing to use the archive if they can accept that. This suggests that perhaps we should allow for multiple signatures on a single revision. Encumbrance attacks ------------------- A special case where we need to be able to destroy history to avoid a legal problem. Allowed as discussed elsewhere: either destroy commits from the tail backwards, or equivalently branch from a previous revision and replace with that. People who saw the original branch can still prove it happened; people who look in the future will not see any record. Either way, probably requires physical branch access. Multiple signature keys ----------------------- Should we allow for several signatures on a single changeset? What would that mean? How do we know what signatures are meaningful or worthwhile? Forensics --------- dwheeler: [O]nce you find out who did a malicious act, the SCM should make it easy to identify all of their actions. In short, if you make it easy to catch someone, you increase the attackers' risk... and that means the attacker is less likely to do it. dwheeler asks that the committer's IP address be recorded. Putting this in the changeset seems to cause too much of a privacy/confidentiality problem. However, an active server might reasonably record the IPs of all clients. Non-repudiation --------------- If a changeset has propagated to Bob, signed by Alice's key, then Bob can prove that someone possessing Alice's key signed it. Alice's only way out is to claim her key was stolen. Trusted review -------------- Can be handled by importing onto another branch. Can have various levels for "quickly checked", "deeply trusted", etc. (Is it really necessary to import onto a new branch rather than add anotations to existing branches? Copying the whole text seems a bit redundant. This might be a nice place for arch-style taggings, where we just add a reference to another branch.) Hooks ----- Automatically running hooks downloaded from someone else is dangerous. In particular, the user may not have the chance to check the hooks are reasonable before they are run. Conversely, users can subvert client-side hooks. If we want to run a check before accepting code onto a shared branch, that must run on the server. Enforcing server-side checks gives a good way to run build, formatting, suspiciousness checks, etc. This implies that write access to a repository is through a mediating daemon rather than by directly writing. Signing ------- We use signing to prove that a particular person (or 'principal', possibly a robot) committed a particular changeset. It is the job of external signing software to help work out whether this is true or not. This has several parts: * Mathematical verification that a signature on a particular changeset header document is correct * Determining that the signature corresponds to a particular public key * Determining that the public key corresponds to the person claimed to have authored the changeset (identified by email address.) The second two are really PKI functions, and somewhat harder than the first. The canonical implementation is to use GPG/OpenPGP, but anything will do. There are simpler RSA/DSA implementations which assume each user manually builds a list of trusted keys. This leaves open the question of which people should be trusted to provide software on a particular branch or at all. This is not a very easy question for software to answer. We assume that people will know by other means. For public code, it may be that all changesets are re-signed by say samba-team@samba.org. I think it is fair to distinguish people by an email address, or at least by $ID@$DOMAIN. There is no need to have this actually receive email, so spam need not be a problem. The signing design is inspired by the very usable security afforded by OpenSSH: it automatically protects where it can, and allows higher security to users who want to do some work (by offline verification of signatures). Using a signing mechanism other than GPG when key developers already have GPG and there is a big infrastructure to support it seems undesirable. It is true that GPG is quite complex. The purpose of signing is to protect against unauthorized modification of archives. Bazaar-NG can apply a GPG signature to both patches and manifests. This vallows a later proof that the revision and the changeset were produced by the author they claim to have been written by. We cannot cryptographically prove that a particular patch was merged into a branch, because the person doing the merge might have subverted the patch in the process of merging it. All we can prove cryptographically is that the merge committer asserts they took the patch. GPGME and PyMe seem to give a reasonable interface for doing this: there is a function to check a signature, and the return indicates the signing name, with possible errors including a missing key, etc. Sign branches, not revisions '''''''''''''''''''''''''''' Aaron Bentley suggested the interesting idea of signing the mapping of revisions onto branches, rather than revisions themselves. For example a branch could contain just a signed pointer to the most recent revision. (It probably is useful to be able to check signatures on previous revisions, for example when recovering from an intrusion.) Protocol attacks ---------------- Both client and server should be resistant to malicious changesets, network requests, etc. There's no easy solution. * Defense in depth. Check reasonablenes at various points. * Disallow changesets that try to change files outside of the branch. Availability ------------ bzr can be configured so as to have no single point of failure to a denial-of-service attack (or at least nearly none): * Can have any number of mirrors of a branch. * If a central server is taken out, developers can continue working with state they already have (unbind their branches), and can collaborate by email or other means until the server is repaired or replaced. * The origin branch can be on a machine whose location is secret and which is not directly publicly accessible. * Branches can be moved between machines or IP addresses without disrupting anything else. * Branches can be moved around out-of-band, as tarballs over bittorrent, etc. I think the only possible denial of service attacks are those that aim to shut down the entire network, or block communication with individual developers, for example by flooding their email address. But if those people can get connected through some other means, they can continue. M 644 inline doc/shared-branches.txt data 5834 *************** Shared branches *************** How much can we simulate the single-branch model of svn/cvs? GNU arch does reasonably well, but at the cost of making disconnected/connected operation leave ugly merge marks in the project history. This does seem to be a drawback of the otherwise-good branch/tree fusion. `Greg Hudson`__ makes the very good point that the Linux kernel is quite unusual among free software projects in relying on human integrators, rather than having a shared branch between the committers. The other case is very small projects (distcc?) where there is a single person with commit access. __ http://web.mit.edu/ghudson/thoughts/bitkeeper.whynot I think decentralized systems can still be very good, but they need to support this model very well: a team of committers, working on a single trunk branch, but merging from other branches as well. The Samba team is more typical of a demanding open-source project than the kernel is. People fundamentally want to be able to share a tree and all commit into it. It's fine to support other models (like sending patches to an owner/integrator/maintainer), but it is not reasonable to require that there be exactly one such person. arch-pqm is a clever kludge; shouldn't be required. Setting up email on all clients, and scripts to automatically process email on the server is certainly possible. But for some people it may not be easy; it is never trivial to debug; and particularly for Windows users it may be very hard. If you don't control your own email server or cannot easily run scripts it will be even more messy. The model of Bitkeeper, and the default model of Bazaar-NG, is that each branch has one owner and exists in one place. People submit their patches to the owner of the branch. This is a pretty good model, but has some limitations. It is a tough transition from CVS; it requires that people learn a whole new way of working as well as a new tool. If the person who owns the branch is away, should all integration work stop? I'm not totally confident in Darcs; it seems like the shared branch can reach a state that is not exactly the same as any of the checkouts. Maybe not? In other respects it is a good model. Possible solution: bound branches --------------------------------- You can make a branch which is a write-through mirror of another branch. They work like CVS checkouts in that you must be connected and fully up-to-date to commit. These can hold as much history as you like from the main branch, so you can view history, annotate, make new branches when offline. If you are offline for a long time then make a new branch, then integrate it back later. The effect is similar to systems which have separate working copies and storage: we have one branch used primarily as a workarea, and another used as storage. I think that making them both branches is possibly cleaner: it expresses the way some state may be held locally, allows offline branching from the bound branch, etc. :: $ bzr get http://foo.net/foo ./foo-main $ cd foo-main (make some changes) $ bzr commit -m 'Add my neat feature' bzr: error: this branch is not-up-to-date with master http://foo.net/foo - run 'bzr update' - or detach this branch from its parent to work independently $ bzr update - setting aside local changes - pulling changes from parent - putting back local changes $ bzr commit -m 'Add my neat feature' The ``get`` command is very similar to ``branch``, but means that commits will be written back. This means that the ``pull`` or ``update`` command must be able to work even when there are local changes, by setting them aside. This in turn means that local changes may conflict with remote changes, and that has to be resolved -- that is no worse than in pulling in changes from elsewhere. commit to a master repository should fail if the slave's patch history is not exactly equal to that of the master -- the slave has diverged. If the slave history is a prefix of the master history then an update will do it. Otherwise, they have diverged and the slave gets detached. We can have shared server-side hooks, even for access control, at least in principle. Perhaps ``bind`` is a nicer word than ``slave``. It is conceptually possible to take an existing branch and bind it to another, as long as the history of the first is a prefix of that of the second. One could also unbind a branch. These should at most be advanced commands, because in general it is simpler just to make a new branch with the desired properties. Difficulties ~~~~~~~~~~~~ It may be a bit hard to get a remote commit exactly right, particularly if we want to keep a working copy there. If working copies are optional, then turning it off will keep things simple but not totally avoid the problem. I think this is just an example of a general problem though: there is no totally satisfactory way to atomically update a working copy. (What happens to cvs or svn if you interrupt while it's doing this? Generally you get a broken wc.) What we can do is create a lock while the update is taking place, which can be used to at least detect the problem. If changes are allowed to the working copy of the master branch then they might conflict with what is committed by the slave. Should those changes be merged into the working copy of the parent? If so, they might conflict. (darcs handles this case by inserting conflict markers in the remote file, which seems unsatisfactory to me.) So I am inclined to say that at the moment of pushing to a master branch, the destination should be clean. If we have explicit edits then probably there should be no editable files there. (We could perhaps make all the files read-only for the duration of the update.) M 644 inline doc/short-demo.txt data 5732 ****************************** Learn Bazaar in twenty minutes ****************************** :Author: Martin Pool :Date: November 2004, London *Bazaar* is an open source distributed version control system. A version control system helps you develop software (or other documents) In Bazaar, branches hold a history of their changes. All working copies are branches, and all branches are working copies. Let's get a copy of someone else's published project:: $ baz branch http://baz.alice.org/hello-2.6 This creates a directory called ``hello-2.6`` in your current directory, containing Alice's *Hello World* 2.6 tree and a history of her changes. We'll leave this directory containing a pristine copy of her work for future reference. We want to add a new feature. Branching and merging is are cheap and easy in Baz, so development of each new feature should be done on a new branch -- this lets you merge them in different orders, and leave aside new development to fix a bug if you want. It's good to choose a meaningful name for your development branches so that you can keep them straight. So let's make an additional branch, based off our local copy of Alice's branch:: $ baz branch ./hello-2.6 ./hello-2.6-goodbye This command completed quickly: we didn't need to fetch anything from Alice's server to make a new branch. Now we have a second directory containing another copy of the source, with a different name. Of course both are still the same as Alice's source. Time to hack:: $ cd hello-2.6-goodbye $ vi hello.c We add a new and much-desired ``--goodbye`` option, build and test it. Having got to a good state, we think we're ready to commit our changes. Many people find it helpful to review the changes before committing them, just to make sure there were no accidental mistakes:: $ baz diff | less This shows the changes relative to the version we started with. If that looks OK, we're ready to commit:: $ baz commit -m 'Add --goodbye option' This records the changes into this branch. We can see that they got recorded:: $ baz log | less This will show our change as the most recent, preceded by all our development. If we only want to see our own changes, we can filter the log in various ways:: $ baz log --author bruce@ $ baz log --date today (That second one might include anything Alice did before we started.) Since our changes were committed, we don't have anything outstanding, as we can see with this command:: $ baz diff No changes Thinking about it a bit more, we decide that it's too messy to mix the "Goodbye" implementation in with "Hello world". We start up our editor and split it our into a new separate file, ``bye.c``. By default, Baz assumes unknown files in the working directory are scratch files -- the same behaviour as CVS and Subversion. We need to tell Baz that this file should be versioned, and then we can commit:: $ baz add bye.c $ baz commit -s 'Refactor bye() into separate file' Of course this commits all our changes as a single atomic transaction. If we wanted, we could tell Baz to build the program before committing to help ensure we never get a broken build. We're ready to publish our improved Hello World, but first we want to make sure we've integrated anything Alice has done while we were working. First we update our mirror of Alice's branch:: $ baz sync ../hello-2.6 This pulls the history of her branch onto our machine; we could disconnect from the network at this point and do all the merging work without needing to connect to ``alice.org``. Now we'll pull Alice's new changes into our branch. We can use a mirror of her branch, or we can go straight to her public branch. In fact, we can merge from different copies of the same branch at different times, and Baz will still understand which changesets have already been merged. Either of these commands will work:: $ baz merge ../hello-2.6 $ baz merge http://baz.alice.org/hello-2.6 This pulls down any changesets on Alice's branch that aren't in ours, and applies them to our tree. Since we've made changes in parallel with Alice, those changes might conflict. Baz by default will insert CVS-style conflict markers into your branch if that happens, but you can also ask it to run a graphical merge tool or to tell emacs to do the merge. Baz will help you merge changes other than file text, such as a file being renamed in one branch. By running ``baz diff`` we can see the effect of her changes on our tree. By running ``baz log --pending`` we can see a description of all of the changes that were pulled in. Once the changes have been reconciled (which will often happen automatically), we can commit this to our branch:: $ baz commit -s 'Merge from Alice' Baz records that our change incorporates those patches from Alice:: $ baz log | less Now that we have the feature we wanted, we can publish our branch on the web. We could copy it up using ``baz branch``, but because Baz branches are simply directories we can just rsync it onto a web server, even if the web server doesn't have Baz installed:: $ rsync -av ./hello-2.6-goodbye webserver.com:public_html/ We send an email to Alice asking her to merge from this location. Typically she'll want to have a look at those changes before accepting them, and she can do that by running ``merge`` then ``diff``, as we did before. If she likes the change, she can merge it, possibly applying some fixes in the process. Next time we merge from her, we'll see our changes are in. .. Local variables: .. mode: rst .. compile-command: "rest2html short-demo.txt > short-demo.html" .. End: M 644 inline doc/supportability.txt data 841 ************************ Bazaar-NG Supportability ************************ The goal is to be able to diagnose and fix a bug the first time it is reported, even if the reporter or developer can't reproduce it. Done: * Verbose logs are always written to ``.bzr.log`` in the current directory. This include all error messages and some trace messages that are not normally displayed. It also includes backtraces if an exception occurs. To do: * The --verbose option should send debug output to stderr. * Re-read changesets after writing them to a temporary file, but before permanently committing them. Make sure that they are valid XML (against RELAX-NG schema), that they can be parsed, and that they recreate the right revision. This should help prevent the archive ever getting corrupted, which is a pain to reverse. M 644 inline doc/svk.txt data 422 svk === The strategic strength is that it can trivially and reliably interoperate with upstream projects using Subversion. This tends to satisfy people who need disconnected operation, and so to allow projects to feel safe about switching to Subversion. On the other hand it may be a bit flaky in implementation; being written in Perl on top of Svn bindings may not inspire confidence; relatively little documentation. M 644 inline doc/tagging.txt data 3135 ******* Tagging ******* It is useful to be able to point to particular revisions on a branch by symbolic names, rather than revision numbers or hashes. Proposal: just use branches --------------------------- This is probably the simplest model. In both Subversion and Arch, tags are basically just branches that do not differ from their parent revision. This has a few advantages: * We do not need to introduce another type of entity, which can simplify both understanding and implementation. We also do not need a special * If branches are cheap (through shared storage, etc) then tags are also appropriately cheap. * The general idea is familiar to people from those two systems (and probably some others). * People often do put tags in the wrong place, and need to update them, or want tags that move along from one state to another (e.g. ``STABLE``.) Tags-as-branches capture this through the usual versioning mechanism, rather than needing to invent a parallel versioning system for tags. There are some problems: * Users want tags to stay fixed, but branches are things that can be committed-to. So at one level there is a mismatch. * In particular, it can be easy to accidentally commit onto a tag branch rather than a "branch branch", and that commit can easily be 'lost' and never integrated back to the right place. One possible resolution is to have a voluntary write-protect bit on a branch, which prevents accidental updates. (Similar to the unix owner write bit.) When it is necessary to update the branch, that bit can be removed or bypassed. This is an alternative to the Arch ``--seal`` mechanism. Proposal: tags within a branch ------------------------------ (This is probably "crack"; I don't think we'll do this.) You can place tags on a branch as shorthand for a particular revision:: bzr tag rel3.14.18 bzr branch foo-project--main@rel3.14.18 Tags are alphanumeric identifiers that do not begin with a digit. Tags will cover an entire revision, not particular files. Another term sometimes used is "labels"; I think we're close enough to CVS's "tags" that it's good to be consistent. However, it does possibly clash with Arch's ``tag`` command and ``id-tagging-method`` (sheesh). In Subversion a tag is just a branch you don't commit to. You *can* work this way in Bazaar if you want to. (And until tags are implemented, this will be the way to do it.) I'm not sure if tags should be versioned objects or not. Options: * Tags are added or changed by a commit; they mark previous revisions but are only visible when looking from a later commit. * Tags are not versioned; if you move them they're gone. * Tags exist within their own versioning space. It is useful to have mutable tags, in case they're incorrectly placed or need to be updated. At the same time we do not want to lose history. I think in this model it is not helpful to update tags within revisions. One approach would be to version tags within a separate namespace, so | STABLE.0 | STABLE.1 | STABLE.2 as just STABLE it finds the most recent tag. (Is this kludgy?) M 644 inline doc/taxonomy.txt data 1819 Taxonomy of version control systems =================================== (ie a look at "cutting questions" that describe different design approaches.) History in working copy ----------------------- Is history stored with working directory (bk, rcs, darcs), or separately (cvs, baz)? Storing in the working directory may be simpler in some respects; it's one less concept people need to learn. On the other hand people typically have more working copies than branches, and this approach means having more copies of the history. Naively, storing everything in the working copy can mean that you keep multiple copies of the history, which would use a lot of disk, or Rewriting history ----------------- Can you change recorded history (darcs); only do this through a special mechanism (cvs, svn); or not do it at all? Being absolutely able to reproduce any point in time reliably is highly attractive. On the other hand, sometimes people commit something (e.g. confidential information) that really should not be recorded. And at a smaller level people might just commit the wrong message. Explicit edits -------------- Can you edit any file in a working copy (cvs, tla) or must you specially mark the before editing (rcs, bk)? There are two reasons to mark files for editing. One is so that people can be notified that a file is about to Track history of patches ------------------------ Can you see how a patch got to its current destination? As far as I know only arch can do this -- you can see what branches it incorporates changes from. Well, kind of; not in an entirely straightforward manner. Collapsing patches ------------------ When patches from other people are integrated, can they be rolled up into larger patches (arch) or do they retain their individual identity (bk, darcs)? M 644 inline doc/testing.txt data 773 *********************** Test plan for Patchflow *********************** This should be amenable to good automated testing. Here are some ideas. * Corrupt the branch in various ways and make sure it is detected. - To start with test all cases checked by check() * Commit. I'm using doctest_ for some API tests, and it looks pretty nice. It adds some documentation of the pre/post-conditions of various operations. It also encourages creating a clean and friendly Python API. Testing through the Python API does not cover all of the external shell interface, but it is much easier to write, because we don't need to deal with serializing everything to/from text. We may need additional tests to this. .. _doctest: http://docs.python.org/lib/module-doctest.html M 644 inline doc/thanks.txt data 313 ****** Thanks ****** Inspiration, suggestions, advice -------------------------------- * Mark Shuttleworth * Robert Collins * David Allouche * Robert Weir * James Blackwell * Garrett Rooney * Andrew Tridgell * Paul Russell * Aaron Bentley Sponsor ------- * Canonical_ .. _Canonical: http://www.canonical.com/ M 644 inline doc/todo-from-arch.txt data 13588 ***************************************** Opportunities for improvement on GNU Arch ***************************************** Bazaar-NG is based on the GNU Arch system, and inherits a lot of its design from Arch. However, there are several things we will change in Baz to (we hope) improve the user experience. The core design of Arch is good, brilliant even. It can scale from small projects too large ones, and is a good foundation for building tools on top. However, the design is far too complex, both in concepts and execution. So the plan is to cut out as many things as we can, add a few other good concepts from other systems, and try to make it into a whole that is consistent and understandable. Good bits to keep ----------------- * Roll-up changesets No other system is able to express this valuable idea: "I merged all these changes from other people; here is the result." However, it should *also* be possible to bring in perfect-fit patches without creating a new commit. * Star-merge Find a common ancestor on diverged and cross-merged branches. * Apply isolated changesets. We should extend this by having a good way to send changesets by email, preferably readable even by people who are not using Arch. * GPG signing of commits. Open source hackers almost all have GPG keys already, and GPG deals with a lot of PKI functions to do with propagating, signing and revoking keys. Signed commits are interesting in many ways, not least of which in detecting intrusion to code servers. * Anonymous downloads can be done without an active server. Good for security; also very good for people who do not have a permnanently-connected machine on which they can install their own software, or which is very tightly secured. It's neat that you can upload over only sftp/ftp, but I'm not sure it's really worth the hassle; getting properly atomic operations over remote-file protocols is hard. * Clean and transparent storage format. This is a neat hack, and gives people assurance that they can get their data back out again even if the tool disappears. Very nice. (Bazaar-NG won't keep the exact same format, but the ideas will be similar.) * Relatively easily parseable/scriptable shell interface. Good for people writing web/emacs/editor/IDE interfaces, or scripts based it. * Automatically build (and hardlink) revision libraries, with consistency checks. I don't know how many people want *every* revision in a library, but it can be handy to have a few key ones. In general making use of hardlinks when they are available and safe is nice. * Rely on ssh for remote access, authentication, and confidentiality. * Patch headers separate from patch bodies. (Sometimes you only want one.) * Autogeneration of Changelogs -- but should be in GNU format, at least optionally. I'm not convinced auto-updating them in the tree is worthwhile; it makes merges wierd. * Sealing branches. It seems useful to prevent accidental commits to things that are meant to be stable. However, the set-once nature of sealing is undesirable, because people can make mistakes or want to seal more than once. One possibility is to have a voluntary write-protect flag set on branches that should not normally be updated. One can remove the flag if it turns out it was set wrongly. * ``resolved`` command in Bazaar-1.1 Good for preventing accidental breakage. * Multi-level undo -- though could perhaps be more understandable, perhaps through ``undo-history``. Bits to cut out --------------- One lesson from usability design is that it does not always work to have a complex model and then try to hide complexity in the user interface. If you want something to be a joy to use, that must be designed in from the bottom up. (Some developers may react to tla by thinking "eww, how gross" on particular points. As much as possible we might like to fix these.) * General impression that the tool is telling you how to run your life. * Non-standard terminology Arch uses terms like "version" and "category" in ways that are confusing to people accustomed to other version control systems. This is not helpful. Therefore: development proceeds on a *branch*, which is a series of *revisions*. Simple and obvious. * Too many commands. * Command-line options are wierdly inconsistent with both other systems, with each others, and with what people would like to do. For example, I would think the obvious usage is ``bzr diff [FILE]``, but ``tla diff`` does not let you specify a file at all. Most commands should take filenames as their argument: log, diff, add, commit, etc. * Despite having too many commands, there are massive and glaring gaps, such reverting a single file or a tree. * Commands are too different from what people are used to in CVS, and often not for a good reason. * Identifiers are too long. In part this is because Arch tries to have identifiers which are both human-assigned and universally unique. * Archive names are probably unnecessary. * Part of the reason for complexity in archives is that the Arch design wants to be able to go and find patches on other branches at a later time. (This is not really implemented or used at the moment.) I think the complexity is unjustified: changesets and revisions have universally unique names so they can simply be archived, either on the machine of the person who wants them or on a central site like supermirror. * The tool is *unforgiving*; if people create a branch with the wrong name it will be around forever. * Branches are heaviweight; a record always persists in the archive. Sometimes it is good to create micro-branches, try something out, and then discard them. If nobody wants the changes, there is no reason for the tool to keep them. * Working offline requires creating a new branch and merging back and forth. This is both more work than it should be, and also polutes the "story" told by branching. As much as possible, the *accidental* difference of the location of the repository should not effect the *semantics* of branches. (However, some merging may obviously be necessary when there is divergence.) * Archive registration. This causes confusion and is unnecessary. Proposed solutions such as archive aliases or an additional command to register-and-get make it worse. * Wierd file names (``++`` and ``,,``, which persist in user directories and cause breakage of many tools. Gives a bad impression, and it's even worse when people have to interact with them. * Overly-long identifiers. (One advantage of pointing to branches using filenames or URLs is that the length of the path depends on how close it is to the users location, and they can more easily use * Too slow by default. Arch can be made fast, but in the hands of a nonexpert user it is often slow. For most users, disk is cheaper than CPU time, which is cheaper than network roundtrips. The performance model should be transparent -- users should not be surprised that something is slow. * Tagging onto branches. Unifying tags and commits is interesting, but the result is hard to mentally model; even Arch maintainers can't say exactly how it is supposed to work in some cases. * Reinventing the world from scratch in libhackerlab/frob/pika/xl. Those are all fine projects and may be useful in the future, but they are totally unnecessary to write a great version control system. It is not an enormous project; it is not CPU-cycle critical; something like Python will be fine. * Lack (for the moment) of an active server. Given that network traffic is the most expensive thing, we can possibly get a better solution by having intelligence on both sides of the link. Suppose we want to get just one file from a previous revision... * Poor Windows/Mac support. Even though many developers only work on Linux, this still holds a tool back. The reason is this: at least some projects have some developers on Windows some of the time. Those projects can't switch to Arch. Most people want to only learn one tool deeply, so it won't be Arch. Don't make any overly Unixy assumptions. Avoid too-cute filesystem dependencies. Being in Python should help with portability: people do need to install it, but many developers will already have it and the total burden is possibly less than that of installing C requisite libraries. * Quirky filename support. Files with non-ascii names, or names containing whitespace tend to be handled poorly, perhaps partly because of arch's shell heritage. By swallowing XML we do at least get automatic quoting of wierd strings, and we will always use UTF-8 for internal storage. * Complex file-id-tagging Nobody should be expected to understand this. There are two basic cases: people want to auto-add everything, and want to add by hand. Both can be reasonably accomodated in a simpler system. * Complex naming-convention regexps in ``.arch-inventory`` and ``{arch}/id-tagging-method``. (The fact that there are two overlapping mechanisms with very different names is also bad.) All this complexity basically just comes down to versioned, ignored, unknown, the same as in every other system. So we might as well just have that. There are relatively few cases where regexps help more than globs, and people do find them more complex. Even experienced users can forget to escape ``\.``. We can have a bit of flexibility with (say) zsh-style extended globs like ``*.(pyo|pyc)``. * Some files inside ``{arch}`` are meant to be edited by the user, and some are not. This is a flaw common to other systems, including Bitkeeper. The user should be clear on whether they should touch things in a directory or not. * Source-librarian function works poorly. It is not the place of a tool to force people to stay organized; it should just facilitate it. In any case, a library without descriptive text is of little use. So bazaar-ng does not force three-level naming but rather lets people arrange their own trees, and put on their own descriptions (either within the tree, or by e.g. having a wiki page listing branches, descriptions and URLs.) * Whining about inode mismatches on pristines/revlibs. It's fine that there is validation, but the tool should not show off its limitations. Just do the right thing. * More generally, not quite enough consistency/safety checking. * Unclear what commands work on subdirs and what works on the whole tree. * Hard to share work on a single branch -- though still not really too bad. * Lack of partial commits of added/deleted files. * Separate id tags for each file; simple implementation but probably costs too much disk space. * Way too many deeply-nested directories; should be just one. * ``.listing`` files are ugly and a point of failure. They can cause trouble on some servers which limit access to dot files. Isn't it possible to have the top-level file be predictable and find everything else needed from there? * Summary separate from log message. Simpler to just have one message, and let people extract the first line/sentence if they wish. Rather than 'keywords', let arbitrary properties be attached to the revision at the time of commit. Simpler disconnected operation ------------------------------ A basic distributed VCS operation is to make it easy to work on an offline laptop. Arch can do this in a few ways, but none of them are really simple. http://wiki.gnuarch.org/moin.cgi/mini_5fTravellingOftenWithArch Yaron Minsky writes (2005-01-18): I was wondering what people considered to be a good setup for using Arch on a laptop. Here's the basic situation. I have a few projects that reside in arch repositories on my desktop computer. Basically, I'd like to be able to do commits from my laptop, and have those commits eventually migrate up to the main repository. I understand that the right way of doing this is to set up archives on the laptop. But what's the cleanest way of doing this? And is there some way of making the commits I do on the laptop show up cleanly and individually on the desktop once they are merged in? Tagging-method -------------- baz default is much less strict. Much of tla depends on being able to categorize files. Some hangovers from larch -- eg precious and backup are essentially the same. junk is never deleted today. Automatic version control with 'untagged-source source'. But this is deprecated for baz? Annoyed by - defaults - having the feature at all - complex way to define it Default of 166 lines. Remove id-tagging-method command or at most make it read-only. If people really want to use deprecated methods they can just edit the file. So we can ship a default id-tagging which works the same as CVS/Svn: give warnings for files that are not known to be junk. This is the default in baz right now. Also we have .arch-inventory, which is per-directory. Why not have 'baz ignore FILENAME'? To remove ignores, perhaps you have to edit the .arch-inventory. Print "FILTER added to PATH/.arch-inventory"; create and baz-add this file if it doesn't. Docs should perhaps emphasize .arch-inventory as the basic method and only mention =tagging-method as an advanced topic. Should this really be regexps, or just file globs? M 644 inline doc/unchanged.txt data 3920 Detecting unchanged files ************************* Many operations need to know which working files have changed compared to the basis revision. (We also sometimes want to know which files have changed between two revisions, but since we know the text-ids and hashes that is much easier.) The simplest way is to just directly compare the files. This is simple and reliable, but has the disadvantage that we need to read in both files. For a large tree like the kernel or even samba, this can use a lot of cache memory and/or be slow. Some people have machines that do not have enough memory to hold even one copy of the tree at a time, and this would use two copies. So it is nice to know which files have not changed without actually reading them. Possibilities: * Make the working files be hardlinks to the file store. Easy to see if they are still the same file or not by simply stat'ing them. For extra protection, make the stored files readonly. Has the additional advantage of reducing the disk usage of the working copy. Disadvantage is that some people have editors that do not handle this safely. In that case the changes will go undetected, and they could corrupt history. Pretty bad. We can provide a ``bzr edit`` command that breaks the link and makes the working copy writable. * As above, but link to a temporary pristine directory, not to the real store. They can get a wrong answer, but at least cannot corrupt the store. * Check the mtime and size of the file; compare them to those of the previous stored version. The mtime doesn't need to be the time the previous revision was committed. There is a possibility of a race here where the file is modified but does not change size, all in the second after the checkout. Many filesystems don't report sub-second modification times, but Linux now allows for it and it may be supported in future. * Read in all the working files, but first compare them to the size and text-hash of the previous file revision; only do the diff if they have actually changed. Means only reading one tree, not two, but we still have to scan all the source. * Copy the file, but use an explicit edit command to remember which ones have been changed. Uneditable files should be readonly to prevent inadvertent changes. The problem with almost all of these models is that it is possible for people to change a file without tripping them. The only way to make this perfectly safe is to actually compare. So perhaps there should be a paranoia option. It is crucial that no failure can lose history. Does that mean hardlinks directly into the file store are just too risky? It is most important that ``diff``, ``status`` and similar things be fast, because they are invoked often. It may be that the ``commit`` command can tolerate being somewhat slower -- but then it would be confusing if ``commit`` saw something different to what ``diff`` does, so they should be the same. For the mooted `edit command`__, we will know whether a file is checked out as r/o or r/w; if a file is known to be read-only it can be assumed to be unmodified. __ optional-edit.html The ``check`` command can make sure that any files that would be assumed to be unmodified are actually unmodified. File times can skew, particularly on network filesystems. We should not make any assumptions that mtimes are the same as the system clock at the time of creation (and that would probably be racy anyhow). Proposal -------- :: if file is not editable: unmodified if not paranoid: if same inum and device as basis: unmodified elif present as read-only: unmodified elif same mtime and size: unmodified read working file, calculate sha1 if same size and sha-1 as previous inventory-entry: unmodified possibly-modified M 644 inline doc/unrelated-merge.txt data 310 **************************** Merging unrelated changesets **************************** It ought to be possible to merge in even totally unrelated changesets, by just treating them as patches. It is possible that the file ids will mismatch, in which case we could perhaps assume the filenames are compatible. M 644 inline doc/usability.txt data 104 * http://www.asktog.com/basics/firstPrinciples.html * http://wiki.gnuarch.org/moin.cgi/BazaarMockupUI M 644 inline doc/use-cases.txt data 2679 Bazaar-NG Use Cases ******************* .. contents:: Review changes ============== Look at somebody else's tree or email submissions. Looking at the patch alone may not be enough; we might need to apply it to a tree, build it and see if we like it. Changes on branches =================== Clearcase allows you to put all new development onto branches that are later merged back. Can we detect which development branches have unmerged changes? Can we dispose of those branches? unmerge ======= Get rid of any changes that have been merged in but not yet committed. Shouldn't this just be ``bzr revert``? cross damage problem with PQM ============================= Lock undo some uncommitted changes ============================= If you've made some changes and don't want them:: baz undo foo.c This stores them as a changeset in a directory that you can move around. You can set a name for it:: baz undo --name blargh-refactor foo.c bar.c You can get it back:: baz redo foo.c move some in-progress changes onto a local branch ================================================= This is useful if we decide some changes on a bound branch should be done on a separate branch; in particular people will want to do this if they want to work in only one subdirectory of a complex config. Possibly this should be the default with no arguments for ``bzr branch``. Or possibly there should be a separate ``bzr unbind``. ignore some files ================= I'm working on a Python project, which leaves bytecode files in the working directory:: baz ignore \*.pyc baz ignore \*.pyo or:: baz ignore '*.py[co]' OK, there is some danger here that people always forget to quote globs on the command line but maybe this will be enough. Maybe take only one at a time so that we can catch unquoted globs like this:: baz ignore *.pyc # wrong! If they do this, they see all '* added foo.pyc to .arch-inventory'; then they can do this to get back:: baz undo .arch-inventory This is potentially much more pleasant than Subversion. Wrong commit message ==================== I accidentally commit some files with the wrong message and want to change them:: % bzr status M foo.c % bzr commit -s 'fix foo' M foo.c [oops!] % bzr uncommit % bzr status M foo.c % bzr commit -s 'fix foo and bar' This fix should be done as soon as possible, before anything else depends on the change. Monday morning ============== Come in Monday morning; can't remember what you were doing. * log; look at what was committed * diff against upstream, or recent revisions * what else? M 644 inline doc/web-interface.txt data 1024 ************* Web interface ************* The web interface may be the first way many people interact with a new VC system. It may not be core functionality but it is important. mpe says he likes the bkbits.net one, in particular that you can quickly search the comments and pull out individual patches. However, it does not really give people a good sense of how the tool works. The `Aegis web interface`__ is rather better, giving a good sense (for better or worse) of the ideas behind the tool, as well as the particular tree being viewed. For example you can see that all changes in progress are registered, that one can identify conflicts before a merge is attempted and so on. In part this is because there are many links ("Information available") and they are explained in some detail: "This item will provide you with a list of changes to this project, including those which have not yet commenced, those in progress, and those which have been completed." __ http://aegis.sourceforge.net/webiface.html M 644 inline doc/work-order.txt data 1778 * * Build a past revision from history: - Working; now rebuild only up to a particular point. - A generic test that can be applied to any test working directory that all previous revisions can be successfully recreated. * Ignore-file support: finalize design and then implement it. * Command-line options: should be passed down as arguments to ``cmd`` functions. - ``commit`` message should be an option, not an argument. * Class representing a tree (or working directory or branch) - Should this really be a class or is it better to just have a set of global functions? What data is held in memory other than the directory name? - List status and inventory sorted by file name. - Add/move/delete files. * Store patches in a zip file? * When building diffs, show only the relevant tail of the two filenames in the index line. * Store patch headers in XML? What parser/generator to use? Maybe tupletree-format, or Zope xmlpickle, or ElementTree_? .. _ElementTree: http://effbot.org/zone/element-index.htm * Make immutable files (e.g. recorded patches) read-only after they're written. This would be a good idea, except that it means people need to use ``rm -rf`` to remove branches, which may be dangerous. Guiding principles ------------------ * In the first instance, just cache everything; don't worry about recreating them from simpler components. So keep a copy around of all previous revisions, their inventory and manifest, etc. * Start writing a test framework into which new development can fit. * No need to handle move/rename or subdirectories yet. * Do try to add assertions against invalid states. * Keep it simple in interface and implementation. .. Local variables: .. mode: rst .. End: M 644 inline doc/workflow.txt data 5225 ******** Workflow ******** People want to manage submissions of patches; see `Havoc's post`__. __ http://log.ometer.com/2004-11.html#19 The ideal interface is:: baz branch http://project.org/bzr/project-2.0 [hackety hack] baz submit It would be nice if people could submit simple changes without needing to set up their own public branches. I don't think people will want to allow random strangers to create branches on their machine, so this probably means submitting the changes by email. To form such a patch we need to know what branch point counts as "upstream", and who to send the patches to. As a reasonable default, we might use the last time we branched and the last Some of this should be better done in integration with e.g. mail clients or external robots or bug trackers. Bazaar-NG allows redrafting rejected patches in an interesting way: Person writes a feature on a new feature branch. They can commit several times, merge up to date, even have sub-forks to an arbitrary extent. When they're ready, they submit their changes to the maintainer, either by mailing the diffs relative to the main branch, or by the maintainer pulling from their tree. If they don't like it, they can not commit it to their tree, and hopefully give some feedback to the contributor. (I don't think that feedback needs to go back through the tool; email or some other communication mechanism is probably fine.) The contributor can then keep working in their branch, until it eventually gets merged. Alternatively, the maintainer might want to merge the change but fix it up themselves. We keep track of the fact that it was merged, and the maintainer can make arbitrary fixups either in the course of merging it or afterwards. When the contributor later merges back everything will work. Another case is that the maintainer wants to improve the patch but not take it into their main tree. What they can do here is take it into a separate feature branch, fix it up, and then ask the contributor to merge back from there. Maintainers__ would also like to keep track of patches that have been submitted but not yet accepted, so they're not lost and can be updated. One way to do this would be to create a branch on the maintainer's machine for a submitted patch, and apply the submission to that. The maintainer can fix it if they want, or take updates to it. The submitter can see what, if anything, was done. Because this branch is identified by a URL it can be cited in bug reports, and it might make sense to name the branch by the bug it is supposed to fix. __ http://gcc.gnu.org/ml/gcc/2002-12/msg00444.html Aegis-style review and integration ---------------------------------- Some projects might want all changes to be submitted for review before merging onto the mainline. This might be done either by convention, or perhaps by not allowing individual developers to merge to the mainline but rather having specific privileged integrators. Aegis_ enforces a lot of workflow/process; it would be good to be able to do something similar on top of bazaar-ng either manually or as a higher-level tool. Aegis's model is that each proposed change is essentially on a branch that later merges into the mainline, which makes a lot of sense. .. _Aegis: compared-aegis.html To do something like Aegis, follow this process: * Developer makes a new branch from the trunk to develop a feature, called say ``project--devel--bug-123``. * When they have almost finished development, they re-merge from the trunk to make sure they're up to date. * By some mechanism they ask a reviewer to consider their changes -- perhaps by sending email, or using a bug tracking system, or something else. They tell the reviewer the location of their branch, which might be on an HTTP server for a public project, or on a directory on a shared server. * The reviewer makes a new branch for review based off the trunk, ``project--review--bug-123``, and merges the development branch into it. The merge should be perfect if the developer was up to date with the trunk. If the merge fails they can either bounce it back to the developer or fix the merge themselves according to local policy or their own discretion. They then review, build and test the branch. If it's OK, they commit to their review branch and send a note asking for it to be integrated (or perhaps they integrate themselves.) * The integrator merges from the review branch onto the trunk, builds/tests and commits. Since they pull from the reviewer's branch there is no way unreviewed changes can sneak through even if the developer adds to their work branch after the review. This can be done by a robot, or by a reviewer. * The developer can look at the review, integration and trunk branches to see that their changes have merged. This model is practiced by some people at Canonical using tla. Since people work within a complex configspec, they like very much to be able to branch in-place so that they do not need to rebuild the whole config to start new development. (Though perhaps the real fix is to make assembling a config simpler...) M 644 inline doc/yaml.txt data 1376 YAML **** I am thinking about keeping changeset headers in YAML_, as a nice tradeoff: * Standard data format, so we don't need to invent, write and debug an ad-hoc format. * More concise than XML, which is useful if we're storing a few. * Reasonably readable for humans; thus suitable for sending in email and giving people at least some chance to read and understand it. Seeing this at the top of a patch would not be actively horrible as long as we keep it reasonably small; that's not true for XML. * Since YAML documents have a document header and terminator ('---' and '...') we can pick them out of a text document and append text patches directly afterwards. * Fits well with Python internal data structures. * There are parsers for various languages, so people can write their own implementation if they really want. * There is space to specify a document format/version at the top (though this is not currently handled in ydump). The ydump module is pretty primitive, but it should have enough. .. _YAML: http://www.yaml.org/ Conclusion ---------- On the whole, I think this is not a good idea. It is a nicer syntax for XML, but there are less bindings, and it does not achieve the goal of having the storage format look reassuring and giving confidence that the data could be retrieved by an alternative implementation. commit refs/heads/master mark :7 committer 1110349177 +0000 data 47 depend only on regular ElementTree installation from :6 M 644 inline bzr.py data 20715 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. """ # not currently working: # bzr check # Run internal consistency checks. # bzr info # Show some information about this branch. __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ###################################################################### # check status def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_inventory(inventory_id): """Return inventory in XML by hash""" Branch('.').get_inventory(inventory_hash).write_xml(sys.stdout) def cmd_get_revision_inventory(revision_id): """Output inventory for a revision.""" b = Branch('.') b.get_revision_inventory(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files. Fails if the files are already added. """ Branch('.').add(file_list, verbose=verbose) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): b = Branch('.') print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') print 'revision number:', b.revno() print 'number of versioned files:', len(b.read_working_inventory()) def cmd_remove(file_list, verbose=False): Branch('.').remove(file_list, verbose=verbose) def cmd_file_id(filename): i = Branch('.').read_working_inventory().path2id(filename) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_log(): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log() def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print uuid() def cmd_commit(message, verbose=False): Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_doctest(): """Run internal doctest suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option import bzr, doctest, bzrlib.store bzrlib.trace.verbose = False doctest.testmod(bzr) doctest.testmod(bzrlib.store) doctest.testmod(bzrlib.inventory) doctest.testmod(bzrlib.branch) doctest.testmod(bzrlib.osutils) doctest.testmod(bzrlib.tree) # more strenuous tests; import bzrlib.tests doctest.testmod(bzrlib.tests) ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'ls': ['revision', 'verbose'], 'status': ['all'], 'log': ['show-ids'], 'remove': ['verbose'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('bzr --help'.split()) ([], {'help': True}) >>> parse_args('bzr --version'.split()) ([], {'version': True}) >>> parse_args('bzr status --all'.split()) (['status'], {'all': True}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? it = iter(argv[1:]) while it: a = it.next() if a[0] == '-': if a[1] == '-': mutter(" got option %r" % a) optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if not it: bailout('option %r needs an argument' % a) opts[optname] = optargfn(it.next()) mutter(" option argument %r" % opts[optname]) else: # takes no option argument opts[optname] = True elif a[:1] == '-': bailout('unknown short option %r' % a) else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. for ap in argform: argname = ap[:-1] if ap[-1] == '?': assert 0 elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. try: t = bzrlib.trace._tracefile t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % format_date(time.time())) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.gethostname())) t.write(' arguments: %r\n' % argv) starttime = os.times()[4] import platform t.write(' platform: %s\n' % platform.platform()) t.write(' python: %s\n' % platform.python_version()) ret = run_bzr(argv) times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum" % times[:4]) mutter(" %.3f elapsed" % (times[4] - starttime)) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') M 644 inline bzrlib/inventory.py data 14929 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Inventories map files to their name in a revision.""" __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os.path, types from sets import Set try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree import Element, ElementTree, SubElement from xml import XMLMixin from errors import bailout from osutils import uuid, quotefn, splitpath, joinpath, appendpath from trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') >>> i.add(InventoryEntry('123', 'src', kind='directory')) >>> i.add(InventoryEntry('2323', 'hello.c', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id=None)) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', parent_id='123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', parent_id='123')) >>> i.add(InventoryEntry('2325', 'wibble', parent_id='123', kind='directory')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', parent_id='2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' :todo: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ def __init__(self, file_id, name, kind='file', text_id=None, parent_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c') >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c') Traceback (most recent call last): BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", []) """ if len(splitpath(name)) != 1: bailout('InventoryEntry name is not a simple filename: %r' % name) self.file_id = file_id self.name = name assert kind in ['file', 'directory'] self.kind = kind self.text_id = text_id self.parent_id = parent_id self.text_sha1 = None self.text_size = None def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.text_id, self.parent_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size is not None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1', 'parent_id']: v = getattr(self, f) if v is not None: e.set(f, v) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind')) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') self.parent_id = elt.get('parent_id') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __cmp__(self, other): if self is other: return 0 if not isinstance(other, InventoryEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.name, other.name) \ or cmp(self.text_sha1, other.text_sha1) \ or cmp(self.text_size, other.text_size) \ or cmp(self.text_id, other.text_id) \ or cmp(self.parent_id, other.parent_id) \ or cmp(self.kind, other.kind) class Inventory(XMLMixin): """Inventory of versioned files in a tree. An Inventory acts like a set of InventoryEntry items. You can also look files up by their file_id or name. May be read from and written to a metadata file in a tree. To manipulate the inventory (for example to add a file), it is read in, modified, and then written back out. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c')) >>> inv['123-123'].name 'hello.c' >>> for file_id in inv: print file_id ... 123-123 May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ ## TODO: Clear up handling of files in subdirectories; we probably ## do want to be able to just look them up by name but this ## probably means gradually walking down the path, looking up as we go. ## TODO: Make sure only canonical filenames are stored. ## TODO: Do something sensible about the possible collisions on ## case-losing filesystems. Perhaps we should just always forbid ## such collisions. ## _tree should probably just be stored as ## InventoryEntry._children on each directory. def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. """ self._byid = dict() # _tree is indexed by parent_id; at each level a map from name # to ie. The None entry is the root. self._tree = {None: {}} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, parent_id=None): """Return (path, entry) pairs, in order by name.""" kids = self._tree[parent_id].items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(parent_id=ie.file_id): yield joinpath([name, cn]), cie def directories(self, include_root=True): """Return (path, entry) pairs for all directories. """ if include_root: yield '', None for path, entry in self.iter_entries(): if entry.kind == 'directory': yield path, entry def children(self, parent_id): """Return entries that are direct children of parent_id.""" return self._tree[parent_id] # TODO: return all paths and entries def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c')) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c')) >>> inv['123123'].name 'hello.c' """ return self._byid[file_id] def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self: bailout("inventory already contains entry with id {%s}" % entry.file_id) if entry.parent_id != None: if entry.parent_id not in self: bailout("parent_id %s of new entry not found in inventory" % entry.parent_id) if self._tree[entry.parent_id].has_key(entry.name): bailout("%s is already versioned" % appendpath(self.id2path(entry.parent_id), entry.name)) self._byid[entry.file_id] = entry self._tree[entry.parent_id][entry.name] = entry if entry.kind == 'directory': self._tree[entry.file_id] = {} def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c')) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self._tree[ie.parent_id][ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in self._tree[file_id].values(): del self[cie.file_id] del self._tree[file_id] del self._byid[file_id] del self._tree[ie.parent_id][ie.name] def id_set(self): return Set(self._byid) def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c')) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __cmp__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo')) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo')) >>> i1 == i2 True """ if self is other: return 0 if not isinstance(other, Inventory): return NotImplemented if self.id_set() ^ other.id_set(): return 1 for file_id in self._byid: c = cmp(self[file_id], other[file_id]) if c: return c return 0 def id2path(self, file_id): """Return as a list the path to file_id.""" p = [] while file_id != None: ie = self[file_id] p = [ie.name] + p file_id = ie.parent_id return joinpath(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. """ assert isinstance(name, types.StringTypes) parent_id = None for f in splitpath(name): try: cie = self._tree[parent_id][f] assert cie.name == f parent_id = cie.file_id except KeyError: # or raise an error? return None return parent_id def get_child(self, parent_id, child_name): return self._tree[parent_id].get(child_name) def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): assert isinstance(file_id, str) return self._byid.has_key(file_id) if __name__ == '__main__': import doctest, inventory doctest.testmod(inventory) M 644 inline bzrlib/revision.py data 2513 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from xml import XMLMixin try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree import Element, ElementTree, SubElement class Revision(XMLMixin): """Single revision on a branch. Revisions may know their revision_hash, but only once they've been written out. This is not stored because you cannot write the hash into the file it describes. :todo: Perhaps make predecessor be a child element, not an attribute? """ def __init__(self, **args): self.inventory_id = None self.revision_id = None self.timestamp = None self.message = None self.__dict__.update(args) def __repr__(self): if self.revision_id: return "" % self.revision_id def to_element(self): root = Element('changeset', committer = self.committer, timestamp = '%f' % self.timestamp, revision_id = self.revision_id, inventory_id = self.inventory_id) if self.precursor: root.set('precursor', self.precursor) root.text = '\n' msg = SubElement(root, 'message') msg.text = self.message msg.tail = '\n' return root def from_element(cls, root): cs = cls(committer = root.get('committer'), timestamp = float(root.get('timestamp')), precursor = root.get('precursor'), revision_id = root.get('revision_id'), inventory_id = root.get('inventory_id')) cs.message = root.findtext('message') # text of return cs from_element = classmethod(from_element) M 644 inline bzrlib/xml.py data 1439 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """XML externalization support.""" __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree import Element, ElementTree, SubElement import os, time from trace import mutter class XMLMixin: def to_element(self): raise Exception("XMLMixin.to_element must be overridden in concrete classes") def write_xml(self, f): ElementTree(self.to_element()).write(f, 'utf-8') f.write('\n') def read_xml(cls, f): return cls.from_element(ElementTree().parse(f)) read_xml = classmethod(read_xml) commit refs/heads/master mark :8 committer 1110350693 +1100 data 61 store committer's timezone in revision and show in changelog from :7 M 644 inline bzr.py data 20792 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. """ # not currently working: # bzr check # Run internal consistency checks. # bzr info # Show some information about this branch. __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ###################################################################### # check status def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_inventory(inventory_id): """Return inventory in XML by hash""" Branch('.').get_inventory(inventory_hash).write_xml(sys.stdout) def cmd_get_revision_inventory(revision_id): """Output inventory for a revision.""" b = Branch('.') b.get_revision_inventory(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files. Fails if the files are already added. """ Branch('.').add(file_list, verbose=verbose) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): b = Branch('.') print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') print 'revision number:', b.revno() print 'number of versioned files:', len(b.read_working_inventory()) def cmd_remove(file_list, verbose=False): Branch('.').remove(file_list, verbose=verbose) def cmd_file_id(filename): i = Branch('.').read_working_inventory().path2id(filename) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_log(): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log() def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message, verbose=False): Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_doctest(): """Run internal doctest suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option import bzr, doctest, bzrlib.store bzrlib.trace.verbose = False doctest.testmod(bzr) doctest.testmod(bzrlib.store) doctest.testmod(bzrlib.inventory) doctest.testmod(bzrlib.branch) doctest.testmod(bzrlib.osutils) doctest.testmod(bzrlib.tree) # more strenuous tests; import bzrlib.tests doctest.testmod(bzrlib.tests) ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'ls': ['revision', 'verbose'], 'status': ['all'], 'log': ['show-ids'], 'remove': ['verbose'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('bzr --help'.split()) ([], {'help': True}) >>> parse_args('bzr --version'.split()) ([], {'version': True}) >>> parse_args('bzr status --all'.split()) (['status'], {'all': True}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? it = iter(argv[1:]) while it: a = it.next() if a[0] == '-': if a[1] == '-': mutter(" got option %r" % a) optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if not it: bailout('option %r needs an argument' % a) opts[optname] = optargfn(it.next()) mutter(" option argument %r" % opts[optname]) else: # takes no option argument opts[optname] = True elif a[:1] == '-': bailout('unknown short option %r' % a) else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. for ap in argform: argname = ap[:-1] if ap[-1] == '?': assert 0 elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. try: t = bzrlib.trace._tracefile t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % format_date(time.time())) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.gethostname())) t.write(' arguments: %r\n' % argv) starttime = os.times()[4] import platform t.write(' platform: %s\n' % platform.platform()) t.write(' python: %s\n' % platform.python_version()) ret = run_bzr(argv) times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum" % times[:4]) mutter(" %.3f elapsed" % (times[4] - starttime)) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') M 644 inline bzrlib/branch.py data 25661 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset from store import ImmutableStore from revision import Revision from errors import bailout from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. :todo: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. :todo: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. :todo: Keep the on-disk branch locked while the object exists. :todo: mkdir() method. """ def __init__(self, base, init=False): """Create new branch object at a particular location. :param base: Base directory for the branch. :param init: If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ self.base = os.path.realpath(base) if init: self._make_control() else: if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def _rel(self, name): """Return filename relative to branch top""" return os.path.join(self.base, name) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch""" return file(self.controlfilename(file_or_path), mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # read in binary mode to detect newline wierdness. fmt = self.controlfile('branch-format', 'rb').read() if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() inv = Inventory.read_xml(self.controlfile('inventory', 'r')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ inv.write_xml(self.controlfile('inventory', 'w')) mutter('wrote inventory to %s' % quotefn(self.controlfilename('inventory'))) inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. This puts the files in the Added state, so that they will be recorded by the next commit. :todo: Perhaps have an option to add the ids even if the files do not (yet) exist. :todo: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. :todo: Option to specify file id. :todo: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self._rel(f)) if isfile(fullpath): kind = 'file' elif isdir(fullpath): kind = 'directory' else: bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if len(fp) > 1: parent_name = joinpath(fp[:-1]) mutter("lookup parent %r" % parent_name) parent_id = inv.path2id(parent_name) if parent_id == None: bailout("cannot add: parent %r is not versioned" % joinpath(fp[:-1])) else: parent_id = None file_id = _gen_file_id(fp[-1]) inv.add(InventoryEntry(file_id, fp[-1], kind=kind, parent_id=parent_id)) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r parent_id={%s}" % (f, file_id, kind, parent_id)) self._write_inventory(inv) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on :todo: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True :todo: Do something useful with directories. :todo: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: show_status('D', inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. :param timestamp: if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self._rel(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = _gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'R' else: state = 'M' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) mutter("append to revision-history") self.controlfile('revision-history', 'at').write(rev_id + '\n') mutter("done!") def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. :todo: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: bailout("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, utc=False): """Write out human-readable log of commits to this branch :param utc: If true, show dates in universal time, not local time.""" revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l revno += 1 precursor = p def show_status(branch, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() :todo: Get state for single files. :todo: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = branch.basis_tree() old_inv = old.inventory new = branch.working_tree() new_inv = new.inventory for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("wierd file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files = []): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def _gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" assert '/' not in name while name[0] == '.': name = name[1:] s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/osutils.py data 6470 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, types, re, time, types from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE from errors import bailout def make_readonly(filename): """Make a filename read-only.""" # TODO: probably needs to be fixed for windows mod = os.stat(filename).st_mode mod = mod & 0777555 os.chmod(filename, mod) def make_writable(filename): mod = os.stat(filename).st_mode mod = mod | 0200 os.chmod(filename, mod) _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])') def quotefn(f): """Return shell-quoted filename""" ## We could be a bit more terse by using double-quotes etc f = _QUOTE_RE.sub(r'\\\1', f) if f[0] == '~': f[0:1] = r'\~' return f def file_kind(f): mode = os.lstat(f)[ST_MODE] if S_ISREG(mode): return 'file' elif S_ISDIR(mode): return 'directory' else: bailout("can't handle file kind of %r" % fp) def isdir(f): """True if f is an accessible directory.""" try: return S_ISDIR(os.lstat(f)[ST_MODE]) except OSError: return False def isfile(f): """True if f is a regular file.""" try: return S_ISREG(os.lstat(f)[ST_MODE]) except OSError: return False def pumpfile(fromfile, tofile): """Copy contents of one file to another.""" tofile.write(fromfile.read()) def uuid(): """Return a new UUID""" ## XXX: Could alternatively read /proc/sys/kernel/random/uuid on ## Linux, but we need something portable for other systems; ## preferably an implementation in Python. bailout('uuids not allowed!') return chomp(os.popen('uuidgen').readline()) def chomp(s): if s and (s[-1] == '\n'): return s[:-1] else: return s def sha_file(f): import sha ## TODO: Maybe read in chunks to handle big files if hasattr(f, 'tell'): assert f.tell() == 0 s = sha.new() s.update(f.read()) return s.hexdigest() def sha_string(f): import sha s = sha.new() s.update(f) return s.hexdigest() def username(): """Return email-style username. Something similar to 'Martin Pool ' :todo: Check it's reasonably well-formed. :todo: Allow taking it from a dotfile to help people on windows who can't easily set variables. :todo: Cope without pwd module, which is only on unix. """ e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: return e import socket try: import pwd uid = os.getuid() w = pwd.getpwuid(uid) realname, junk = w.pw_gecos.split(',', 1) return '%s <%s@%s>' % (realname, w.pw_name, socket.getfqdn()) except ImportError: pass import getpass, socket return '<%s@%s>' % (getpass.getuser(), socket.getfqdn()) def user_email(): """Return just the email component of a username.""" e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: import re m = re.search(r'[\w+.-]+@[\w+.-]+', e) if not m: bailout('%r is not a reasonable email address' % e) return m.group(0) import getpass, socket return '%s@%s' % (getpass.getuser(), socket.getfqdn()) def compare_files(a, b): """Returns true if equal in contents""" # TODO: don't read the whole thing in one go. result = a.read() == b.read() return result def local_time_offset(): if time.daylight: return -time.altzone else: return -time.timezone def format_date(t, offset=0, timezone='original'): ## TODO: Perhaps a global option to use either universal or local time? ## Or perhaps just let people set $TZ? import time assert isinstance(t, float) if timezone == 'utc': tt = time.gmtime(t) offset = 0 elif timezone == 'original': tt = time.gmtime(t - offset) else: assert timezone == 'local' tt = time.localtime(t) offset = local_time_offset() return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt) + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)) def compact_date(when): return time.strftime('%Y%m%d%H%M%S', time.gmtime(when)) def filesize(f): """Return size of given open file.""" return os.fstat(f.fileno())[ST_SIZE] if hasattr(os, 'urandom'): # python 2.4 and later rand_bytes = os.urandom else: # FIXME: No good on non-Linux _rand_file = file('/dev/urandom', 'rb') rand_bytes = _rand_file.read ## TODO: We could later have path objects that remember their list ## decomposition (might be too tricksy though.) def splitpath(p): """Turn string into list of parts. >>> splitpath('a') ['a'] >>> splitpath('a/b') ['a', 'b'] >>> splitpath('a/./b') ['a', 'b'] >>> splitpath('a/.b') ['a', '.b'] >>> splitpath('a/../b') Traceback (most recent call last): ... BzrError: ("sorry, '..' not allowed in path", []) """ assert isinstance(p, types.StringTypes) ps = [f for f in p.split('/') if f != '.'] for f in ps: if f == '..': bailout("sorry, %r not allowed in path" % f) return ps def joinpath(p): assert isinstance(p, list) for f in p: if (f == '..') or (f is None) or (f == ''): bailout("sorry, %r not allowed in path" % f) return '/'.join(p) def appendpath(p1, p2): if p1 == '': return p2 else: return p1 + '/' + p2 def extern_command(cmd, ignore_errors = False): mutter('external command: %s' % `cmd`) if os.system(cmd): if not ignore_errors: bailout('command failed') M 644 inline bzrlib/revision.py data 2668 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from xml import XMLMixin try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree import Element, ElementTree, SubElement class Revision(XMLMixin): """Single revision on a branch. Revisions may know their revision_hash, but only once they've been written out. This is not stored because you cannot write the hash into the file it describes. :todo: Perhaps make predecessor be a child element, not an attribute? """ def __init__(self, **args): self.inventory_id = None self.revision_id = None self.timestamp = None self.message = None self.timezone = None self.__dict__.update(args) def __repr__(self): if self.revision_id: return "" % self.revision_id def to_element(self): root = Element('changeset', committer = self.committer, timestamp = '%.9f' % self.timestamp, revision_id = self.revision_id, inventory_id = self.inventory_id, timezone = str(self.timezone)) if self.precursor: root.set('precursor', self.precursor) root.text = '\n' msg = SubElement(root, 'message') msg.text = self.message msg.tail = '\n' return root def from_element(cls, root): cs = cls(committer = root.get('committer'), timestamp = float(root.get('timestamp')), precursor = root.get('precursor'), revision_id = root.get('revision_id'), inventory_id = root.get('inventory_id')) v = root.get('timezone') cs.timezone = v and int(v) cs.message = root.findtext('message') # text of return cs from_element = classmethod(from_element) commit refs/heads/master mark :9 committer 1110350900 +1100 data 3 doc from :8 M 644 inline bzrlib/branch.py data 25734 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset from store import ImmutableStore from revision import Revision from errors import bailout from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. :todo: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. :todo: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. :todo: Keep the on-disk branch locked while the object exists. :todo: mkdir() method. """ def __init__(self, base, init=False): """Create new branch object at a particular location. :param base: Base directory for the branch. :param init: If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ self.base = os.path.realpath(base) if init: self._make_control() else: if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def _rel(self, name): """Return filename relative to branch top""" return os.path.join(self.base, name) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch""" return file(self.controlfilename(file_or_path), mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # read in binary mode to detect newline wierdness. fmt = self.controlfile('branch-format', 'rb').read() if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() inv = Inventory.read_xml(self.controlfile('inventory', 'r')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ inv.write_xml(self.controlfile('inventory', 'w')) mutter('wrote inventory to %s' % quotefn(self.controlfilename('inventory'))) inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. This puts the files in the Added state, so that they will be recorded by the next commit. :todo: Perhaps have an option to add the ids even if the files do not (yet) exist. :todo: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. :todo: Option to specify file id. :todo: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self._rel(f)) if isfile(fullpath): kind = 'file' elif isdir(fullpath): kind = 'directory' else: bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if len(fp) > 1: parent_name = joinpath(fp[:-1]) mutter("lookup parent %r" % parent_name) parent_id = inv.path2id(parent_name) if parent_id == None: bailout("cannot add: parent %r is not versioned" % joinpath(fp[:-1])) else: parent_id = None file_id = _gen_file_id(fp[-1]) inv.add(InventoryEntry(file_id, fp[-1], kind=kind, parent_id=parent_id)) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r parent_id={%s}" % (f, file_id, kind, parent_id)) self._write_inventory(inv) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on :todo: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True :todo: Do something useful with directories. :todo: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: show_status('D', inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. :param timestamp: if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self._rel(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = _gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'R' else: state = 'M' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) mutter("append to revision-history") self.controlfile('revision-history', 'at').write(rev_id + '\n') mutter("done!") def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. :todo: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: bailout("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, utc=False): """Write out human-readable log of commits to this branch :param utc: If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l revno += 1 precursor = p def show_status(branch, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() :todo: Get state for single files. :todo: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = branch.basis_tree() old_inv = old.inventory new = branch.working_tree() new_inv = new.inventory for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("wierd file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files = []): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def _gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" assert '/' not in name while name[0] == '.': name = name[1:] s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :10 committer 1110350940 +1100 data 26 import more files from baz from :9 M 644 inline build-api data 75 PYTHONPATH=$PWD epydoc -o api/html --docformat restructuredtext bzr bzrlib M 644 inline doc/Makefile data 216 %.html: %.txt rest2html $^ > $@.tmp && mv $@.tmp $@ all: $(addsuffix .html,$(basename $(wildcard *.txt))) upload: all (echo 'cd www'; ls *html | sed -e 's/^/put /') | sftp -b /dev/fd/0 bazng@escudero.ubuntu.com M 644 inline doc/default.css data 3962 /* :Author: David Goodger :Contact: goodger@users.sourceforge.net :date: $Date: 2004/01/28 20:46:38 $ :version: $Revision: 1.36 $ :copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. */ .first { margin-top: 0 } .last { margin-bottom: 0 } a.toc-backref { text-decoration: none ; color: black } blockquote.epigraph { margin: 2em 5em ; } dd { margin-bottom: 0.5em } div.abstract { margin: 2em 5em } div.abstract p.topic-title { font-weight: bold ; text-align: center } div.attention, div.caution, div.danger, div.error, div.hint, div.important, div.note, div.tip, div.warning, div.admonition { margin: 2em ; border: medium outset ; padding: 1em } div.attention p.admonition-title, div.caution p.admonition-title, div.danger p.admonition-title, div.error p.admonition-title, div.warning p.admonition-title { color: red ; font-weight: bold ; font-family: sans-serif } div.hint p.admonition-title, div.important p.admonition-title, div.note p.admonition-title, div.tip p.admonition-title, div.admonition p.admonition-title { font-weight: bold ; font-family: sans-serif } div.dedication { margin: 2em 5em ; text-align: center ; font-style: italic } div.dedication p.topic-title { font-weight: bold ; font-style: normal } div.figure { margin-left: 2em } div.footer, div.header { font-size: smaller } div.sidebar { margin-left: 1em ; border: medium outset ; padding: 0em 1em ; background-color: #ffffee ; width: 40% ; float: right ; clear: right } div.sidebar p.rubric { font-family: sans-serif ; font-size: medium } div.system-messages { margin: 5em } div.system-messages h1 { color: red } div.system-message { border: medium outset ; padding: 1em } div.system-message p.system-message-title { color: red ; font-weight: bold } div.topic { margin: 2em } h1.title { text-align: center } h2.subtitle { text-align: center } hr { width: 75% } ol.simple, ul.simple { margin-bottom: 1em } ol.arabic { list-style: decimal } ol.loweralpha { list-style: lower-alpha } ol.upperalpha { list-style: upper-alpha } ol.lowerroman { list-style: lower-roman } ol.upperroman { list-style: upper-roman } p.attribution { text-align: right ; margin-left: 50% } p.caption { font-style: italic } p.credits { font-style: italic ; font-size: smaller } p.label { white-space: nowrap } p.rubric { font-weight: bold ; font-size: larger ; color: maroon ; text-align: center } p.sidebar-title { font-family: sans-serif ; font-weight: bold ; font-size: larger } p.sidebar-subtitle { font-family: sans-serif ; font-weight: bold } p.topic-title { font-weight: bold } pre.address { margin-bottom: 0 ; margin-top: 0 ; font-family: serif ; font-size: 100% } pre.line-block { font-family: serif ; font-size: 100% } pre.literal-block, pre.doctest-block { margin-left: 2em ; margin-right: 2em ; background-color: #eeeeee } span.classifier { font-family: sans-serif ; font-style: oblique } span.classifier-delimiter { font-family: sans-serif ; font-weight: bold } span.interpreted { font-family: sans-serif } span.option { white-space: nowrap } span.option-argument { font-style: italic } span.pre { white-space: pre } span.problematic { color: red } table { margin-top: 0.5em ; margin-bottom: 0.5em } table.citation { border-left: solid thin gray ; padding-left: 0.5ex } table.docinfo { margin: 2em 4em } table.footnote { border-left: solid thin black ; padding-left: 0.5ex } td, th { padding-left: 0.5em ; padding-right: 0.5em ; vertical-align: top } th.docinfo-name, th.field-name { font-weight: bold ; text-align: left ; white-space: nowrap } h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { font-size: 100% } tt { background-color: #eeeeee } ul.auto-toc { list-style-type: none } M 644 inline notes/performance.txt data 5240 For a tree holding 2.4.18 (two copies), 2.4.19, 2.4.20 With gzip -9: mbp@hope% du .bzr 195110 .bzr/text-store 20 .bzr/revision-store 12355 .bzr/inventory-store 216325 .bzr mbp@hope% du -s . 523128 . Without gzip: This is actually a pretty bad example because of deleting and re-importing 2.4.18, but still not totally unreasonable. ---- linux-2.4.0: 116399 kB after addding everything: 119505kB bzr status 2.68s user 0.13s system 84% cpu 3.330 total bzr commit 'import 2.4.0' 4.41s user 2.15s system 11% cpu 59.490 total 242446 . 122068 .bzr ---- Performance (2005-03-01) To add all files from linux-2.4.18: about 70s, mostly inventory serialization/deserialization. To commit: - finished, 6.520u/3.870s cpu, 33.940u/10.730s cum - 134.040 elapsed Interesting that it spends so long on external processing! I wonder if this is for running uuidgen? Let's try generating things internally. Great, this cuts it to 17.15s user 0.61s system 83% cpu 21.365 total to add, with no external command time. The commit now seems to spend most of its time copying to disk. - finished, 6.550u/3.320s cpu, 35.050u/9.870s cum - 89.650 elapsed I wonder where the external time is now? We were also using uuids() for revisions. Let's remove everything and re-add. Detecting everything was removed takes - finished, 2.460u/0.110s cpu, 0.000u/0.000s cum - 3.430 elapsed which may be mostly XML deserialization? Just getting the previous revision takes about this long: bzr invoked at Tue 2005-03-01 15:53:05.183741 EST +1100 by mbp@sourcefrog.net on hope arguments: ['/home/mbp/bin/bzr', 'get-revision-inventory', 'mbp@sourcefrog.net-20050301044608-8513202ab179aff4-44e8cd52a41aa705'] platform: Linux-2.6.10-4-686-i686-with-debian-3.1 - finished, 3.910u/0.390s cpu, 0.000u/0.000s cum - 6.690 elapsed Now committing the revision which removes all files should be fast. - finished, 1.280u/0.030s cpu, 0.000u/0.000s cum - 1.320 elapsed Now re-add with new code that doesn't call uuidgen: - finished, 1.990u/0.030s cpu, 0.000u/0.000s cum - 2.040 elapsed 16.61s user 0.55s system 74% cpu 22.965 total Status:: - finished, 2.500u/0.110s cpu, 0.010u/0.000s cum - 3.350 elapsed And commit:: Now patch up to 2.4.19. There were some bugs in handling missing directories, but with that fixed we do much better:: bzr status 5.86s user 1.06s system 10% cpu 1:05.55 total This is slow because it's diffing every file; we should use mtimes etc to make this faster. The cpu time is reasonable. I see difflib is pure Python; it might be faster to shell out to GNU diff when we need it. Export is very fast:: - finished, 4.220u/1.480s cpu, 0.010u/0.000s cum - 10.810 elapsed bzr export 1 ../linux-2.4.18.export1 3.92s user 1.72s system 21% cpu 26.030 total Now to find and add the new changes:: - finished, 2.190u/0.030s cpu, 0.000u/0.000s cum - 2.300 elapsed :: bzr commit 'import 2.4.19' 9.36s user 1.91s system 23% cpu 47.127 total And the result is exactly right. Try exporting:: mbp@hope% bzr export 4 ../linux-2.4.19.export4 bzr export 4 ../linux-2.4.19.export4 4.21s user 1.70s system 18% cpu 32.304 total and the export is exactly the same as the tarball. Now we can optimize the diff a bit more by not comparing files that have the right SHA-1 from within the commit For comparison:: patch -p1 < ../kernel.pkg/patch-2.4.20 1.61s user 1.03s system 13% cpu 19.106 total Now status after applying the .20 patch. With full-text verification:: bzr status 7.07s user 1.32s system 13% cpu 1:04.29 total with that turned off:: bzr status 5.86s user 0.56s system 25% cpu 25.577 total After adding: bzr status 6.14s user 0.61s system 25% cpu 26.583 total Should add some kind of profile counter for quick compares vs slow compares. bzr commit 'import 2.4.20' 7.57s user 1.36s system 20% cpu 43.568 total export: finished, 3.940u/1.820s cpu, 0.000u/0.000s cum, 50.990 elapsed also exports correctly now .21 bzr commit 'import 2.4.1' 5.59s user 0.51s system 60% cpu 10.122 total 265520 . 137704 .bzr import 2.4.2 317758 . 183463 .bzr with everything through to 2.4.29 imported, the .bzr directory is 1132MB, compared to 185MB for one tree. The .bzr.log is 100MB!. So the storage is 6.1 times larger, although we're holding 30 versions. It's pretty large but I think not ridiculous. By contrast the tarball for 2.4.0 is 104MB, and the tarball plus uncompressed patches are 315MB. Uncompressed, the text store is 1041MB. So it is only three times worse than patches, and could be compressed at presumably roughly equal efficiency. It is large, but also a very simple design and perhaps adequate for the moment. The text store with each file individually gziped is 264MB, which is also a very simple format and makes it less than twice the size of the source tree. This is actually rather pessimistic because I think there are some orphaned texts in there. Measured by du, the compressed full-text store is 363MB; also probably tolerable. The real fix is perhaps to use some kind of weave, not so much for storage efficiency as for fast annotation and therefore possible annotation-based merge. commit refs/heads/master mark :11 committer 1110351080 +1100 data 74 - change revision XML to - more assertions when reading back in from :10 M 644 inline bzrlib/revision.py data 2818 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from xml import XMLMixin try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree import Element, ElementTree, SubElement class Revision(XMLMixin): """Single revision on a branch. Revisions may know their revision_hash, but only once they've been written out. This is not stored because you cannot write the hash into the file it describes. :todo: Perhaps make predecessor be a child element, not an attribute? """ def __init__(self, **args): self.inventory_id = None self.revision_id = None self.timestamp = None self.message = None self.timezone = None self.__dict__.update(args) def __repr__(self): if self.revision_id: return "" % self.revision_id def to_element(self): root = Element('revision', committer = self.committer, timestamp = '%.9f' % self.timestamp, revision_id = self.revision_id, inventory_id = self.inventory_id, timezone = str(self.timezone)) if self.precursor: root.set('precursor', self.precursor) root.text = '\n' msg = SubElement(root, 'message') msg.text = self.message msg.tail = '\n' return root def from_element(cls, elt): # is deprecated... if elt.tag not in ('revision', 'changeset'): bailout("unexpected tag in revision file: %r" % elt) cs = cls(committer = elt.get('committer'), timestamp = float(elt.get('timestamp')), precursor = elt.get('precursor'), revision_id = elt.get('revision_id'), inventory_id = elt.get('inventory_id')) v = elt.get('timezone') cs.timezone = v and int(v) cs.message = elt.findtext('message') # text of return cs from_element = classmethod(from_element) commit refs/heads/master mark :12 committer 1110351648 +1100 data 33 new --timezone option for bzr log from :11 M 644 inline bzr.py data 20869 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. """ # not currently working: # bzr check # Run internal consistency checks. # bzr info # Show some information about this branch. __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ###################################################################### # check status def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_inventory(inventory_id): """Return inventory in XML by hash""" Branch('.').get_inventory(inventory_hash).write_xml(sys.stdout) def cmd_get_revision_inventory(revision_id): """Output inventory for a revision.""" b = Branch('.') b.get_revision_inventory(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files. Fails if the files are already added. """ Branch('.').add(file_list, verbose=verbose) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): b = Branch('.') print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') print 'revision number:', b.revno() print 'number of versioned files:', len(b.read_working_inventory()) def cmd_remove(file_list, verbose=False): Branch('.').remove(file_list, verbose=verbose) def cmd_file_id(filename): i = Branch('.').read_working_inventory().path2id(filename) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_log(timezone): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message, verbose=False): Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_doctest(): """Run internal doctest suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option import bzr, doctest, bzrlib.store bzrlib.trace.verbose = False doctest.testmod(bzr) doctest.testmod(bzrlib.store) doctest.testmod(bzrlib.inventory) doctest.testmod(bzrlib.branch) doctest.testmod(bzrlib.osutils) doctest.testmod(bzrlib.tree) # more strenuous tests; import bzrlib.tests doctest.testmod(bzrlib.tests) ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('bzr --help'.split()) ([], {'help': True}) >>> parse_args('bzr --version'.split()) ([], {'version': True}) >>> parse_args('bzr status --all'.split()) (['status'], {'all': True}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? it = iter(argv[1:]) while it: a = it.next() if a[0] == '-': if a[1] == '-': mutter(" got option %r" % a) optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if not it: bailout('option %r needs an argument' % a) opts[optname] = optargfn(it.next()) mutter(" option argument %r" % opts[optname]) else: # takes no option argument opts[optname] = True elif a[:1] == '-': bailout('unknown short option %r' % a) else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. for ap in argform: argname = ap[:-1] if ap[-1] == '?': assert 0 elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. try: t = bzrlib.trace._tracefile t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % format_date(time.time())) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.gethostname())) t.write(' arguments: %r\n' % argv) starttime = os.times()[4] import platform t.write(' platform: %s\n' % platform.platform()) t.write(' python: %s\n' % platform.python_version()) ret = run_bzr(argv) times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum" % times[:4]) mutter(" %.3f elapsed" % (times[4] - starttime)) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') M 644 inline bzrlib/branch.py data 25813 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset from store import ImmutableStore from revision import Revision from errors import bailout from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. :todo: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. :todo: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. :todo: Keep the on-disk branch locked while the object exists. :todo: mkdir() method. """ def __init__(self, base, init=False): """Create new branch object at a particular location. :param base: Base directory for the branch. :param init: If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ self.base = os.path.realpath(base) if init: self._make_control() else: if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def _rel(self, name): """Return filename relative to branch top""" return os.path.join(self.base, name) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch""" return file(self.controlfilename(file_or_path), mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # read in binary mode to detect newline wierdness. fmt = self.controlfile('branch-format', 'rb').read() if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() inv = Inventory.read_xml(self.controlfile('inventory', 'r')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ inv.write_xml(self.controlfile('inventory', 'w')) mutter('wrote inventory to %s' % quotefn(self.controlfilename('inventory'))) inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. This puts the files in the Added state, so that they will be recorded by the next commit. :todo: Perhaps have an option to add the ids even if the files do not (yet) exist. :todo: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. :todo: Option to specify file id. :todo: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self._rel(f)) if isfile(fullpath): kind = 'file' elif isdir(fullpath): kind = 'directory' else: bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if len(fp) > 1: parent_name = joinpath(fp[:-1]) mutter("lookup parent %r" % parent_name) parent_id = inv.path2id(parent_name) if parent_id == None: bailout("cannot add: parent %r is not versioned" % joinpath(fp[:-1])) else: parent_id = None file_id = _gen_file_id(fp[-1]) inv.add(InventoryEntry(file_id, fp[-1], kind=kind, parent_id=parent_id)) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r parent_id={%s}" % (f, file_id, kind, parent_id)) self._write_inventory(inv) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on :todo: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True :todo: Do something useful with directories. :todo: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: show_status('D', inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. :param timestamp: if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self._rel(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = _gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'R' else: state = 'M' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) mutter("append to revision-history") self.controlfile('revision-history', 'at').write(rev_id + '\n') mutter("done!") def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. :todo: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: bailout("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original'): """Write out human-readable log of commits to this branch :param utc: If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l revno += 1 precursor = p def show_status(branch, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() :todo: Get state for single files. :todo: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = branch.basis_tree() old_inv = old.inventory new = branch.working_tree() new_inv = new.inventory for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("wierd file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files = []): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def _gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" assert '/' not in name while name[0] == '.': name = name[1:] s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/osutils.py data 6575 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, types, re, time, types from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE from errors import bailout def make_readonly(filename): """Make a filename read-only.""" # TODO: probably needs to be fixed for windows mod = os.stat(filename).st_mode mod = mod & 0777555 os.chmod(filename, mod) def make_writable(filename): mod = os.stat(filename).st_mode mod = mod | 0200 os.chmod(filename, mod) _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])') def quotefn(f): """Return shell-quoted filename""" ## We could be a bit more terse by using double-quotes etc f = _QUOTE_RE.sub(r'\\\1', f) if f[0] == '~': f[0:1] = r'\~' return f def file_kind(f): mode = os.lstat(f)[ST_MODE] if S_ISREG(mode): return 'file' elif S_ISDIR(mode): return 'directory' else: bailout("can't handle file kind of %r" % fp) def isdir(f): """True if f is an accessible directory.""" try: return S_ISDIR(os.lstat(f)[ST_MODE]) except OSError: return False def isfile(f): """True if f is a regular file.""" try: return S_ISREG(os.lstat(f)[ST_MODE]) except OSError: return False def pumpfile(fromfile, tofile): """Copy contents of one file to another.""" tofile.write(fromfile.read()) def uuid(): """Return a new UUID""" ## XXX: Could alternatively read /proc/sys/kernel/random/uuid on ## Linux, but we need something portable for other systems; ## preferably an implementation in Python. bailout('uuids not allowed!') return chomp(os.popen('uuidgen').readline()) def chomp(s): if s and (s[-1] == '\n'): return s[:-1] else: return s def sha_file(f): import sha ## TODO: Maybe read in chunks to handle big files if hasattr(f, 'tell'): assert f.tell() == 0 s = sha.new() s.update(f.read()) return s.hexdigest() def sha_string(f): import sha s = sha.new() s.update(f) return s.hexdigest() def username(): """Return email-style username. Something similar to 'Martin Pool ' :todo: Check it's reasonably well-formed. :todo: Allow taking it from a dotfile to help people on windows who can't easily set variables. :todo: Cope without pwd module, which is only on unix. """ e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: return e import socket try: import pwd uid = os.getuid() w = pwd.getpwuid(uid) realname, junk = w.pw_gecos.split(',', 1) return '%s <%s@%s>' % (realname, w.pw_name, socket.getfqdn()) except ImportError: pass import getpass, socket return '<%s@%s>' % (getpass.getuser(), socket.getfqdn()) def user_email(): """Return just the email component of a username.""" e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: import re m = re.search(r'[\w+.-]+@[\w+.-]+', e) if not m: bailout('%r is not a reasonable email address' % e) return m.group(0) import getpass, socket return '%s@%s' % (getpass.getuser(), socket.getfqdn()) def compare_files(a, b): """Returns true if equal in contents""" # TODO: don't read the whole thing in one go. result = a.read() == b.read() return result def local_time_offset(): if time.daylight: return -time.altzone else: return -time.timezone def format_date(t, offset=0, timezone='original'): ## TODO: Perhaps a global option to use either universal or local time? ## Or perhaps just let people set $TZ? import time assert isinstance(t, float) if timezone == 'utc': tt = time.gmtime(t) offset = 0 elif timezone == 'original': tt = time.gmtime(t - offset) elif timezone == 'local': tt = time.localtime(t) offset = local_time_offset() else: bailout("unsupported timezone format %r", ['options are "utc", "original", "local"']) return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt) + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)) def compact_date(when): return time.strftime('%Y%m%d%H%M%S', time.gmtime(when)) def filesize(f): """Return size of given open file.""" return os.fstat(f.fileno())[ST_SIZE] if hasattr(os, 'urandom'): # python 2.4 and later rand_bytes = os.urandom else: # FIXME: No good on non-Linux _rand_file = file('/dev/urandom', 'rb') rand_bytes = _rand_file.read ## TODO: We could later have path objects that remember their list ## decomposition (might be too tricksy though.) def splitpath(p): """Turn string into list of parts. >>> splitpath('a') ['a'] >>> splitpath('a/b') ['a', 'b'] >>> splitpath('a/./b') ['a', 'b'] >>> splitpath('a/.b') ['a', '.b'] >>> splitpath('a/../b') Traceback (most recent call last): ... BzrError: ("sorry, '..' not allowed in path", []) """ assert isinstance(p, types.StringTypes) ps = [f for f in p.split('/') if f != '.'] for f in ps: if f == '..': bailout("sorry, %r not allowed in path" % f) return ps def joinpath(p): assert isinstance(p, list) for f in p: if (f == '..') or (f is None) or (f == ''): bailout("sorry, %r not allowed in path" % f) return '/'.join(p) def appendpath(p1, p2): if p1 == '': return p2 else: return p1 + '/' + p2 def extern_command(cmd, ignore_errors = False): mutter('external command: %s' % `cmd`) if os.system(cmd): if not ignore_errors: bailout('command failed') M 644 inline bzrlib/tests.py data 4727 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # XXX: We might prefer these to be in a text file rather than Python # source, but that only works in doctest from Python 2.4 and later, # which is not present in Warty. r""" Bazaar-NG test cases ******************** These are run by ``bzr.doctest``. >>> import bzr, bzrlib, os >>> bzr.cmd_rocks() it sure does! Hey, nice place to begin. The basic object is a Branch. We have a special helper class ScratchBranch that automatically makes a directory and cleans itself up, but is in other respects identical. ScratchBranches are initially empty: >>> b = bzr.ScratchBranch() >>> b.show_status() New files in that directory are, it is initially unknown: >>> file(b.base + '/hello.c', 'wt').write('int main() {}') >>> b.show_status() ? hello.c That's not quite true; some files (like editor backups) are ignored by default: >>> file(b.base + '/hello.c~', 'wt').write('int main() {}') >>> b.show_status() ? hello.c >>> list(b.unknowns()) ['hello.c'] The ``add`` command marks a file to be added in the next revision: >>> b.add('hello.c') >>> b.show_status() A hello.c You can also add files that otherwise would be ignored. The ignore patterns only apply to files that would be otherwise unknown, so they have no effect once it's added. >>> b.add('hello.c~') >>> b.show_status() A hello.c A hello.c~ It is an error to add a file that isn't present in the working copy: >>> b.add('nothere') Traceback (most recent call last): ... BzrError: ('cannot add: not a regular file or directory: nothere', []) If we add a file and then change our mind, we can either revert it or remove the file. If we revert, we are left with the working copy (in either I or ? state). If we remove, the working copy is gone. Let's do that to the backup, presumably added accidentally. >>> b.remove('hello.c~') >>> b.show_status() A hello.c Now to commit, creating a new revision. (Fake the date and name for reproducibility.) >>> b.commit('start hello world', timestamp=0, committer='foo@nowhere') >>> b.show_status() >>> b.show_status(show_all=True) . hello.c I hello.c~ We can look back at history >>> r = b.get_revision(b.lookup_revision(1)) >>> r.message 'start hello world' >>> b.write_log(show_timezone='utc') ---------------------------------------- revno: 1 committer: foo@nowhere timestamp: Thu 1970-01-01 00:00:00 +0000 message: start hello world (The other fields will be a bit unpredictable, depending on who ran this test and when.) As of 2005-02-21, we can also add subdirectories to the revision! >>> os.mkdir(b.base + "/lib") >>> b.show_status() ? lib/ >>> b.add('lib') >>> b.show_status() A lib/ >>> b.commit('add subdir') >>> b.show_status() >>> b.show_status(show_all=True) . hello.c I hello.c~ . lib/ and we can also add files within subdirectories: >>> file(b.base + '/lib/hello', 'w').write('hello!\n') >>> b.show_status() ? lib/hello Tests for adding subdirectories, etc. >>> b = bzrlib.branch.ScratchBranch() >>> os.mkdir(b._rel('d1')) >>> os.mkdir(b._rel('d2')) >>> os.mkdir(b._rel('d2/d3')) >>> list(b.working_tree().unknowns()) ['d1', 'd2'] Create some files, but they're not seen as unknown yet: >>> file(b._rel('d1/f1'), 'w').close() >>> file(b._rel('d2/f2'), 'w').close() >>> file(b._rel('d2/f3'), 'w').close() >>> [v[0] for v in b.inventory.directories()] [''] >>> list(b.working_tree().unknowns()) ['d1', 'd2'] Adding a directory, and we see the file underneath: >>> b.add('d1') >>> [v[0] for v in b.inventory.directories()] ['', 'd1'] >>> list(b.working_tree().unknowns()) ['d1/f1', 'd2'] >>> # d2 comes first because it's in the top directory >>> b.add('d2') >>> b.commit('add some stuff') >>> list(b.working_tree().unknowns()) ['d1/f1', 'd2/d3', 'd2/f2', 'd2/f3'] """ commit refs/heads/master mark :13 committer 1110351782 +1100 data 19 fix up cmd_log args from :12 M 644 inline bzr.py data 20880 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. """ # not currently working: # bzr check # Run internal consistency checks. # bzr info # Show some information about this branch. __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ###################################################################### # check status def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_inventory(inventory_id): """Return inventory in XML by hash""" Branch('.').get_inventory(inventory_hash).write_xml(sys.stdout) def cmd_get_revision_inventory(revision_id): """Output inventory for a revision.""" b = Branch('.') b.get_revision_inventory(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files. Fails if the files are already added. """ Branch('.').add(file_list, verbose=verbose) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): b = Branch('.') print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') print 'revision number:', b.revno() print 'number of versioned files:', len(b.read_working_inventory()) def cmd_remove(file_list, verbose=False): Branch('.').remove(file_list, verbose=verbose) def cmd_file_id(filename): i = Branch('.').read_working_inventory().path2id(filename) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message, verbose=False): Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_doctest(): """Run internal doctest suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option import bzr, doctest, bzrlib.store bzrlib.trace.verbose = False doctest.testmod(bzr) doctest.testmod(bzrlib.store) doctest.testmod(bzrlib.inventory) doctest.testmod(bzrlib.branch) doctest.testmod(bzrlib.osutils) doctest.testmod(bzrlib.tree) # more strenuous tests; import bzrlib.tests doctest.testmod(bzrlib.tests) ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('bzr --help'.split()) ([], {'help': True}) >>> parse_args('bzr --version'.split()) ([], {'version': True}) >>> parse_args('bzr status --all'.split()) (['status'], {'all': True}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? it = iter(argv[1:]) while it: a = it.next() if a[0] == '-': if a[1] == '-': mutter(" got option %r" % a) optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if not it: bailout('option %r needs an argument' % a) opts[optname] = optargfn(it.next()) mutter(" option argument %r" % opts[optname]) else: # takes no option argument opts[optname] = True elif a[:1] == '-': bailout('unknown short option %r' % a) else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. for ap in argform: argname = ap[:-1] if ap[-1] == '?': assert 0 elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. try: t = bzrlib.trace._tracefile t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % format_date(time.time())) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.gethostname())) t.write(' arguments: %r\n' % argv) starttime = os.times()[4] import platform t.write(' platform: %s\n' % platform.platform()) t.write(' python: %s\n' % platform.python_version()) ret = run_bzr(argv) times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum" % times[:4]) mutter(" %.3f elapsed" % (times[4] - starttime)) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :14 committer 1110352462 +1100 data 56 write inventory to temporary file and atomically replace from :13 M 644 inline bzrlib/branch.py data 25987 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset from store import ImmutableStore from revision import Revision from errors import bailout from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. :todo: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. :todo: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. :todo: Keep the on-disk branch locked while the object exists. :todo: mkdir() method. """ def __init__(self, base, init=False): """Create new branch object at a particular location. :param base: Base directory for the branch. :param init: If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ self.base = os.path.realpath(base) if init: self._make_control() else: if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def _rel(self, name): """Return filename relative to branch top""" return os.path.join(self.base, name) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch""" return file(self.controlfilename(file_or_path), mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # read in binary mode to detect newline wierdness. fmt = self.controlfile('branch-format', 'rb').read() if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() inv = Inventory.read_xml(self.controlfile('inventory', 'r')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'w') inv.write_xml(tmpf) tmpf.close() os.rename(tmpfname, self.controlfilename('inventory')) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. This puts the files in the Added state, so that they will be recorded by the next commit. :todo: Perhaps have an option to add the ids even if the files do not (yet) exist. :todo: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. :todo: Option to specify file id. :todo: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self._rel(f)) if isfile(fullpath): kind = 'file' elif isdir(fullpath): kind = 'directory' else: bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if len(fp) > 1: parent_name = joinpath(fp[:-1]) mutter("lookup parent %r" % parent_name) parent_id = inv.path2id(parent_name) if parent_id == None: bailout("cannot add: parent %r is not versioned" % joinpath(fp[:-1])) else: parent_id = None file_id = _gen_file_id(fp[-1]) inv.add(InventoryEntry(file_id, fp[-1], kind=kind, parent_id=parent_id)) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r parent_id={%s}" % (f, file_id, kind, parent_id)) self._write_inventory(inv) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on :todo: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True :todo: Do something useful with directories. :todo: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: show_status('D', inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. :param timestamp: if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self._rel(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = _gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'R' else: state = 'M' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) mutter("append to revision-history") self.controlfile('revision-history', 'at').write(rev_id + '\n') mutter("done!") def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. :todo: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: bailout("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original'): """Write out human-readable log of commits to this branch :param utc: If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l revno += 1 precursor = p def show_status(branch, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() :todo: Get state for single files. :todo: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = branch.basis_tree() old_inv = old.inventory new = branch.working_tree() new_inv = new.inventory for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("wierd file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files = []): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def _gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" assert '/' not in name while name[0] == '.': name = name[1:] s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :15 committer 1110353384 +1100 data 74 files that have been deleted are not considered present in the WorkingTree from :14 M 644 inline bzrlib/branch.py data 26081 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset from store import ImmutableStore from revision import Revision from errors import bailout from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. :todo: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. :todo: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. :todo: Keep the on-disk branch locked while the object exists. :todo: mkdir() method. """ def __init__(self, base, init=False): """Create new branch object at a particular location. :param base: Base directory for the branch. :param init: If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ self.base = os.path.realpath(base) if init: self._make_control() else: if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def _rel(self, name): """Return filename relative to branch top""" return os.path.join(self.base, name) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch""" return file(self.controlfilename(file_or_path), mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # read in binary mode to detect newline wierdness. fmt = self.controlfile('branch-format', 'rb').read() if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() inv = Inventory.read_xml(self.controlfile('inventory', 'r')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'w') inv.write_xml(tmpf) tmpf.close() os.rename(tmpfname, self.controlfilename('inventory')) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. This puts the files in the Added state, so that they will be recorded by the next commit. :todo: Perhaps have an option to add the ids even if the files do not (yet) exist. :todo: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. :todo: Option to specify file id. :todo: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self._rel(f)) if isfile(fullpath): kind = 'file' elif isdir(fullpath): kind = 'directory' else: bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if len(fp) > 1: parent_name = joinpath(fp[:-1]) mutter("lookup parent %r" % parent_name) parent_id = inv.path2id(parent_name) if parent_id == None: bailout("cannot add: parent %r is not versioned" % joinpath(fp[:-1])) else: parent_id = None file_id = _gen_file_id(fp[-1]) inv.add(InventoryEntry(file_id, fp[-1], kind=kind, parent_id=parent_id)) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r parent_id={%s}" % (f, file_id, kind, parent_id)) self._write_inventory(inv) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on :todo: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True :todo: Do something useful with directories. :todo: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: show_status('D', inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. :param timestamp: if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self._rel(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = _gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'R' else: state = 'M' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) mutter("append to revision-history") self.controlfile('revision-history', 'at').write(rev_id + '\n') mutter("done!") def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. :todo: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: bailout("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original'): """Write out human-readable log of commits to this branch :param utc: If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l revno += 1 precursor = p def show_status(branch, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b._rel('foo')) >>> b.show_status() D foo :todo: Get state for single files. :todo: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = branch.basis_tree() old_inv = old.inventory new = branch.working_tree() new_inv = new.inventory for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("wierd file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files = []): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def _gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" assert '/' not in name while name[0] == '.': name = name[1:] s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/tree.py data 12605 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tree classes, representing directory at point in time. """ from sets import Set import os.path, os, fnmatch from inventory import Inventory from trace import mutter, note from osutils import pumpfile, compare_files, filesize, quotefn, sha_file, \ joinpath, splitpath, appendpath, isdir, isfile, file_kind from errors import bailout import branch from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE import bzrlib class Tree: """Abstract file tree. There are several subclasses: * `WorkingTree` exists as files on disk editable by the user. * `RevisionTree` is a tree as recorded at some point in the past. * `EmptyTree` Trees contain an `Inventory` object, and also know how to retrieve file texts mentioned in the inventory, either from a working directory or from a store. It is possible for trees to contain files that are not described in their inventory or vice versa; for this use `filenames()`. Trees can be compared, etc, regardless of whether they are working trees or versioned trees. """ def has_filename(self, filename): """True if the tree has given filename.""" raise NotImplementedError() def has_id(self, file_id): return self.inventory.has_id(file_id) def id_set(self): """Return set of all ids in this tree.""" return self.inventory.id_set() def id2path(self, file_id): return self.inventory.id2path(file_id) def _get_inventory(self): return self._inventory inventory = property(_get_inventory, doc="Inventory of this Tree") def _check_retrieved(self, ie, f): # TODO: Test this check by damaging the store? if ie.text_size is not None: fs = filesize(f) if fs != ie.text_size: bailout("mismatched size for file %r in %r" % (ie.file_id, self._store), ["inventory expects %d bytes" % ie.text_size, "file is actually %d bytes" % fs, "store is probably damaged/corrupt"]) f_hash = sha_file(f) f.seek(0) if ie.text_sha1 != f_hash: bailout("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store), ["inventory expects %s" % ie.text_sha1, "file is actually %s" % f_hash, "store is probably damaged/corrupt"]) def export(self, dest): """Export this tree to a new directory. `dest` should not exist, and will be created holding the contents of this tree. :todo: To handle subdirectories we need to create the directories first. :note: If the export fails, the destination directory will be left in a half-assed state. """ os.mkdir(dest) mutter('export version %r' % self) inv = self.inventory for dp, ie in inv.iter_entries(): kind = ie.kind fullpath = appendpath(dest, dp) if kind == 'directory': os.mkdir(fullpath) elif kind == 'file': pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb')) else: bailout("don't know how to export {%s} of kind %r", fid, kind) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) class WorkingTree(Tree): """Working copy tree. The inventory is held in the `Branch` working-inventory, and the files are in a directory on disk. It is possible for a `WorkingTree` to have a filename which is not listed in the Inventory and vice versa. """ def __init__(self, basedir, inv): self._inventory = inv self.basedir = basedir self.path2id = inv.path2id def __repr__(self): return "<%s of %s>" % (self.__class__.__name__, self.basedir) def _rel(self, filename): return os.path.join(self.basedir, filename) def has_filename(self, filename): return os.path.exists(self._rel(filename)) def get_file(self, file_id): return self.get_file_byname(self.id2path(file_id)) def get_file_byname(self, filename): return file(self._rel(filename), 'rb') def _get_store_filename(self, file_id): return self._rel(self.id2path(file_id)) def has_id(self, file_id): # files that have been deleted are excluded if not self.inventory.has_id(file_id): return False return os.access(self._rel(self.inventory.id2path(file_id)), os.F_OK) def get_file_size(self, file_id): return os.stat(self._get_store_filename(file_id))[ST_SIZE] def get_file_sha1(self, file_id): f = self.get_file(file_id) return sha_file(f) def file_class(self, filename): if self.path2id(filename): return 'V' elif self.is_ignored(filename): return 'I' else: return '?' def file_kind(self, filename): if isfile(self._rel(filename)): return 'file' elif isdir(self._rel(filename)): return 'directory' else: return 'unknown' def list_files(self): """Recursively list all files as (path, class, kind, id). Lists, but does not descend into unversioned directories. This does not include files that have been deleted in this tree. Skips the control directory. """ inv = self.inventory def descend(from_dir, from_dir_id, dp): ls = os.listdir(dp) ls.sort() for f in ls: if bzrlib.BZRDIR == f: continue # path within tree fp = appendpath(from_dir, f) # absolute path fap = appendpath(dp, f) f_ie = inv.get_child(from_dir_id, f) if f_ie: c = 'V' elif self.is_ignored(fp): c = 'I' else: c = '?' fk = file_kind(fap) if f_ie: if f_ie.kind != fk: bailout("file %r entered as kind %r id %r, now of kind %r" % (fap, f_ie.kind, f_ie.file_id, fk)) yield fp, c, fk, (f_ie and f_ie.file_id) if fk != 'directory': continue if c != 'V': # don't descend unversioned directories continue for ff in descend(fp, f_ie.file_id, fap): yield ff for f in descend('', None, self.basedir): yield f def unknowns(self, path='', dir_id=None): """Yield names of unknown files in this WorkingTree. If there are any unknown directories then only the directory is returned, not all its children. But if there are unknown files under a versioned subdirectory, they are returned. Currently returned depth-first, sorted by name within directories. """ for fpath, fclass, fkind, fid in self.list_files(): if fclass == '?': yield fpath def ignored_files(self): for fpath, fclass, fkind, fid in self.list_files(): if fclass == 'I': yield fpath def get_ignore_list(self): """Return list of ignore patterns.""" if self.has_filename(bzrlib.IGNORE_FILENAME): f = self.get_file_byname(bzrlib.IGNORE_FILENAME) return [line.rstrip("\n\r") for line in f.readlines()] else: return bzrlib.DEFAULT_IGNORE def is_ignored(self, filename): """Check whether the filename matches an ignore pattern. Patterns containing '/' need to match the whole path; others match against only the last component.""" ## TODO: Take them from a file, not hardcoded ## TODO: Use extended zsh-style globs maybe? ## TODO: Use '**' to match directories? for pat in self.get_ignore_list(): if '/' in pat: if fnmatch.fnmatchcase(filename, pat): return True else: if fnmatch.fnmatchcase(splitpath(filename)[-1], pat): return True return False class RevisionTree(Tree): """Tree viewing a previous revision. File text can be retrieved from the text store. :todo: Some kind of `__repr__` method, but a good one probably means knowing the branch and revision number, or at least passing a description to the constructor. """ def __init__(self, store, inv): self._store = store self._inventory = inv def get_file(self, file_id): ie = self._inventory[file_id] f = self._store[ie.text_id] mutter(" get fileid{%s} from %r" % (file_id, self)) fs = filesize(f) if ie.text_size is None: note("warning: no text size recorded on %r" % ie) self._check_retrieved(ie, f) return f def get_file_size(self, file_id): return self._inventory[file_id].text_size def get_file_sha1(self, file_id): ie = self._inventory[file_id] return ie.text_sha1 def has_filename(self, filename): return bool(self.inventory.path2id(filename)) def list_files(self): # The only files returned by this are those from the version for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id class EmptyTree(Tree): def __init__(self): self._inventory = Inventory() def has_filename(self, filename): return False def list_files(self): if False: # just to make it a generator yield None ###################################################################### # diff # TODO: Merge these two functions into a single one that can operate # on either a whole tree or a set of files. # TODO: Return the diff in order by filename, not by category or in # random order. Can probably be done by lock-stepping through the # filenames from both trees. def file_status(filename, old_tree, new_tree): """Return single-letter status, old and new names for a file. The complexity here is in deciding how to represent renames; many complex cases are possible. """ old_inv = old_tree.inventory new_inv = new_tree.inventory new_id = new_inv.path2id(filename) old_id = old_inv.path2id(filename) if not new_id and not old_id: # easy: doesn't exist in either; not versioned at all if new_tree.is_ignored(filename): return 'I', None, None else: return '?', None, None elif new_id: # There is now a file of this name, great. pass else: # There is no longer a file of this name, but we can describe # what happened to the file that used to have # this name. There are two possibilities: either it was # deleted entirely, or renamed. assert old_id if new_inv.has_id(old_id): return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id) else: return 'D', old_inv.id2path(old_id), None # if the file_id is new in this revision, it is added if new_id and not old_inv.has_id(new_id): return 'A' # if there used to be a file of this name, but that ID has now # disappeared, it is deleted if old_id and not new_inv.has_id(old_id): return 'D' return 'wtf?' commit refs/heads/master mark :16 committer 1110353580 +1100 data 53 fix inverted calculation for original timezone -> utc from :15 M 644 inline bzrlib/osutils.py data 6575 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, types, re, time, types from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE from errors import bailout def make_readonly(filename): """Make a filename read-only.""" # TODO: probably needs to be fixed for windows mod = os.stat(filename).st_mode mod = mod & 0777555 os.chmod(filename, mod) def make_writable(filename): mod = os.stat(filename).st_mode mod = mod | 0200 os.chmod(filename, mod) _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])') def quotefn(f): """Return shell-quoted filename""" ## We could be a bit more terse by using double-quotes etc f = _QUOTE_RE.sub(r'\\\1', f) if f[0] == '~': f[0:1] = r'\~' return f def file_kind(f): mode = os.lstat(f)[ST_MODE] if S_ISREG(mode): return 'file' elif S_ISDIR(mode): return 'directory' else: bailout("can't handle file kind of %r" % fp) def isdir(f): """True if f is an accessible directory.""" try: return S_ISDIR(os.lstat(f)[ST_MODE]) except OSError: return False def isfile(f): """True if f is a regular file.""" try: return S_ISREG(os.lstat(f)[ST_MODE]) except OSError: return False def pumpfile(fromfile, tofile): """Copy contents of one file to another.""" tofile.write(fromfile.read()) def uuid(): """Return a new UUID""" ## XXX: Could alternatively read /proc/sys/kernel/random/uuid on ## Linux, but we need something portable for other systems; ## preferably an implementation in Python. bailout('uuids not allowed!') return chomp(os.popen('uuidgen').readline()) def chomp(s): if s and (s[-1] == '\n'): return s[:-1] else: return s def sha_file(f): import sha ## TODO: Maybe read in chunks to handle big files if hasattr(f, 'tell'): assert f.tell() == 0 s = sha.new() s.update(f.read()) return s.hexdigest() def sha_string(f): import sha s = sha.new() s.update(f) return s.hexdigest() def username(): """Return email-style username. Something similar to 'Martin Pool ' :todo: Check it's reasonably well-formed. :todo: Allow taking it from a dotfile to help people on windows who can't easily set variables. :todo: Cope without pwd module, which is only on unix. """ e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: return e import socket try: import pwd uid = os.getuid() w = pwd.getpwuid(uid) realname, junk = w.pw_gecos.split(',', 1) return '%s <%s@%s>' % (realname, w.pw_name, socket.getfqdn()) except ImportError: pass import getpass, socket return '<%s@%s>' % (getpass.getuser(), socket.getfqdn()) def user_email(): """Return just the email component of a username.""" e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: import re m = re.search(r'[\w+.-]+@[\w+.-]+', e) if not m: bailout('%r is not a reasonable email address' % e) return m.group(0) import getpass, socket return '%s@%s' % (getpass.getuser(), socket.getfqdn()) def compare_files(a, b): """Returns true if equal in contents""" # TODO: don't read the whole thing in one go. result = a.read() == b.read() return result def local_time_offset(): if time.daylight: return -time.altzone else: return -time.timezone def format_date(t, offset=0, timezone='original'): ## TODO: Perhaps a global option to use either universal or local time? ## Or perhaps just let people set $TZ? import time assert isinstance(t, float) if timezone == 'utc': tt = time.gmtime(t) offset = 0 elif timezone == 'original': tt = time.gmtime(t + offset) elif timezone == 'local': tt = time.localtime(t) offset = local_time_offset() else: bailout("unsupported timezone format %r", ['options are "utc", "original", "local"']) return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt) + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)) def compact_date(when): return time.strftime('%Y%m%d%H%M%S', time.gmtime(when)) def filesize(f): """Return size of given open file.""" return os.fstat(f.fileno())[ST_SIZE] if hasattr(os, 'urandom'): # python 2.4 and later rand_bytes = os.urandom else: # FIXME: No good on non-Linux _rand_file = file('/dev/urandom', 'rb') rand_bytes = _rand_file.read ## TODO: We could later have path objects that remember their list ## decomposition (might be too tricksy though.) def splitpath(p): """Turn string into list of parts. >>> splitpath('a') ['a'] >>> splitpath('a/b') ['a', 'b'] >>> splitpath('a/./b') ['a', 'b'] >>> splitpath('a/.b') ['a', '.b'] >>> splitpath('a/../b') Traceback (most recent call last): ... BzrError: ("sorry, '..' not allowed in path", []) """ assert isinstance(p, types.StringTypes) ps = [f for f in p.split('/') if f != '.'] for f in ps: if f == '..': bailout("sorry, %r not allowed in path" % f) return ps def joinpath(p): assert isinstance(p, list) for f in p: if (f == '..') or (f is None) or (f == ''): bailout("sorry, %r not allowed in path" % f) return '/'.join(p) def appendpath(p1, p2): if p1 == '': return p2 else: return p1 + '/' + p2 def extern_command(cmd, ignore_errors = False): mutter('external command: %s' % `cmd`) if os.system(cmd): if not ignore_errors: bailout('command failed') commit refs/heads/master mark :17 committer 1110354007 +1100 data 25 allow --option=ARG syntax from :16 M 644 inline bzr.py data 21225 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. """ # not currently working: # bzr check # Run internal consistency checks. # bzr info # Show some information about this branch. __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ###################################################################### # check status def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_inventory(inventory_id): """Return inventory in XML by hash""" Branch('.').get_inventory(inventory_hash).write_xml(sys.stdout) def cmd_get_revision_inventory(revision_id): """Output inventory for a revision.""" b = Branch('.') b.get_revision_inventory(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files. Fails if the files are already added. """ Branch('.').add(file_list, verbose=verbose) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): b = Branch('.') print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') print 'revision number:', b.revno() print 'number of versioned files:', len(b.read_working_inventory()) def cmd_remove(file_list, verbose=False): Branch('.').remove(file_list, verbose=verbose) def cmd_file_id(filename): i = Branch('.').read_working_inventory().path2id(filename) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message, verbose=False): Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_doctest(): """Run internal doctest suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option import bzr, doctest, bzrlib.store bzrlib.trace.verbose = False doctest.testmod(bzr) doctest.testmod(bzrlib.store) doctest.testmod(bzrlib.inventory) doctest.testmod(bzrlib.branch) doctest.testmod(bzrlib.osutils) doctest.testmod(bzrlib.tree) # more strenuous tests; import bzrlib.tests doctest.testmod(bzrlib.tests) ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('bzr --help'.split()) ([], {'help': True}) >>> parse_args('bzr --version'.split()) ([], {'version': True}) >>> parse_args('bzr status --all'.split()) (['status'], {'all': True}) >>> parse_args('bzr commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? it = iter(argv[1:]) while it: a = it.next() if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not it: bailout('option %r needs an argument' % a) else: optarg = it.next() opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. for ap in argform: argname = ap[:-1] if ap[-1] == '?': assert 0 elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. try: t = bzrlib.trace._tracefile t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % format_date(time.time())) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.gethostname())) t.write(' arguments: %r\n' % argv) starttime = os.times()[4] import platform t.write(' platform: %s\n' % platform.platform()) t.write(' python: %s\n' % platform.python_version()) ret = run_bzr(argv) times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum" % times[:4]) mutter(" %.3f elapsed" % (times[4] - starttime)) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :18 committer 1110521483 +1100 data 45 show count of versioned/unknown/ignored files from :17 M 644 inline bzr.py data 21907 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. """ # not currently working: # bzr check # Run internal consistency checks. # bzr info # Show some information about this branch. __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ###################################################################### # check status def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_inventory(inventory_id): """Return inventory in XML by hash""" Branch('.').get_inventory(inventory_hash).write_xml(sys.stdout) def cmd_get_revision_inventory(revision_id): """Output inventory for a revision.""" b = Branch('.') b.get_revision_inventory(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files. Fails if the files are already added. """ Branch('.').add(file_list, verbose=verbose) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): b = Branch('.') print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') print 'revision number:', b.revno() count_versioned = count_unknown = count_ignored = 0 count_version_dirs = 0 for fpath, fclass, fkind, fid in b.working_tree().list_files(): if fclass == 'V': count_versioned += 1 if fkind == 'directory': count_version_dirs += 1 elif fclass == 'I': count_ignored += 1 elif fclass == '?': count_unknown += 1 else: bailout('unknown file class %r for %r' % (fclass, fpath)) print 'number of versioned entries: %d' % count_versioned print 'number of versioned subdirectories: %d' % count_version_dirs print 'number of unknown files: %d' % count_unknown print 'number of ignored files: %d' % count_ignored def cmd_remove(file_list, verbose=False): Branch('.').remove(file_list, verbose=verbose) def cmd_file_id(filename): i = Branch('.').read_working_inventory().path2id(filename) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message, verbose=False): Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_doctest(): """Run internal doctest suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option import bzr, doctest, bzrlib.store bzrlib.trace.verbose = False doctest.testmod(bzr) doctest.testmod(bzrlib.store) doctest.testmod(bzrlib.inventory) doctest.testmod(bzrlib.branch) doctest.testmod(bzrlib.osutils) doctest.testmod(bzrlib.tree) # more strenuous tests; import bzrlib.tests doctest.testmod(bzrlib.tests) ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('bzr --help'.split()) ([], {'help': True}) >>> parse_args('bzr --version'.split()) ([], {'version': True}) >>> parse_args('bzr status --all'.split()) (['status'], {'all': True}) >>> parse_args('bzr commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? it = iter(argv[1:]) while it: a = it.next() if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not it: bailout('option %r needs an argument' % a) else: optarg = it.next() opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. for ap in argform: argname = ap[:-1] if ap[-1] == '?': assert 0 elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. try: t = bzrlib.trace._tracefile t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % format_date(time.time())) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.gethostname())) t.write(' arguments: %r\n' % argv) starttime = os.times()[4] import platform t.write(' platform: %s\n' % platform.platform()) t.write(' python: %s\n' % platform.python_version()) ret = run_bzr(argv) times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum" % times[:4]) mutter(" %.3f elapsed" % (times[4] - starttime)) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :19 committer 1110522136 +1100 data 32 more information in info command from :18 M 644 inline bzr.py data 21867 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. """ # not currently working: # bzr check # Run internal consistency checks. # bzr info # Show some information about this branch. __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ###################################################################### # check status def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_inventory(inventory_id): """Return inventory in XML by hash""" Branch('.').get_inventory(inventory_hash).write_xml(sys.stdout) def cmd_get_revision_inventory(revision_id): """Output inventory for a revision.""" b = Branch('.') b.get_revision_inventory(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files. Fails if the files are already added. """ Branch('.').add(file_list, verbose=verbose) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): b = Branch('.') print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') print 'revision number:', b.revno() count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %5d %s' % (count_status[fs], name) print ' %5d versioned subdirectories' % count_version_dirs def cmd_remove(file_list, verbose=False): Branch('.').remove(file_list, verbose=verbose) def cmd_file_id(filename): i = Branch('.').read_working_inventory().path2id(filename) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message, verbose=False): Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_doctest(): """Run internal doctest suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option import bzr, doctest, bzrlib.store bzrlib.trace.verbose = False doctest.testmod(bzr) doctest.testmod(bzrlib.store) doctest.testmod(bzrlib.inventory) doctest.testmod(bzrlib.branch) doctest.testmod(bzrlib.osutils) doctest.testmod(bzrlib.tree) # more strenuous tests; import bzrlib.tests doctest.testmod(bzrlib.tests) ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('bzr --help'.split()) ([], {'help': True}) >>> parse_args('bzr --version'.split()) ([], {'version': True}) >>> parse_args('bzr status --all'.split()) (['status'], {'all': True}) >>> parse_args('bzr commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? it = iter(argv[1:]) while it: a = it.next() if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not it: bailout('option %r needs an argument' % a) else: optarg = it.next() opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. for ap in argform: argname = ap[:-1] if ap[-1] == '?': assert 0 elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. try: t = bzrlib.trace._tracefile t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % format_date(time.time())) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.gethostname())) t.write(' arguments: %r\n' % argv) starttime = os.times()[4] import platform t.write(' platform: %s\n' % platform.platform()) t.write(' python: %s\n' % platform.python_version()) ret = run_bzr(argv) times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum" % times[:4]) mutter(" %.3f elapsed" % (times[4] - starttime)) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :20 committer 1110522280 +1100 data 92 don't abort on trees that happen to contain symlinks (they still can't be versioned though). from :19 M 644 inline bzrlib/osutils.py data 6654 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, types, re, time, types from stat import S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE from errors import bailout def make_readonly(filename): """Make a filename read-only.""" # TODO: probably needs to be fixed for windows mod = os.stat(filename).st_mode mod = mod & 0777555 os.chmod(filename, mod) def make_writable(filename): mod = os.stat(filename).st_mode mod = mod | 0200 os.chmod(filename, mod) _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])') def quotefn(f): """Return shell-quoted filename""" ## We could be a bit more terse by using double-quotes etc f = _QUOTE_RE.sub(r'\\\1', f) if f[0] == '~': f[0:1] = r'\~' return f def file_kind(f): mode = os.lstat(f)[ST_MODE] if S_ISREG(mode): return 'file' elif S_ISDIR(mode): return 'directory' elif S_ISLNK(mode): return 'symlink' else: bailout("can't handle file kind with mode %o of %r" % (mode, f)) def isdir(f): """True if f is an accessible directory.""" try: return S_ISDIR(os.lstat(f)[ST_MODE]) except OSError: return False def isfile(f): """True if f is a regular file.""" try: return S_ISREG(os.lstat(f)[ST_MODE]) except OSError: return False def pumpfile(fromfile, tofile): """Copy contents of one file to another.""" tofile.write(fromfile.read()) def uuid(): """Return a new UUID""" ## XXX: Could alternatively read /proc/sys/kernel/random/uuid on ## Linux, but we need something portable for other systems; ## preferably an implementation in Python. bailout('uuids not allowed!') return chomp(os.popen('uuidgen').readline()) def chomp(s): if s and (s[-1] == '\n'): return s[:-1] else: return s def sha_file(f): import sha ## TODO: Maybe read in chunks to handle big files if hasattr(f, 'tell'): assert f.tell() == 0 s = sha.new() s.update(f.read()) return s.hexdigest() def sha_string(f): import sha s = sha.new() s.update(f) return s.hexdigest() def username(): """Return email-style username. Something similar to 'Martin Pool ' :todo: Check it's reasonably well-formed. :todo: Allow taking it from a dotfile to help people on windows who can't easily set variables. :todo: Cope without pwd module, which is only on unix. """ e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: return e import socket try: import pwd uid = os.getuid() w = pwd.getpwuid(uid) realname, junk = w.pw_gecos.split(',', 1) return '%s <%s@%s>' % (realname, w.pw_name, socket.getfqdn()) except ImportError: pass import getpass, socket return '<%s@%s>' % (getpass.getuser(), socket.getfqdn()) def user_email(): """Return just the email component of a username.""" e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: import re m = re.search(r'[\w+.-]+@[\w+.-]+', e) if not m: bailout('%r is not a reasonable email address' % e) return m.group(0) import getpass, socket return '%s@%s' % (getpass.getuser(), socket.getfqdn()) def compare_files(a, b): """Returns true if equal in contents""" # TODO: don't read the whole thing in one go. result = a.read() == b.read() return result def local_time_offset(): if time.daylight: return -time.altzone else: return -time.timezone def format_date(t, offset=0, timezone='original'): ## TODO: Perhaps a global option to use either universal or local time? ## Or perhaps just let people set $TZ? import time assert isinstance(t, float) if timezone == 'utc': tt = time.gmtime(t) offset = 0 elif timezone == 'original': tt = time.gmtime(t + offset) elif timezone == 'local': tt = time.localtime(t) offset = local_time_offset() else: bailout("unsupported timezone format %r", ['options are "utc", "original", "local"']) return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt) + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)) def compact_date(when): return time.strftime('%Y%m%d%H%M%S', time.gmtime(when)) def filesize(f): """Return size of given open file.""" return os.fstat(f.fileno())[ST_SIZE] if hasattr(os, 'urandom'): # python 2.4 and later rand_bytes = os.urandom else: # FIXME: No good on non-Linux _rand_file = file('/dev/urandom', 'rb') rand_bytes = _rand_file.read ## TODO: We could later have path objects that remember their list ## decomposition (might be too tricksy though.) def splitpath(p): """Turn string into list of parts. >>> splitpath('a') ['a'] >>> splitpath('a/b') ['a', 'b'] >>> splitpath('a/./b') ['a', 'b'] >>> splitpath('a/.b') ['a', '.b'] >>> splitpath('a/../b') Traceback (most recent call last): ... BzrError: ("sorry, '..' not allowed in path", []) """ assert isinstance(p, types.StringTypes) ps = [f for f in p.split('/') if f != '.'] for f in ps: if f == '..': bailout("sorry, %r not allowed in path" % f) return ps def joinpath(p): assert isinstance(p, list) for f in p: if (f == '..') or (f is None) or (f == ''): bailout("sorry, %r not allowed in path" % f) return '/'.join(p) def appendpath(p1, p2): if p1 == '': return p2 else: return p1 + '/' + p2 def extern_command(cmd, ignore_errors = False): mutter('external command: %s' % `cmd`) if os.system(cmd): if not ignore_errors: bailout('command failed') commit refs/heads/master mark :21 committer 1110522985 +1100 data 70 - bzr info: show summary information on branch history - nicer plurals from :20 M 644 inline bzr.py data 22586 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. """ # not currently working: # bzr check # Run internal consistency checks. # bzr info # Show some information about this branch. __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ###################################################################### # check status def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_inventory(inventory_id): """Return inventory in XML by hash""" Branch('.').get_inventory(inventory_hash).write_xml(sys.stdout) def cmd_get_revision_inventory(revision_id): """Output inventory for a revision.""" b = Branch('.') b.get_revision_inventory(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files. Fails if the files are already added. """ Branch('.').add(file_list, verbose=verbose) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): b = Branch('.') print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl is not None: return pl else: return 's' count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %5d %s' % (count_status[fs], name) print ' %5d versioned subdirector%s' % (count_version_dirs, plural(count_version_dirs, 'y', 'ies')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %5d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %5d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %5d day%s old' % (age, plural(age)) def cmd_remove(file_list, verbose=False): Branch('.').remove(file_list, verbose=verbose) def cmd_file_id(filename): i = Branch('.').read_working_inventory().path2id(filename) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message, verbose=False): Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_doctest(): """Run internal doctest suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option import bzr, doctest, bzrlib.store bzrlib.trace.verbose = False doctest.testmod(bzr) doctest.testmod(bzrlib.store) doctest.testmod(bzrlib.inventory) doctest.testmod(bzrlib.branch) doctest.testmod(bzrlib.osutils) doctest.testmod(bzrlib.tree) # more strenuous tests; import bzrlib.tests doctest.testmod(bzrlib.tests) ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('bzr --help'.split()) ([], {'help': True}) >>> parse_args('bzr --version'.split()) ([], {'version': True}) >>> parse_args('bzr status --all'.split()) (['status'], {'all': True}) >>> parse_args('bzr commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? it = iter(argv[1:]) while it: a = it.next() if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not it: bailout('option %r needs an argument' % a) else: optarg = it.next() opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. for ap in argform: argname = ap[:-1] if ap[-1] == '?': assert 0 elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. try: t = bzrlib.trace._tracefile t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % format_date(time.time())) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.gethostname())) t.write(' arguments: %r\n' % argv) starttime = os.times()[4] import platform t.write(' platform: %s\n' % platform.platform()) t.write(' python: %s\n' % platform.python_version()) ret = run_bzr(argv) times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum" % times[:4]) mutter(" %.3f elapsed" % (times[4] - starttime)) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :22 committer 1110583174 +1100 data 46 bzr info: show date of first and latest commit from :21 M 644 inline bzr.py data 22922 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. """ # not currently working: # bzr check # Run internal consistency checks. # bzr info # Show some information about this branch. __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ###################################################################### # check status def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_inventory(inventory_id): """Return inventory in XML by hash""" Branch('.').get_inventory(inventory_hash).write_xml(sys.stdout) def cmd_get_revision_inventory(revision_id): """Output inventory for a revision.""" b = Branch('.') b.get_revision_inventory(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files. Fails if the files are already added. """ Branch('.').add(file_list, verbose=verbose) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): b = Branch('.') print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl is not None: return pl else: return 's' count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %5d %s' % (count_status[fs], name) print ' %5d versioned subdirector%s' % (count_version_dirs, plural(count_version_dirs, 'y', 'ies')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %5d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %5d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %5d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) def cmd_remove(file_list, verbose=False): Branch('.').remove(file_list, verbose=verbose) def cmd_file_id(filename): i = Branch('.').read_working_inventory().path2id(filename) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message, verbose=False): Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_doctest(): """Run internal doctest suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option import bzr, doctest, bzrlib.store bzrlib.trace.verbose = False doctest.testmod(bzr) doctest.testmod(bzrlib.store) doctest.testmod(bzrlib.inventory) doctest.testmod(bzrlib.branch) doctest.testmod(bzrlib.osutils) doctest.testmod(bzrlib.tree) # more strenuous tests; import bzrlib.tests doctest.testmod(bzrlib.tests) ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('bzr --help'.split()) ([], {'help': True}) >>> parse_args('bzr --version'.split()) ([], {'version': True}) >>> parse_args('bzr status --all'.split()) (['status'], {'all': True}) >>> parse_args('bzr commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? it = iter(argv[1:]) while it: a = it.next() if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not it: bailout('option %r needs an argument' % a) else: optarg = it.next() opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. for ap in argform: argname = ap[:-1] if ap[-1] == '?': assert 0 elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. try: t = bzrlib.trace._tracefile t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % format_date(time.time())) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.gethostname())) t.write(' arguments: %r\n' % argv) starttime = os.times()[4] import platform t.write(' platform: %s\n' % platform.platform()) t.write(' python: %s\n' % platform.python_version()) ret = run_bzr(argv) times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum" % times[:4]) mutter(" %.3f elapsed" % (times[4] - starttime)) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :23 committer 1110583193 +1100 data 53 format_date: handle revisions with no timezone offset from :22 M 644 inline bzrlib/osutils.py data 6704 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, types, re, time, types from stat import S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE from errors import bailout def make_readonly(filename): """Make a filename read-only.""" # TODO: probably needs to be fixed for windows mod = os.stat(filename).st_mode mod = mod & 0777555 os.chmod(filename, mod) def make_writable(filename): mod = os.stat(filename).st_mode mod = mod | 0200 os.chmod(filename, mod) _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])') def quotefn(f): """Return shell-quoted filename""" ## We could be a bit more terse by using double-quotes etc f = _QUOTE_RE.sub(r'\\\1', f) if f[0] == '~': f[0:1] = r'\~' return f def file_kind(f): mode = os.lstat(f)[ST_MODE] if S_ISREG(mode): return 'file' elif S_ISDIR(mode): return 'directory' elif S_ISLNK(mode): return 'symlink' else: bailout("can't handle file kind with mode %o of %r" % (mode, f)) def isdir(f): """True if f is an accessible directory.""" try: return S_ISDIR(os.lstat(f)[ST_MODE]) except OSError: return False def isfile(f): """True if f is a regular file.""" try: return S_ISREG(os.lstat(f)[ST_MODE]) except OSError: return False def pumpfile(fromfile, tofile): """Copy contents of one file to another.""" tofile.write(fromfile.read()) def uuid(): """Return a new UUID""" ## XXX: Could alternatively read /proc/sys/kernel/random/uuid on ## Linux, but we need something portable for other systems; ## preferably an implementation in Python. bailout('uuids not allowed!') return chomp(os.popen('uuidgen').readline()) def chomp(s): if s and (s[-1] == '\n'): return s[:-1] else: return s def sha_file(f): import sha ## TODO: Maybe read in chunks to handle big files if hasattr(f, 'tell'): assert f.tell() == 0 s = sha.new() s.update(f.read()) return s.hexdigest() def sha_string(f): import sha s = sha.new() s.update(f) return s.hexdigest() def username(): """Return email-style username. Something similar to 'Martin Pool ' :todo: Check it's reasonably well-formed. :todo: Allow taking it from a dotfile to help people on windows who can't easily set variables. :todo: Cope without pwd module, which is only on unix. """ e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: return e import socket try: import pwd uid = os.getuid() w = pwd.getpwuid(uid) realname, junk = w.pw_gecos.split(',', 1) return '%s <%s@%s>' % (realname, w.pw_name, socket.getfqdn()) except ImportError: pass import getpass, socket return '<%s@%s>' % (getpass.getuser(), socket.getfqdn()) def user_email(): """Return just the email component of a username.""" e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: import re m = re.search(r'[\w+.-]+@[\w+.-]+', e) if not m: bailout('%r is not a reasonable email address' % e) return m.group(0) import getpass, socket return '%s@%s' % (getpass.getuser(), socket.getfqdn()) def compare_files(a, b): """Returns true if equal in contents""" # TODO: don't read the whole thing in one go. result = a.read() == b.read() return result def local_time_offset(): if time.daylight: return -time.altzone else: return -time.timezone def format_date(t, offset=0, timezone='original'): ## TODO: Perhaps a global option to use either universal or local time? ## Or perhaps just let people set $TZ? import time assert isinstance(t, float) if timezone == 'utc': tt = time.gmtime(t) offset = 0 elif timezone == 'original': if offset == None: offset = 0 tt = time.gmtime(t + offset) elif timezone == 'local': tt = time.localtime(t) offset = local_time_offset() else: bailout("unsupported timezone format %r", ['options are "utc", "original", "local"']) return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt) + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)) def compact_date(when): return time.strftime('%Y%m%d%H%M%S', time.gmtime(when)) def filesize(f): """Return size of given open file.""" return os.fstat(f.fileno())[ST_SIZE] if hasattr(os, 'urandom'): # python 2.4 and later rand_bytes = os.urandom else: # FIXME: No good on non-Linux _rand_file = file('/dev/urandom', 'rb') rand_bytes = _rand_file.read ## TODO: We could later have path objects that remember their list ## decomposition (might be too tricksy though.) def splitpath(p): """Turn string into list of parts. >>> splitpath('a') ['a'] >>> splitpath('a/b') ['a', 'b'] >>> splitpath('a/./b') ['a', 'b'] >>> splitpath('a/.b') ['a', '.b'] >>> splitpath('a/../b') Traceback (most recent call last): ... BzrError: ("sorry, '..' not allowed in path", []) """ assert isinstance(p, types.StringTypes) ps = [f for f in p.split('/') if f != '.'] for f in ps: if f == '..': bailout("sorry, %r not allowed in path" % f) return ps def joinpath(p): assert isinstance(p, list) for f in p: if (f == '..') or (f is None) or (f == ''): bailout("sorry, %r not allowed in path" % f) return '/'.join(p) def appendpath(p1, p2): if p1 == '': return p2 else: return p1 + '/' + p2 def extern_command(cmd, ignore_errors = False): mutter('external command: %s' % `cmd`) if os.system(cmd): if not ignore_errors: bailout('command failed') commit refs/heads/master mark :24 committer 1110583433 +1100 data 19 Add .bzrignore file from :23 M 644 inline .bzrignore data 33 doc/*.html *.py[oc] *~ .* {arch} commit refs/heads/master mark :25 committer Martin Pool 1110611799 +1100 data 80 cope when gecos field doesn't have a comma reported by ysaito, rooneg -- thanks from :24 M 644 inline bzrlib/osutils.py data 6817 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, types, re, time, types from stat import S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE from errors import bailout def make_readonly(filename): """Make a filename read-only.""" # TODO: probably needs to be fixed for windows mod = os.stat(filename).st_mode mod = mod & 0777555 os.chmod(filename, mod) def make_writable(filename): mod = os.stat(filename).st_mode mod = mod | 0200 os.chmod(filename, mod) _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])') def quotefn(f): """Return shell-quoted filename""" ## We could be a bit more terse by using double-quotes etc f = _QUOTE_RE.sub(r'\\\1', f) if f[0] == '~': f[0:1] = r'\~' return f def file_kind(f): mode = os.lstat(f)[ST_MODE] if S_ISREG(mode): return 'file' elif S_ISDIR(mode): return 'directory' elif S_ISLNK(mode): return 'symlink' else: bailout("can't handle file kind with mode %o of %r" % (mode, f)) def isdir(f): """True if f is an accessible directory.""" try: return S_ISDIR(os.lstat(f)[ST_MODE]) except OSError: return False def isfile(f): """True if f is a regular file.""" try: return S_ISREG(os.lstat(f)[ST_MODE]) except OSError: return False def pumpfile(fromfile, tofile): """Copy contents of one file to another.""" tofile.write(fromfile.read()) def uuid(): """Return a new UUID""" ## XXX: Could alternatively read /proc/sys/kernel/random/uuid on ## Linux, but we need something portable for other systems; ## preferably an implementation in Python. bailout('uuids not allowed!') return chomp(os.popen('uuidgen').readline()) def chomp(s): if s and (s[-1] == '\n'): return s[:-1] else: return s def sha_file(f): import sha ## TODO: Maybe read in chunks to handle big files if hasattr(f, 'tell'): assert f.tell() == 0 s = sha.new() s.update(f.read()) return s.hexdigest() def sha_string(f): import sha s = sha.new() s.update(f) return s.hexdigest() def username(): """Return email-style username. Something similar to 'Martin Pool ' :todo: Check it's reasonably well-formed. :todo: Allow taking it from a dotfile to help people on windows who can't easily set variables. :todo: Cope without pwd module, which is only on unix. """ e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: return e import socket try: import pwd uid = os.getuid() w = pwd.getpwuid(uid) gecos = w.pw_gecos comma = gecos.find(',') if comma == -1: realname = gecos else: realname = gecos[:comma] return '%s <%s@%s>' % (realname, w.pw_name, socket.getfqdn()) except ImportError: pass import getpass, socket return '<%s@%s>' % (getpass.getuser(), socket.getfqdn()) def user_email(): """Return just the email component of a username.""" e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: import re m = re.search(r'[\w+.-]+@[\w+.-]+', e) if not m: bailout('%r is not a reasonable email address' % e) return m.group(0) import getpass, socket return '%s@%s' % (getpass.getuser(), socket.getfqdn()) def compare_files(a, b): """Returns true if equal in contents""" # TODO: don't read the whole thing in one go. result = a.read() == b.read() return result def local_time_offset(): if time.daylight: return -time.altzone else: return -time.timezone def format_date(t, offset=0, timezone='original'): ## TODO: Perhaps a global option to use either universal or local time? ## Or perhaps just let people set $TZ? import time assert isinstance(t, float) if timezone == 'utc': tt = time.gmtime(t) offset = 0 elif timezone == 'original': if offset == None: offset = 0 tt = time.gmtime(t + offset) elif timezone == 'local': tt = time.localtime(t) offset = local_time_offset() else: bailout("unsupported timezone format %r", ['options are "utc", "original", "local"']) return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt) + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)) def compact_date(when): return time.strftime('%Y%m%d%H%M%S', time.gmtime(when)) def filesize(f): """Return size of given open file.""" return os.fstat(f.fileno())[ST_SIZE] if hasattr(os, 'urandom'): # python 2.4 and later rand_bytes = os.urandom else: # FIXME: No good on non-Linux _rand_file = file('/dev/urandom', 'rb') rand_bytes = _rand_file.read ## TODO: We could later have path objects that remember their list ## decomposition (might be too tricksy though.) def splitpath(p): """Turn string into list of parts. >>> splitpath('a') ['a'] >>> splitpath('a/b') ['a', 'b'] >>> splitpath('a/./b') ['a', 'b'] >>> splitpath('a/.b') ['a', '.b'] >>> splitpath('a/../b') Traceback (most recent call last): ... BzrError: ("sorry, '..' not allowed in path", []) """ assert isinstance(p, types.StringTypes) ps = [f for f in p.split('/') if f != '.'] for f in ps: if f == '..': bailout("sorry, %r not allowed in path" % f) return ps def joinpath(p): assert isinstance(p, list) for f in p: if (f == '..') or (f is None) or (f == ''): bailout("sorry, %r not allowed in path" % f) return '/'.join(p) def appendpath(p1, p2): if p1 == '': return p2 else: return p1 + '/' + p2 def extern_command(cmd, ignore_errors = False): mutter('external command: %s' % `cmd`) if os.system(cmd): if not ignore_errors: bailout('command failed') commit refs/heads/master mark :26 committer 1110612872 +1100 data 39 fix StopIteration error on python2.3(?) from :25 M 644 inline bzr.py data 22911 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. """ # not currently working: # bzr check # Run internal consistency checks. # bzr info # Show some information about this branch. __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ###################################################################### # check status def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_inventory(inventory_id): """Return inventory in XML by hash""" Branch('.').get_inventory(inventory_hash).write_xml(sys.stdout) def cmd_get_revision_inventory(revision_id): """Output inventory for a revision.""" b = Branch('.') b.get_revision_inventory(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files. Fails if the files are already added. """ Branch('.').add(file_list, verbose=verbose) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): b = Branch('.') print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl is not None: return pl else: return 's' count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %5d %s' % (count_status[fs], name) print ' %5d versioned subdirector%s' % (count_version_dirs, plural(count_version_dirs, 'y', 'ies')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %5d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %5d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %5d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) def cmd_remove(file_list, verbose=False): Branch('.').remove(file_list, verbose=verbose) def cmd_file_id(filename): i = Branch('.').read_working_inventory().path2id(filename) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message, verbose=False): Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_doctest(): """Run internal doctest suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option import bzr, doctest, bzrlib.store bzrlib.trace.verbose = False doctest.testmod(bzr) doctest.testmod(bzrlib.store) doctest.testmod(bzrlib.inventory) doctest.testmod(bzrlib.branch) doctest.testmod(bzrlib.osutils) doctest.testmod(bzrlib.tree) # more strenuous tests; import bzrlib.tests doctest.testmod(bzrlib.tests) ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('bzr --help'.split()) ([], {'help': True}) >>> parse_args('bzr --version'.split()) ([], {'version': True}) >>> parse_args('bzr status --all'.split()) (['status'], {'all': True}) >>> parse_args('bzr commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. for ap in argform: argname = ap[:-1] if ap[-1] == '?': assert 0 elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. try: t = bzrlib.trace._tracefile t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % format_date(time.time())) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.gethostname())) t.write(' arguments: %r\n' % argv) starttime = os.times()[4] import platform t.write(' platform: %s\n' % platform.platform()) t.write(' python: %s\n' % platform.python_version()) ret = run_bzr(argv) times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum" % times[:4]) mutter(" %.3f elapsed" % (times[4] - starttime)) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :27 committer 1110613111 +1100 data 67 - fix up use of ElementTree without cElementTree patch from rooneg from :26 M 644 inline bzrlib/inventory.py data 14941 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Inventories map files to their name in a revision.""" __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os.path, types from sets import Set try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from xml import XMLMixin from errors import bailout from osutils import uuid, quotefn, splitpath, joinpath, appendpath from trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') >>> i.add(InventoryEntry('123', 'src', kind='directory')) >>> i.add(InventoryEntry('2323', 'hello.c', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id=None)) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', parent_id='123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', parent_id='123')) >>> i.add(InventoryEntry('2325', 'wibble', parent_id='123', kind='directory')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', parent_id='2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' :todo: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ def __init__(self, file_id, name, kind='file', text_id=None, parent_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c') >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c') Traceback (most recent call last): BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", []) """ if len(splitpath(name)) != 1: bailout('InventoryEntry name is not a simple filename: %r' % name) self.file_id = file_id self.name = name assert kind in ['file', 'directory'] self.kind = kind self.text_id = text_id self.parent_id = parent_id self.text_sha1 = None self.text_size = None def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.text_id, self.parent_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size is not None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1', 'parent_id']: v = getattr(self, f) if v is not None: e.set(f, v) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind')) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') self.parent_id = elt.get('parent_id') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __cmp__(self, other): if self is other: return 0 if not isinstance(other, InventoryEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.name, other.name) \ or cmp(self.text_sha1, other.text_sha1) \ or cmp(self.text_size, other.text_size) \ or cmp(self.text_id, other.text_id) \ or cmp(self.parent_id, other.parent_id) \ or cmp(self.kind, other.kind) class Inventory(XMLMixin): """Inventory of versioned files in a tree. An Inventory acts like a set of InventoryEntry items. You can also look files up by their file_id or name. May be read from and written to a metadata file in a tree. To manipulate the inventory (for example to add a file), it is read in, modified, and then written back out. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c')) >>> inv['123-123'].name 'hello.c' >>> for file_id in inv: print file_id ... 123-123 May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ ## TODO: Clear up handling of files in subdirectories; we probably ## do want to be able to just look them up by name but this ## probably means gradually walking down the path, looking up as we go. ## TODO: Make sure only canonical filenames are stored. ## TODO: Do something sensible about the possible collisions on ## case-losing filesystems. Perhaps we should just always forbid ## such collisions. ## _tree should probably just be stored as ## InventoryEntry._children on each directory. def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. """ self._byid = dict() # _tree is indexed by parent_id; at each level a map from name # to ie. The None entry is the root. self._tree = {None: {}} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, parent_id=None): """Return (path, entry) pairs, in order by name.""" kids = self._tree[parent_id].items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(parent_id=ie.file_id): yield joinpath([name, cn]), cie def directories(self, include_root=True): """Return (path, entry) pairs for all directories. """ if include_root: yield '', None for path, entry in self.iter_entries(): if entry.kind == 'directory': yield path, entry def children(self, parent_id): """Return entries that are direct children of parent_id.""" return self._tree[parent_id] # TODO: return all paths and entries def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c')) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c')) >>> inv['123123'].name 'hello.c' """ return self._byid[file_id] def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self: bailout("inventory already contains entry with id {%s}" % entry.file_id) if entry.parent_id != None: if entry.parent_id not in self: bailout("parent_id %s of new entry not found in inventory" % entry.parent_id) if self._tree[entry.parent_id].has_key(entry.name): bailout("%s is already versioned" % appendpath(self.id2path(entry.parent_id), entry.name)) self._byid[entry.file_id] = entry self._tree[entry.parent_id][entry.name] = entry if entry.kind == 'directory': self._tree[entry.file_id] = {} def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c')) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self._tree[ie.parent_id][ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in self._tree[file_id].values(): del self[cie.file_id] del self._tree[file_id] del self._byid[file_id] del self._tree[ie.parent_id][ie.name] def id_set(self): return Set(self._byid) def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c')) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __cmp__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo')) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo')) >>> i1 == i2 True """ if self is other: return 0 if not isinstance(other, Inventory): return NotImplemented if self.id_set() ^ other.id_set(): return 1 for file_id in self._byid: c = cmp(self[file_id], other[file_id]) if c: return c return 0 def id2path(self, file_id): """Return as a list the path to file_id.""" p = [] while file_id != None: ie = self[file_id] p = [ie.name] + p file_id = ie.parent_id return joinpath(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. """ assert isinstance(name, types.StringTypes) parent_id = None for f in splitpath(name): try: cie = self._tree[parent_id][f] assert cie.name == f parent_id = cie.file_id except KeyError: # or raise an error? return None return parent_id def get_child(self, parent_id, child_name): return self._tree[parent_id].get(child_name) def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): assert isinstance(file_id, str) return self._byid.has_key(file_id) if __name__ == '__main__': import doctest, inventory doctest.testmod(inventory) M 644 inline bzrlib/revision.py data 2830 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from xml import XMLMixin try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement class Revision(XMLMixin): """Single revision on a branch. Revisions may know their revision_hash, but only once they've been written out. This is not stored because you cannot write the hash into the file it describes. :todo: Perhaps make predecessor be a child element, not an attribute? """ def __init__(self, **args): self.inventory_id = None self.revision_id = None self.timestamp = None self.message = None self.timezone = None self.__dict__.update(args) def __repr__(self): if self.revision_id: return "" % self.revision_id def to_element(self): root = Element('revision', committer = self.committer, timestamp = '%.9f' % self.timestamp, revision_id = self.revision_id, inventory_id = self.inventory_id, timezone = str(self.timezone)) if self.precursor: root.set('precursor', self.precursor) root.text = '\n' msg = SubElement(root, 'message') msg.text = self.message msg.tail = '\n' return root def from_element(cls, elt): # is deprecated... if elt.tag not in ('revision', 'changeset'): bailout("unexpected tag in revision file: %r" % elt) cs = cls(committer = elt.get('committer'), timestamp = float(elt.get('timestamp')), precursor = elt.get('precursor'), revision_id = elt.get('revision_id'), inventory_id = elt.get('inventory_id')) v = elt.get('timezone') cs.timezone = v and int(v) cs.message = elt.findtext('message') # text of return cs from_element = classmethod(from_element) M 644 inline bzrlib/xml.py data 1451 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """XML externalization support.""" __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement import os, time from trace import mutter class XMLMixin: def to_element(self): raise Exception("XMLMixin.to_element must be overridden in concrete classes") def write_xml(self, f): ElementTree(self.to_element()).write(f, 'utf-8') f.write('\n') def read_xml(cls, f): return cls.from_element(ElementTree().parse(f)) read_xml = classmethod(read_xml) commit refs/heads/master mark :28 committer Martin Pool 1110617652 +1100 data 103 doc: notes on implementing codeville-style merge on top of a weave; looks nice but opens a can of worms from :27 M 644 inline doc/compared-codeville.txt data 4145 Codeville ********* Documentation on how this actually works is pretty scarce to say the least. I *think* I understand their merge algorithm though, and it's pretty clever. Basically we do a two-way merge between annotated forms of the two files: that is, with each line marked with the revision in which it last changed. (I am simplifying here by speaking of lines and changes, but I don't think it causes any essential problem.) Now we walk through each file, line by line. If the change that introduced the line state in branch A is already merged into branch B, then we can just take B. Now is this actually better? It may be better in several ways: * Do not need to choose just a single ancestor, but rather can take advantage of all possible previous changes. * Can handle OTHER containing changes which have been merged into THIS, but have then been overwritten. * Can handle cherrypicks(!) by remembering which lines came in from that cherrypick; then we don't need to merge them again. Some questions: * Do we actually need to store the annotations, or can we just infer it at the time we do the merge? * Can this be accomodated in something like an SCCS weave format? I think something like a weave may work, in as much as it is basically a concatenation of annotations, but I don't know if it represents merges well enough to cope. Can this handle binaries or type-specific merges, and if so how? Unmergeable binaries are easy: just get the user to pick one. Things like XML are harder; we probably need to punt out to a type-specific three-way merge. Of course this approach does not forbid also offering a 3-way merge. ---- I suppose this could be accomodated by an annotation cache on top of plain history storage, or by using a storage format such as a weave that can efficiently produce annotation information. That is to say there is nothing inherently necessary about remembering the line history at the point when it is committed, except that it might be more efficient to do this once and remember it than to ---- There is another interesting approach that can be used even in a tool that does not inherently remember annotations: Given two files to merge, find all regions of difference. For each such, try to find a common ancestor having the same content for the region. Subdivide the region if necessary. This naive approach is probably infeasible, since it would mean checking every possible predecessor. ---- Rather than storing or calculating annotations, we could try using a complex weave, which allows one file version to be represented as a weave of multiple disjoint previous versions. It sounds complex but it might work. Essentially we store each file as a selection of lines that should be turned on in that file. These files might come from any of the predecessors that were merged into that file. Complex to get right but it might work. This is written in terms of lines, but it might make more sense to just use byte ranges: perhaps more efficient when handling long files, and makes binaries less of a special case. codeville in fact does *not* seem to do this, though to me it seems like a fairly natural corollary of their design. This seems to imply holding the file text and ancestry of every branch that ever merged into this one, rather than just finding them if we later want them. Hm. That is nice in terms of doing smart merges. That possibly causes trouble in terms of having a name for these branches floating around inside our space, and making sure we don't clash with them. It may make sense in terms of having a working directory be just a view into a shared database, looking at a particular line of development. Indeed the main difficulty seems to be of naming branches in this space. Perhaps we should move back to using repositories and named branches within them, but not rely on branch names being unique out of the context of a single repository. Wow, this seems to open a big can of worms. ---- So the conclusion is that this is very cool, but it does not require a fundamental change of model and can be implemented later. commit refs/heads/master mark :29 committer Martin Pool 1110691371 +1100 data 55 When removing files, new status should be I or ?, not D from :28 M 644 inline bzrlib/branch.py data 26316 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset from store import ImmutableStore from revision import Revision from errors import bailout from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. :todo: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. :todo: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. :todo: Keep the on-disk branch locked while the object exists. :todo: mkdir() method. """ def __init__(self, base, init=False): """Create new branch object at a particular location. :param base: Base directory for the branch. :param init: If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ self.base = os.path.realpath(base) if init: self._make_control() else: if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def _rel(self, name): """Return filename relative to branch top""" return os.path.join(self.base, name) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch""" return file(self.controlfilename(file_or_path), mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # read in binary mode to detect newline wierdness. fmt = self.controlfile('branch-format', 'rb').read() if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() inv = Inventory.read_xml(self.controlfile('inventory', 'r')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'w') inv.write_xml(tmpf) tmpf.close() os.rename(tmpfname, self.controlfilename('inventory')) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. This puts the files in the Added state, so that they will be recorded by the next commit. :todo: Perhaps have an option to add the ids even if the files do not (yet) exist. :todo: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. :todo: Option to specify file id. :todo: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self._rel(f)) if isfile(fullpath): kind = 'file' elif isdir(fullpath): kind = 'directory' else: bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if len(fp) > 1: parent_name = joinpath(fp[:-1]) mutter("lookup parent %r" % parent_name) parent_id = inv.path2id(parent_name) if parent_id == None: bailout("cannot add: parent %r is not versioned" % joinpath(fp[:-1])) else: parent_id = None file_id = _gen_file_id(fp[-1]) inv.add(InventoryEntry(file_id, fp[-1], kind=kind, parent_id=parent_id)) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r parent_id={%s}" % (f, file_id, kind, parent_id)) self._write_inventory(inv) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on :todo: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True :todo: Do something useful with directories. :todo: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. :param timestamp: if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self._rel(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = _gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'R' else: state = 'M' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) mutter("append to revision-history") self.controlfile('revision-history', 'at').write(rev_id + '\n') mutter("done!") def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. :todo: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: bailout("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original'): """Write out human-readable log of commits to this branch :param utc: If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l revno += 1 precursor = p def show_status(branch, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b._rel('foo')) >>> b.show_status() D foo :todo: Get state for single files. :todo: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = branch.basis_tree() old_inv = old.inventory new = branch.working_tree() new_inv = new.inventory for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("wierd file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files = []): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def _gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" assert '/' not in name while name[0] == '.': name = name[1:] s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :30 committer Martin Pool 1110691736 +1100 data 20 remove dead commands from :29 M 644 inline bzr.py data 22588 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. """ # not currently working: # bzr check # Run internal consistency checks. # bzr info # Show some information about this branch. __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ###################################################################### # check status def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files. Fails if the files are already added. """ Branch('.').add(file_list, verbose=verbose) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): b = Branch('.') print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl is not None: return pl else: return 's' count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %5d %s' % (count_status[fs], name) print ' %5d versioned subdirector%s' % (count_version_dirs, plural(count_version_dirs, 'y', 'ies')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %5d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %5d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %5d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) def cmd_remove(file_list, verbose=False): Branch('.').remove(file_list, verbose=verbose) def cmd_file_id(filename): i = Branch('.').read_working_inventory().path2id(filename) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message, verbose=False): Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_doctest(): """Run internal doctest suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option import bzr, doctest, bzrlib.store bzrlib.trace.verbose = False doctest.testmod(bzr) doctest.testmod(bzrlib.store) doctest.testmod(bzrlib.inventory) doctest.testmod(bzrlib.branch) doctest.testmod(bzrlib.osutils) doctest.testmod(bzrlib.tree) # more strenuous tests; import bzrlib.tests doctest.testmod(bzrlib.tests) ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('bzr --help'.split()) ([], {'help': True}) >>> parse_args('bzr --version'.split()) ([], {'version': True}) >>> parse_args('bzr status --all'.split()) (['status'], {'all': True}) >>> parse_args('bzr commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. for ap in argform: argname = ap[:-1] if ap[-1] == '?': assert 0 elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. try: t = bzrlib.trace._tracefile t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % format_date(time.time())) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.gethostname())) t.write(' arguments: %r\n' % argv) starttime = os.times()[4] import platform t.write(' platform: %s\n' % platform.platform()) t.write(' python: %s\n' % platform.python_version()) ret = run_bzr(argv) times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum" % times[:4]) mutter(" %.3f elapsed" % (times[4] - starttime)) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :31 committer Martin Pool 1110691953 +1100 data 25 fix up parse_args doctest from :30 M 644 inline bzr.py data 22572 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. """ # not currently working: # bzr check # Run internal consistency checks. # bzr info # Show some information about this branch. __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ###################################################################### # check status def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files. Fails if the files are already added. """ Branch('.').add(file_list, verbose=verbose) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): b = Branch('.') print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl is not None: return pl else: return 's' count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %5d %s' % (count_status[fs], name) print ' %5d versioned subdirector%s' % (count_version_dirs, plural(count_version_dirs, 'y', 'ies')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %5d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %5d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %5d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) def cmd_remove(file_list, verbose=False): Branch('.').remove(file_list, verbose=verbose) def cmd_file_id(filename): i = Branch('.').read_working_inventory().path2id(filename) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message, verbose=False): Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_doctest(): """Run internal doctest suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option import bzr, doctest, bzrlib.store bzrlib.trace.verbose = False doctest.testmod(bzr) doctest.testmod(bzrlib.store) doctest.testmod(bzrlib.inventory) doctest.testmod(bzrlib.branch) doctest.testmod(bzrlib.osutils) doctest.testmod(bzrlib.tree) # more strenuous tests; import bzrlib.tests doctest.testmod(bzrlib.tests) ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. for ap in argform: argname = ap[:-1] if ap[-1] == '?': assert 0 elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. try: t = bzrlib.trace._tracefile t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % format_date(time.time())) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.gethostname())) t.write(' arguments: %r\n' % argv) starttime = os.times()[4] import platform t.write(' platform: %s\n' % platform.platform()) t.write(' python: %s\n' % platform.python_version()) ret = run_bzr(argv) times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum" % times[:4]) mutter(" %.3f elapsed" % (times[4] - starttime)) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :32 committer 1110692333 +1100 data 66 - Split commands into bzrlib.commands - main bzr.py now very small from :31 R bzr.py bzrlib/commands.py M 644 inline bzr.py data 915 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, bzrlib, bzrlib.commands if __name__ == '__main__': sys.exit(bzrlib.commands.main(sys.argv)) commit refs/heads/master mark :33 committer 1110692878 +1100 data 37 fix up doctest for code rearrangement from :32 M 644 inline bzrlib/tests.py data 4742 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # XXX: We might prefer these to be in a text file rather than Python # source, but that only works in doctest from Python 2.4 and later, # which is not present in Warty. r""" Bazaar-NG test cases ******************** These are run by ``bzr.doctest``. >>> import bzr, bzrlib, os >>> bzrlib.commands.cmd_rocks() it sure does! Hey, nice place to begin. The basic object is a Branch. We have a special helper class ScratchBranch that automatically makes a directory and cleans itself up, but is in other respects identical. ScratchBranches are initially empty: >>> b = bzrlib.ScratchBranch() >>> b.show_status() New files in that directory are, it is initially unknown: >>> file(b.base + '/hello.c', 'wt').write('int main() {}') >>> b.show_status() ? hello.c That's not quite true; some files (like editor backups) are ignored by default: >>> file(b.base + '/hello.c~', 'wt').write('int main() {}') >>> b.show_status() ? hello.c >>> list(b.unknowns()) ['hello.c'] The ``add`` command marks a file to be added in the next revision: >>> b.add('hello.c') >>> b.show_status() A hello.c You can also add files that otherwise would be ignored. The ignore patterns only apply to files that would be otherwise unknown, so they have no effect once it's added. >>> b.add('hello.c~') >>> b.show_status() A hello.c A hello.c~ It is an error to add a file that isn't present in the working copy: >>> b.add('nothere') Traceback (most recent call last): ... BzrError: ('cannot add: not a regular file or directory: nothere', []) If we add a file and then change our mind, we can either revert it or remove the file. If we revert, we are left with the working copy (in either I or ? state). If we remove, the working copy is gone. Let's do that to the backup, presumably added accidentally. >>> b.remove('hello.c~') >>> b.show_status() A hello.c Now to commit, creating a new revision. (Fake the date and name for reproducibility.) >>> b.commit('start hello world', timestamp=0, committer='foo@nowhere') >>> b.show_status() >>> b.show_status(show_all=True) . hello.c I hello.c~ We can look back at history >>> r = b.get_revision(b.lookup_revision(1)) >>> r.message 'start hello world' >>> b.write_log(show_timezone='utc') ---------------------------------------- revno: 1 committer: foo@nowhere timestamp: Thu 1970-01-01 00:00:00 +0000 message: start hello world (The other fields will be a bit unpredictable, depending on who ran this test and when.) As of 2005-02-21, we can also add subdirectories to the revision! >>> os.mkdir(b.base + "/lib") >>> b.show_status() ? lib/ >>> b.add('lib') >>> b.show_status() A lib/ >>> b.commit('add subdir') >>> b.show_status() >>> b.show_status(show_all=True) . hello.c I hello.c~ . lib/ and we can also add files within subdirectories: >>> file(b.base + '/lib/hello', 'w').write('hello!\n') >>> b.show_status() ? lib/hello Tests for adding subdirectories, etc. >>> b = bzrlib.branch.ScratchBranch() >>> os.mkdir(b._rel('d1')) >>> os.mkdir(b._rel('d2')) >>> os.mkdir(b._rel('d2/d3')) >>> list(b.working_tree().unknowns()) ['d1', 'd2'] Create some files, but they're not seen as unknown yet: >>> file(b._rel('d1/f1'), 'w').close() >>> file(b._rel('d2/f2'), 'w').close() >>> file(b._rel('d2/f3'), 'w').close() >>> [v[0] for v in b.inventory.directories()] [''] >>> list(b.working_tree().unknowns()) ['d1', 'd2'] Adding a directory, and we see the file underneath: >>> b.add('d1') >>> [v[0] for v in b.inventory.directories()] ['', 'd1'] >>> list(b.working_tree().unknowns()) ['d1/f1', 'd2'] >>> # d2 comes first because it's in the top directory >>> b.add('d2') >>> b.commit('add some stuff') >>> list(b.working_tree().unknowns()) ['d1/f1', 'd2/d3', 'd2/f2', 'd2/f3'] """ commit refs/heads/master mark :34 committer 1110694462 +1100 data 3 doc from :33 M 644 inline bzrlib/commands.py data 22645 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. """ # not currently working: # bzr info # Show some information about this branch. __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files. Fails if the files are already added. """ Branch('.').add(file_list, verbose=verbose) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): b = Branch('.') print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl is not None: return pl else: return 's' count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %5d %s' % (count_status[fs], name) print ' %5d versioned subdirector%s' % (count_version_dirs, plural(count_version_dirs, 'y', 'ies')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %5d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %5d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %5d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) def cmd_remove(file_list, verbose=False): Branch('.').remove(file_list, verbose=verbose) def cmd_file_id(filename): i = Branch('.').read_working_inventory().path2id(filename) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message, verbose=False): Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_doctest(): """Run internal doctest suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option import bzr, doctest, bzrlib.store bzrlib.trace.verbose = False doctest.testmod(bzr) doctest.testmod(bzrlib.store) doctest.testmod(bzrlib.inventory) doctest.testmod(bzrlib.branch) doctest.testmod(bzrlib.osutils) doctest.testmod(bzrlib.tree) # more strenuous tests; import bzrlib.tests doctest.testmod(bzrlib.tests) ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. for ap in argform: argname = ap[:-1] if ap[-1] == '?': assert 0 elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. try: t = bzrlib.trace._tracefile t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % format_date(time.time())) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.gethostname())) t.write(' arguments: %r\n' % argv) starttime = os.times()[4] import platform t.write(' platform: %s\n' % platform.platform()) t.write(' python: %s\n' % platform.python_version()) ret = run_bzr(argv) times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum" % times[:4]) mutter(" %.3f elapsed" % (times[4] - starttime)) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :35 committer Martin Pool 1110715611 +1100 data 42 Notes from Steve Berczuk on purpose of SCM from :34 M 644 inline doc/purpose.txt data 3138 What is the purpose of version control? *************************************** There are several overlapping purposes: * Allowing concurrent development by several people. Primarily, helping resolve changes when they are integrated, but also making each person aware of what the others have been doing, allowing them to talk about changes, etc. * To allow different lines of development, with sensible reintegration. e.g. stable/development, or branches for experimental features. * To record a history of development, so that you can find out why a feature went in or who added it, or which releases might be affected by a bug. * In particular, as a *record of decisions* about the project made by the developers. * Help in writing a gloss on that history; not simply a record of events but an explanation of what happened and why. Such a record may need to be written at several levels: a very detailed explanation of changes to a function; a note that a bug was fixed and why; and then a brief NEWS file for the whole release. While past events can never change, the intepretation placed upon them may change, even after the event. For example, even after a release has gone out, one might want to go back and note that a bug was actually fixed. The system is helping the developers tell a story about the development of the project. * As an aid to thinking about the project. (Much as a personal journal is not merely a record or even analysis of events, but also a chance to reflect and to think of the future.) People can review diffs, and then write a description of what changed, and in doing so perhaps realize something else they should do, or realize they made a mistake. Making branches helps work out the order of feature integration and the stability of different lines of development. * As an 'undo' protection mechanism. This is one reason why version control can be useful on projects that have only a single developer and never branch. * Incidentally, as a backup mechanism. Version control systems, particularly distributed systems, tend to cause code to exist on several machines, which gives some protection against loss of any single copy. It's still a good idea to use a separate backup system as well. * As a file-sharing mechanism: even with just a single developer and line of development it can be useful to keep files synchronized between several machines, which may not always be connected. ---- Steve Berczuk says__: __ http://www.bell-labs.com/cgi-user/OrgPatterns/OrgPatterns?ConfigurationManagementPatterns A successful configuration management process allows: * Developers to work together on a project, sharing common code. * Developers to share development effort on a module. * Developers to have access to the current stable (tested) version of a system. * The ability to back up to a previous stable version (one of a number of NamedStableBases), of a system * The ability of a developer to checkpoint changes to a module and to back off to a previous version of that module commit refs/heads/master mark :36 committer Martin Pool 1110768571 +1100 data 3 doc from :35 M 644 inline doc/random.txt data 8337 I think Ruby's point is right: we need to think about how a tool *feels* as you're using it. Making regular commits gives a nice rhythm to to working; in some ways it's nicer to just commit single files with C-x v v than to build complex changesets. (See gmane.c.v-c.arch.devel post 19 Nov, Tom Lord.) * Would like to generate an activity report, to e.g. mail to your boss or post to your blog. "What did I change today, across all these specified branches?" * It is possibly nice that tla by default forbids you from committing if emacs autosave or lock files exist -- I find it confusing to commit somethin other than what is shown in the editor window because there are unsaved changes. However, grumbling about unknown files is annoying, and requiring people to edit regexps in the id-tagging-method file to fix it is totally unreasonable. Perhaps there should be a preference to abort on unknown files, or perhaps it should be possible to specify forbidden files. Perhaps this is related to a mechanism to detect conflicted files: should refuse to commit if there are any .rej files lying around. *Those who lose history are doomed to recreate it.* -- broked (on #gnu.arch.users) *A universal convention supplies all of maintainability, clarity, consistency, and a foundation for good programming habits too. What it doesn't do is insist that you follow it against your will. That's Python!* -- Tim Peters on comp.lang.python, 2001-06-16 (Bazaar provides mechanism and convention, but it is up to you whether you wish to follow or enforce that convention.) ---- jblack asks for A way to subtract merges, so that you can see the work you've done to a branch since conception. ---- :: now that is a neat idea: advertise branches over zeroconf should make lca fun :-) ---- http://thedailywtf.com/ShowPost.aspx?PostID=24281 Source control is necessary and useful, but in a team of one (or even two) people the setup overhead isn't always worth it--especially if you're going to join source control in a month, and you don't want to have to migrate everything out of your existing (in my case, skunkworks) system before you can use it. At least that was my experience--I putzed with CVS a bit and knew other source control systems pretty well, but in the day-to-day it wasn't worth the bother (granted, I was a bit offended at having to wait to use the mainline source control, but that's another matter). I think Bazaar-NG will have such low setup overhead (just ``init``, ``add``) that it can be easily used for even tiny projects. The ability to merge previously-unrelated trees means they can fold their project in later. ---- From tridge: * cope without $EMAIL better * notes at start of .bzr.log: * you can delete this * or include it in bug reports * should you be able to remove things from the default ignore list? * headers at start of diff, giving some comments, perhaps dates * is diff against /dev/null really OK? I think so. * separate remove/delete commands? * detect files which were removed and now in 'missing' state * should we actually compare files for 'status', or check mtime and size; reading every file in the samba source tree can take a long time. without this, doing a status on a large tree can be very slow. but relying on mtime/size is a bit dangerous. people really do work on trees which take a large chunk of memory and which will not stay in memory * status up-to-date files: not 'U', and don't list without --all * if status does compare file text, then it should be quick when checking just a single file * wrapper for svn that every time run logs - command - all inputs - time it took - sufficient to replay everything - record all files * status crashes if a file is missing * option for -p1 level on diff, etc. perhaps * commit without message should start $EDITOR * don't duplicate all files on commit * start importing tridge-junkcode * perhaps need xdelta storage sooner rather than later, to handle very large file ---- The first operation most people do with a new version-control system is *not* making their own project, but rather getting a checkout of an existing project, building it, and possibly submitting a patch. So those operations should be *extremely* easy. ---- * Way to check that a branch is fully merged, and no longer needed: should mean all its changes have been integrated upstream, no uncommitted changes or rejects or unknown files. * Filter revisions by containing a particular word (as for log). Perhaps have key-value fields that might be used for e.g. line-of-development or bug nr? * List difference in the revisions on one branch vs another. * Perhaps use a partially-readable but still hopefully unique ID for revisions/inventories? * Preview what will happen in a merge before it is applied * When a changeset deletes a file, should have the option to just make it unknown/ignored. Perhaps this is best handled by an interactive merge. If the file is unchanged locally and deleted remotely, it will by default be deleted (but the user has the option to reject the delete, or to make it just unversioned, or to save a copy.) If it is modified locall then the user still needs to choose between those options but there is no default (or perhaps the default is to reject the delete.) * interactive commit, prompting whether each hunk should be sent (as for darcs) * Write up something about detection of unmodified files * Preview a merge so as to get some idea what will happen: * What revisions will be merged (log entries, etc) * What files will be affected? * Are those simple updates, or have they been updated locally as well. * Any renames or metadata clashes? * Show diffs or conflict markers. * Do the merge, but write into a second directory. * "Show me all changesets that touch this file" Can be done by walking back through all revisions, and filtering out those where the file-id either gets a new name or a new text. * Way to commit backdated revisions or pretend to be something by someone else, for the benefit of import tools; in general allow everything taken from the current environment to be overridden. * Cope well when trying to checkout or update over a flaky connection. Passive HTTP possibly helps with this: we can fetch all the file texts first, then the inventory, and can even retry interrupted connections. * Use readline for reading log messages, and store a history of previous commit messages! * Warn when adding huge files(?) - more than say 10MB? On the other hand, why not just cope? ---- 20050218090900.GA2071@opteron.random Subject: Re: [darcs-users] Re: [BK] upgrade will be needed From: Andrea Arcangeli Newsgroups: gmane.linux.kernel Date: Fri, 18 Feb 2005 10:09:00 +0100 On Thu, Feb 17, 2005 at 06:24:53PM -0800, Tupshin Harper wrote: > small to medium sized ones). Last I checked, Arch was still too slow in > some areas, though that might have changed in recent months. Also, many IMHO someone needs to rewrite ARCH using the RCS or SCCS format for the backend and a single file for the changesets and with sane parameters conventions miming SVN. The internal algorithms of arch seems the most advanced possible. It's just the interface and the fs backend that's so bad and doesn't compress in the backups either. SVN bsddb doesn't compress either by default, but at least the new fsfs compresses pretty well, not as good as CVS, but not as badly as bsddb and arch either. I may be completely wrong, so take the above just as a humble suggestion. darcs scares me a bit because it's in haskell, I don't believe very much in functional languages for compute intensive stuff, ram utilization skyrockets sometime (I wouldn't like to need >1G of ram to manage the tree). Other languages like python or perl are much slower than C/C++ too but at least ram utilization can be normally dominated to sane levels with them and they can be greatly optimized easily with C/C++ extensions of the performance critical parts. commit refs/heads/master mark :37 committer Martin Pool 1110768878 +1100 data 53 note dependency on elementtree thanks to Faheem Mitha from :36 M 644 inline README data 720 Release notes for Bazaar-NG (pre-0) mbp@sourcefrog.net, November 2004, London * There is little locking or transaction control here; if you interrupt it the tree may be arbitrarily broken. This will be fixed. * Don't use this for critical data; at the very least keep separate regular snapshots of your tree. Dependencies ------------ This is mostly developed on Linux (Ubuntu); it should work on Unix, Windows, or OS X with relatively little trouble. bzr requires a fairly recent Python, say after 2.2. 2.4 is recommended. You must install either cElementTree_ or ElementTree_ first. .. _cElementTree: http://effbot.org/zone/celementtree.htm .. _ElementTree: http://effbot.org/zone/element-index.htm commit refs/heads/master mark :38 committer Martin Pool 1110768939 +1100 data 21 thanks to more people from :37 M 644 inline doc/thanks.txt data 385 ****** Thanks ****** Inspiration, suggestions, advice, bug reports --------------------------------------------- * Mark Shuttleworth * Robert Collins * David Allouche * Robert Weir * James Blackwell * Garrett Rooney * Andrew Tridgell * Paul Russell * Aaron Bentley * Faheem Mitha * Luke Gorrie * Dan Nicolaescu Sponsor ------- * Canonical_ .. _Canonical: http://www.canonical.com/ commit refs/heads/master mark :39 committer Martin Pool 1110769057 +1100 data 43 make doc index consistent with new web page from :38 M 644 inline doc/index.txt data 5753 Bazaar-NG ********* .. These documents are formatted as ReStructuredText. You can .. .. convert them to HTML, PDF, etc using the ``python-docutils`` .. .. package. .. *Bazaar-NG* (``bzr``) is a project of `Canonical Ltd`__ to develop an open source distributed version control system that is powerful, friendly, and scalable. The project is at an early stage of development. __ http://canonical.com/ **Note:** These documents are in a very preliminary state, and so may be internally or externally inconsistent or redundant. Comments are still very welcome. Please send them to . For more information, see the homepage at http://bazaar-ng.org/ User documentation ------------------ * `Project overview/introduction `__ * `Command reference `__ -- intended to be user documentation, and gives the best overview at the moment of what the system will feel like to use. Fairly complete. * `Quick reference `__ -- single page description of how to use, intended to check it's adequately simple. Incomplete. * `FAQ `__ -- mostly user-oriented FAQ. Requirements and general design ------------------------------- * `Various purposes of a VCS `__ -- taking snapshots and helping with merges is not the whole story. * `Requirements `__ * `Costs `__ of various factors: time, disk, network, etc. * `Deadly sins `__ that gcc maintainers suggest we avoid. * `Overview of the whole design `__ and miscellaneous small design points. * `File formats `__ * `Random observations `__ that don't fit anywhere else yet. Design of particular features ----------------------------- * `Automatic generation of ChangeLogs `__ * `Cherry picking `__ -- merge just selected non-contiguous changes from a branch. * `Common changeset format `__ for interchange format between VCS. * `Compression `__ of file text for more efficient storage. * `Config specs `__ assemble a tree from several places. * `Conflicts `_ that can occur during merge-like operations. * `Recovering from interrupted operations `__ * `Inventory command `__ * `Branch joins `__ represent that all the changes from one branch are integrated into another. * `Kill a version `__ to fix a broken commit or wrong message, or to remove confidential information from the history. * `Hash collisions `__ and weaknesses, and the security implications thereof. * `Layers `__ within the design * `Library interface `__ for Python. * `Merge `__ * `Mirroring `__ * `Optional edit command `__: sometimes people want to make the working copy read-only, or not present at all. * `Partial commits `__ * `Patch pools `__ to efficiently store related branches. * `Revision syntax `__ -- ``hello.c@12``, etc. * `Roll-up commits `__ -- a single revision incorporates the changes from several others. * `Scalability `__ * `Security `__ * `Shared branches `__ maintained by more than one person * `Supportability `__ -- how to handle any bugs or problems in the field. * `Place tags on revisions for easy reference `__ * `Detecting unchanged files `__ * `Merging previously-unrelated branches `__ * `Usability principles `__ (very small at the moment) * ``__ * ``__ * ``__ Modelling/controlling flow of patches. * ``__ -- Discussion of using YAML_ as a storage or transmission format. .. _YAML: http://www.yaml.org/ Comparisons to other systems ---------------------------- * `Taxonomy `__: basic questions a VCS must answer. * `Bitkeeper `__, the proprietary system used by some kernel developers. * `Aegis `__, a tool focussed on enforcing process and workflow. * `Codeville `__ has an intruiging but scarcely-documented merge algorithm. * `CVSNT `__, with more Windows support and some merge enhancements. * `OpenCM `__, another hash-based tool with a good whitepaper. * `PRCS `__, a non-distributed inventory-based tool. * `GNU Arch `__, with many pros and cons. * `Darcs `__, a merge-focussed tool with good usability. * `Quilt `__ -- Andrew Morton's patch scripts, popular with kernel maintainers. * `Monotone `__, Graydon Hoare's hash-based distributed system. * `SVK `__ -- distributed operation stacked on Subversion. * `Sun Teamware `__ Project management and organization ----------------------------------- * `Development news `__ * `Notes on how to get a VCS adopted `__ * `Testing plan `__ -- very sketchy. * `Thanks `__ to various people * `Roadmap `__: High-level order for implementing features. * ``__: current tasks. * `Extra commands `__ for internal/developer/debugger use. * `Choice of Python as a development language `__ commit refs/heads/master mark :40 committer Martin Pool 1110769141 +1100 data 47 upload docs to new location under main web site from :39 M 644 inline doc/Makefile data 220 %.html: %.txt rest2html $^ > $@.tmp && mv $@.tmp $@ all: $(addsuffix .html,$(basename $(wildcard *.txt))) upload: all (echo 'cd www/doc'; ls *html | sed -e 's/^/put /') | sftp -b /dev/fd/0 bazng@escudero.ubuntu.com commit refs/heads/master mark :41 committer Martin Pool 1110769264 +1100 data 25 more release-note updates from :40 M 644 inline README data 810 *********************************** Release notes for Bazaar-NG (pre-0) *********************************** mbp@sourcefrog.net, March 2005, Canberra Caveats ------- * There is little locking or transaction control here; if you interrupt it the tree may be arbitrarily broken. This will be fixed. * Don't use this for critical data; at the very least keep separate regular snapshots of your tree. Dependencies ------------ This is mostly developed on Linux (Ubuntu); it should work on Unix, Windows, or OS X with relatively little trouble. bzr requires a fairly recent Python, say after 2.2. 2.4 is recommended. You must install either cElementTree_ or ElementTree_ first. .. _cElementTree: http://effbot.org/zone/celementtree.htm .. _ElementTree: http://effbot.org/zone/element-index.htm commit refs/heads/master mark :42 committer Martin Pool 1110774914 +1100 data 28 human-assigned revision ids? from :41 M 644 inline doc/random.txt data 8468 I think Ruby's point is right: we need to think about how a tool *feels* as you're using it. Making regular commits gives a nice rhythm to to working; in some ways it's nicer to just commit single files with C-x v v than to build complex changesets. (See gmane.c.v-c.arch.devel post 19 Nov, Tom Lord.) * Would like to generate an activity report, to e.g. mail to your boss or post to your blog. "What did I change today, across all these specified branches?" * It is possibly nice that tla by default forbids you from committing if emacs autosave or lock files exist -- I find it confusing to commit somethin other than what is shown in the editor window because there are unsaved changes. However, grumbling about unknown files is annoying, and requiring people to edit regexps in the id-tagging-method file to fix it is totally unreasonable. Perhaps there should be a preference to abort on unknown files, or perhaps it should be possible to specify forbidden files. Perhaps this is related to a mechanism to detect conflicted files: should refuse to commit if there are any .rej files lying around. *Those who lose history are doomed to recreate it.* -- broked (on #gnu.arch.users) *A universal convention supplies all of maintainability, clarity, consistency, and a foundation for good programming habits too. What it doesn't do is insist that you follow it against your will. That's Python!* -- Tim Peters on comp.lang.python, 2001-06-16 (Bazaar provides mechanism and convention, but it is up to you whether you wish to follow or enforce that convention.) ---- jblack asks for A way to subtract merges, so that you can see the work you've done to a branch since conception. ---- :: now that is a neat idea: advertise branches over zeroconf should make lca fun :-) ---- http://thedailywtf.com/ShowPost.aspx?PostID=24281 Source control is necessary and useful, but in a team of one (or even two) people the setup overhead isn't always worth it--especially if you're going to join source control in a month, and you don't want to have to migrate everything out of your existing (in my case, skunkworks) system before you can use it. At least that was my experience--I putzed with CVS a bit and knew other source control systems pretty well, but in the day-to-day it wasn't worth the bother (granted, I was a bit offended at having to wait to use the mainline source control, but that's another matter). I think Bazaar-NG will have such low setup overhead (just ``init``, ``add``) that it can be easily used for even tiny projects. The ability to merge previously-unrelated trees means they can fold their project in later. ---- From tridge: * cope without $EMAIL better * notes at start of .bzr.log: * you can delete this * or include it in bug reports * should you be able to remove things from the default ignore list? * headers at start of diff, giving some comments, perhaps dates * is diff against /dev/null really OK? I think so. * separate remove/delete commands? * detect files which were removed and now in 'missing' state * should we actually compare files for 'status', or check mtime and size; reading every file in the samba source tree can take a long time. without this, doing a status on a large tree can be very slow. but relying on mtime/size is a bit dangerous. people really do work on trees which take a large chunk of memory and which will not stay in memory * status up-to-date files: not 'U', and don't list without --all * if status does compare file text, then it should be quick when checking just a single file * wrapper for svn that every time run logs - command - all inputs - time it took - sufficient to replay everything - record all files * status crashes if a file is missing * option for -p1 level on diff, etc. perhaps * commit without message should start $EDITOR * don't duplicate all files on commit * start importing tridge-junkcode * perhaps need xdelta storage sooner rather than later, to handle very large file ---- The first operation most people do with a new version-control system is *not* making their own project, but rather getting a checkout of an existing project, building it, and possibly submitting a patch. So those operations should be *extremely* easy. ---- * Way to check that a branch is fully merged, and no longer needed: should mean all its changes have been integrated upstream, no uncommitted changes or rejects or unknown files. * Filter revisions by containing a particular word (as for log). Perhaps have key-value fields that might be used for e.g. line-of-development or bug nr? * List difference in the revisions on one branch vs another. * Perhaps use a partially-readable but still hopefully unique ID for revisions/inventories? * Preview what will happen in a merge before it is applied * When a changeset deletes a file, should have the option to just make it unknown/ignored. Perhaps this is best handled by an interactive merge. If the file is unchanged locally and deleted remotely, it will by default be deleted (but the user has the option to reject the delete, or to make it just unversioned, or to save a copy.) If it is modified locall then the user still needs to choose between those options but there is no default (or perhaps the default is to reject the delete.) * interactive commit, prompting whether each hunk should be sent (as for darcs) * Write up something about detection of unmodified files * Preview a merge so as to get some idea what will happen: * What revisions will be merged (log entries, etc) * What files will be affected? * Are those simple updates, or have they been updated locally as well. * Any renames or metadata clashes? * Show diffs or conflict markers. * Do the merge, but write into a second directory. * "Show me all changesets that touch this file" Can be done by walking back through all revisions, and filtering out those where the file-id either gets a new name or a new text. * Way to commit backdated revisions or pretend to be something by someone else, for the benefit of import tools; in general allow everything taken from the current environment to be overridden. * Cope well when trying to checkout or update over a flaky connection. Passive HTTP possibly helps with this: we can fetch all the file texts first, then the inventory, and can even retry interrupted connections. * Use readline for reading log messages, and store a history of previous commit messages! * Warn when adding huge files(?) - more than say 10MB? On the other hand, why not just cope? * Perhaps allow people to specify a revision-id, much as people have unique but human-assigned names for patches at the moment? ---- 20050218090900.GA2071@opteron.random Subject: Re: [darcs-users] Re: [BK] upgrade will be needed From: Andrea Arcangeli Newsgroups: gmane.linux.kernel Date: Fri, 18 Feb 2005 10:09:00 +0100 On Thu, Feb 17, 2005 at 06:24:53PM -0800, Tupshin Harper wrote: > small to medium sized ones). Last I checked, Arch was still too slow in > some areas, though that might have changed in recent months. Also, many IMHO someone needs to rewrite ARCH using the RCS or SCCS format for the backend and a single file for the changesets and with sane parameters conventions miming SVN. The internal algorithms of arch seems the most advanced possible. It's just the interface and the fs backend that's so bad and doesn't compress in the backups either. SVN bsddb doesn't compress either by default, but at least the new fsfs compresses pretty well, not as good as CVS, but not as badly as bsddb and arch either. I may be completely wrong, so take the above just as a humble suggestion. darcs scares me a bit because it's in haskell, I don't believe very much in functional languages for compute intensive stuff, ram utilization skyrockets sometime (I wouldn't like to need >1G of ram to manage the tree). Other languages like python or perl are much slower than C/C++ too but at least ram utilization can be normally dominated to sane levels with them and they can be greatly optimized easily with C/C++ extensions of the performance critical parts. commit refs/heads/master mark :43 committer Martin Pool 1110775434 +1100 data 4 todo from :42 M 644 inline bzrlib/commands.py data 22924 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. """ # not currently working: # bzr info # Show some information about this branch. __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files. Fails if the files are already added. """ Branch('.').add(file_list, verbose=verbose) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): b = Branch('.') print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl is not None: return pl else: return 's' count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %5d %s' % (count_status[fs], name) print ' %5d versioned subdirector%s' % (count_version_dirs, plural(count_version_dirs, 'y', 'ies')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %5d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %5d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %5d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) def cmd_remove(file_list, verbose=False): Branch('.').remove(file_list, verbose=verbose) def cmd_file_id(filename): i = Branch('.').read_working_inventory().path2id(filename) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message, verbose=False): Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_doctest(): """Run internal doctest suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option import bzr, doctest, bzrlib.store bzrlib.trace.verbose = False doctest.testmod(bzr) doctest.testmod(bzrlib.store) doctest.testmod(bzrlib.inventory) doctest.testmod(bzrlib.branch) doctest.testmod(bzrlib.osutils) doctest.testmod(bzrlib.tree) # more strenuous tests; import bzrlib.tests doctest.testmod(bzrlib.tests) ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. for ap in argform: argname = ap[:-1] if ap[-1] == '?': assert 0 elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. try: # TODO: Lift into separate function in trace.py # TODO: Also show contents of /etc/lsb-release, if it can be parsed. # Perhaps that should eventually go into the platform library? # TODO: If the file doesn't exist, add a note describing it. t = bzrlib.trace._tracefile t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % format_date(time.time())) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.gethostname())) t.write(' arguments: %r\n' % argv) starttime = os.times()[4] import platform t.write(' platform: %s\n' % platform.platform()) t.write(' python: %s\n' % platform.python_version()) ret = run_bzr(argv) times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum" % times[:4]) mutter(" %.3f elapsed" % (times[4] - starttime)) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :44 committer Martin Pool 1110775596 +1100 data 29 show fqdn in tracefile header from :43 M 644 inline bzrlib/commands.py data 22920 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. """ # not currently working: # bzr info # Show some information about this branch. __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files. Fails if the files are already added. """ Branch('.').add(file_list, verbose=verbose) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): b = Branch('.') print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl is not None: return pl else: return 's' count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %5d %s' % (count_status[fs], name) print ' %5d versioned subdirector%s' % (count_version_dirs, plural(count_version_dirs, 'y', 'ies')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %5d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %5d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %5d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) def cmd_remove(file_list, verbose=False): Branch('.').remove(file_list, verbose=verbose) def cmd_file_id(filename): i = Branch('.').read_working_inventory().path2id(filename) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message, verbose=False): Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_doctest(): """Run internal doctest suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option import bzr, doctest, bzrlib.store bzrlib.trace.verbose = False doctest.testmod(bzr) doctest.testmod(bzrlib.store) doctest.testmod(bzrlib.inventory) doctest.testmod(bzrlib.branch) doctest.testmod(bzrlib.osutils) doctest.testmod(bzrlib.tree) # more strenuous tests; import bzrlib.tests doctest.testmod(bzrlib.tests) ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. for ap in argform: argname = ap[:-1] if ap[-1] == '?': assert 0 elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. try: # TODO: Lift into separate function in trace.py # TODO: Also show contents of /etc/lsb-release, if it can be parsed. # Perhaps that should eventually go into the platform library? # TODO: If the file doesn't exist, add a note describing it. t = bzrlib.trace._tracefile t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % format_date(time.time())) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.getfqdn())) t.write(' arguments: %r\n' % argv) starttime = os.times()[4] import platform t.write(' platform: %s\n' % platform.platform()) t.write(' python: %s\n' % platform.python_version()) ret = run_bzr(argv) times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum" % times[:4]) mutter(" %.3f elapsed" % (times[4] - starttime)) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :45 committer Martin Pool 1110784044 +1100 data 72 - add setup.py and install instructions - rename main script to just bzr from :44 R bzr.py bzr M 644 inline setup.py data 472 #! /usr/bin/env python # This is an installation script for bzr. Run it with # './setup.py install', or # './setup.py --help' for more options from distutils.core import setup setup(name='bzr', version='0.0.0', author='Martin Pool', author_email='mbp@sourcefrog.net', url='http://www.bazaar-ng.org/', description='Friendly distributed version control system', license='GNU GPL v2', packages=['bzrlib'], scripts=['bzr']) M 644 inline README data 1029 *********************************** Release notes for Bazaar-NG (pre-0) *********************************** mbp@sourcefrog.net, March 2005, Canberra Caveats ------- * There is little locking or transaction control here; if you interrupt it the tree may be arbitrarily broken. This will be fixed. * Don't use this for critical data; at the very least keep separate regular snapshots of your tree. Dependencies ------------ This is mostly developed on Linux (Ubuntu); it should work on Unix, Windows, or OS X with relatively little trouble. bzr requires a fairly recent Python, say after 2.2. 2.4 is recommended. You must install either cElementTree_ or ElementTree_ first. .. _cElementTree: http://effbot.org/zone/celementtree.htm .. _ElementTree: http://effbot.org/zone/element-index.htm Installation ------------ bzr has a standard python ``setup.py`` script. To install it system-wide:: # python ./setup.py install Alternatively you can simply put the source directory on $PYTHONPATH and $PATH. commit refs/heads/master mark :46 committer Martin Pool 1110786045 +1100 data 4 todo from :45 M 644 inline bzrlib/commands.py data 23227 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. """ # not currently working: # bzr info # Show some information about this branch. __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files. Fails if the files are already added. """ Branch('.').add(file_list, verbose=verbose) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): b = Branch('.') print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl is not None: return pl else: return 's' count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %5d %s' % (count_status[fs], name) print ' %5d versioned subdirector%s' % (count_version_dirs, plural(count_version_dirs, 'y', 'ies')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %5d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %5d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %5d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) def cmd_remove(file_list, verbose=False): Branch('.').remove(file_list, verbose=verbose) def cmd_file_id(filename): i = Branch('.').read_working_inventory().path2id(filename) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message, verbose=False): Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_doctest(): """Run internal doctest suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option import bzr, doctest, bzrlib.store bzrlib.trace.verbose = False doctest.testmod(bzr) doctest.testmod(bzrlib.store) doctest.testmod(bzrlib.inventory) doctest.testmod(bzrlib.branch) doctest.testmod(bzrlib.osutils) doctest.testmod(bzrlib.tree) # more strenuous tests; import bzrlib.tests doctest.testmod(bzrlib.tests) ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. for ap in argform: argname = ap[:-1] if ap[-1] == '?': assert 0 elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. try: # TODO: Lift into separate function in trace.py # TODO: Also show contents of /etc/lsb-release, if it can be parsed. # Perhaps that should eventually go into the platform library? # TODO: If the file doesn't exist, add a note describing it. t = bzrlib.trace._tracefile t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % format_date(time.time())) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.getfqdn())) t.write(' arguments: %r\n' % argv) starttime = os.times()[4] import platform t.write(' platform: %s\n' % platform.platform()) t.write(' python: %s\n' % platform.python_version()) ret = run_bzr(argv) times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum" % times[:4]) mutter(" %.3f elapsed" % (times[4] - starttime)) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :47 committer 1110863013 +1100 data 55 notes on more good bits from darcs: partial commit, etc from :46 M 644 inline doc/darcs.txt data 2339 Darcs compared to Arch ====================== Simpler to use; perhaps harder to completely understand. Always local; always fast. Patch commution is slow and perhaps doesn't clearly do what people want. Too slow! Can't reliably get back to any previous point. Explicitly not addressing source archive/librarian function. Loads everything into memory. Written in Haskell. A really simple pre-commit check hook is remarkably useful. http://www.scannedinavian.org/DarcsWiki/DifferencesFromArch Sometimes useful to be able to set email per-branch, for people who work on different projects under different personas. Token replace ------------- Very cute; possibly handy; not absolutely necessary in most places. Somewhat limited by the requirement that it be reversible. This is one of very few cases where it does seem necessary that we store deltas, rather than tree states. But that seems to cause other problems in terms of being able to reliably sign revisions. This can perhaps be inferred by a smart 3-way merge tool. Certainly you could have it do sub-line merges. Partial commit -------------- darcs allows you to commit only some of the changes to a single file. This is like the common feature of commiting only a subset of changed files, but taken to a higher level. It is useful more often than one might think: it is common to fix some documentation 'on the wing' and while strictly it should be in a separate commit it is not always worth the hassle to back out changes, fix the docs, then do the real change. Similarly for making a separate branch. Although the idea is very good, the current darcs implementation is limited to selecting by patch hunk, which means that neighbouring changes cannot be separated. Fixing this probably means having some kind of pluggable GUI to build the file-to-be-committed or an edited patch, possibly using something like meld, emacs, or dirdiff. Another approach some people might like is editing the diff file to chop out hunks. I don't think this needs to be on by default, as it is in darcs. It is usual to commit all the changes. For this to work safely, it is good to have a commit hook that builds/tests the tree. Of course this needs to be evaluated against the tree as it will be committed (taking account of partial commits), not the working tree. M 644 inline doc/thanks.txt data 398 ****** Thanks ****** Inspiration, suggestions, advice, bug reports --------------------------------------------- * Mark Shuttleworth * Robert Collins * David Allouche * Robert Weir * James Blackwell * Garrett Rooney * Andrew Tridgell * Paul Russell * Aaron Bentley * Faheem Mitha * Luke Gorrie * Dan Nicolaescu * Tim Parkin Sponsor ------- * Canonical_ .. _Canonical: http://www.canonical.com/ commit refs/heads/master mark :48 committer Martin Pool 1110863994 +1100 data 13 witty comment from :47 M 644 inline bzrlib/xml.py data 1556 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """XML externalization support.""" # "XML is like violence: if it doesn't solve your problem, you aren't # using enough of it." -- various __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement import os, time from trace import mutter class XMLMixin: def to_element(self): raise Exception("XMLMixin.to_element must be overridden in concrete classes") def write_xml(self, f): ElementTree(self.to_element()).write(f, 'utf-8') f.write('\n') def read_xml(cls, f): return cls.from_element(ElementTree().parse(f)) read_xml = classmethod(read_xml) commit refs/heads/master mark :49 committer 1111196504 +0000 data 33 fix local-time-offset calculation from :48 M 644 inline bzrlib/osutils.py data 6935 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, types, re, time, types from stat import S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE from errors import bailout def make_readonly(filename): """Make a filename read-only.""" # TODO: probably needs to be fixed for windows mod = os.stat(filename).st_mode mod = mod & 0777555 os.chmod(filename, mod) def make_writable(filename): mod = os.stat(filename).st_mode mod = mod | 0200 os.chmod(filename, mod) _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])') def quotefn(f): """Return shell-quoted filename""" ## We could be a bit more terse by using double-quotes etc f = _QUOTE_RE.sub(r'\\\1', f) if f[0] == '~': f[0:1] = r'\~' return f def file_kind(f): mode = os.lstat(f)[ST_MODE] if S_ISREG(mode): return 'file' elif S_ISDIR(mode): return 'directory' elif S_ISLNK(mode): return 'symlink' else: bailout("can't handle file kind with mode %o of %r" % (mode, f)) def isdir(f): """True if f is an accessible directory.""" try: return S_ISDIR(os.lstat(f)[ST_MODE]) except OSError: return False def isfile(f): """True if f is a regular file.""" try: return S_ISREG(os.lstat(f)[ST_MODE]) except OSError: return False def pumpfile(fromfile, tofile): """Copy contents of one file to another.""" tofile.write(fromfile.read()) def uuid(): """Return a new UUID""" ## XXX: Could alternatively read /proc/sys/kernel/random/uuid on ## Linux, but we need something portable for other systems; ## preferably an implementation in Python. bailout('uuids not allowed!') return chomp(os.popen('uuidgen').readline()) def chomp(s): if s and (s[-1] == '\n'): return s[:-1] else: return s def sha_file(f): import sha ## TODO: Maybe read in chunks to handle big files if hasattr(f, 'tell'): assert f.tell() == 0 s = sha.new() s.update(f.read()) return s.hexdigest() def sha_string(f): import sha s = sha.new() s.update(f) return s.hexdigest() def username(): """Return email-style username. Something similar to 'Martin Pool ' :todo: Check it's reasonably well-formed. :todo: Allow taking it from a dotfile to help people on windows who can't easily set variables. :todo: Cope without pwd module, which is only on unix. """ e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: return e import socket try: import pwd uid = os.getuid() w = pwd.getpwuid(uid) gecos = w.pw_gecos comma = gecos.find(',') if comma == -1: realname = gecos else: realname = gecos[:comma] return '%s <%s@%s>' % (realname, w.pw_name, socket.getfqdn()) except ImportError: pass import getpass, socket return '<%s@%s>' % (getpass.getuser(), socket.getfqdn()) def user_email(): """Return just the email component of a username.""" e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: import re m = re.search(r'[\w+.-]+@[\w+.-]+', e) if not m: bailout('%r is not a reasonable email address' % e) return m.group(0) import getpass, socket return '%s@%s' % (getpass.getuser(), socket.getfqdn()) def compare_files(a, b): """Returns true if equal in contents""" # TODO: don't read the whole thing in one go. result = a.read() == b.read() return result def local_time_offset(t=None): """Return offset of local zone from GMT, either at present or at time t.""" if time.localtime(t).tm_isdst and time.daylight: return -time.altzone else: return -time.timezone def format_date(t, offset=0, timezone='original'): ## TODO: Perhaps a global option to use either universal or local time? ## Or perhaps just let people set $TZ? import time assert isinstance(t, float) if timezone == 'utc': tt = time.gmtime(t) offset = 0 elif timezone == 'original': if offset == None: offset = 0 tt = time.gmtime(t + offset) elif timezone == 'local': tt = time.localtime(t) offset = local_time_offset(t) else: bailout("unsupported timezone format %r", ['options are "utc", "original", "local"']) return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt) + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)) def compact_date(when): return time.strftime('%Y%m%d%H%M%S', time.gmtime(when)) def filesize(f): """Return size of given open file.""" return os.fstat(f.fileno())[ST_SIZE] if hasattr(os, 'urandom'): # python 2.4 and later rand_bytes = os.urandom else: # FIXME: No good on non-Linux _rand_file = file('/dev/urandom', 'rb') rand_bytes = _rand_file.read ## TODO: We could later have path objects that remember their list ## decomposition (might be too tricksy though.) def splitpath(p): """Turn string into list of parts. >>> splitpath('a') ['a'] >>> splitpath('a/b') ['a', 'b'] >>> splitpath('a/./b') ['a', 'b'] >>> splitpath('a/.b') ['a', '.b'] >>> splitpath('a/../b') Traceback (most recent call last): ... BzrError: ("sorry, '..' not allowed in path", []) """ assert isinstance(p, types.StringTypes) ps = [f for f in p.split('/') if f != '.'] for f in ps: if f == '..': bailout("sorry, %r not allowed in path" % f) return ps def joinpath(p): assert isinstance(p, list) for f in p: if (f == '..') or (f is None) or (f == ''): bailout("sorry, %r not allowed in path" % f) return '/'.join(p) def appendpath(p1, p2): if p1 == '': return p2 else: return p1 + '/' + p2 def extern_command(cmd, ignore_errors = False): mutter('external command: %s' % `cmd`) if os.system(cmd): if not ignore_errors: bailout('command failed') M 644 inline doc/thanks.txt data 420 ****** Thanks ****** Inspiration, suggestions, advice, bug reports --------------------------------------------- * Mark Shuttleworth * Robert Collins * David Allouche * Robert Weir * James Blackwell * Garrett Rooney * Andrew Tridgell * Paul Russell * Aaron Bentley * Faheem Mitha * Luke Gorrie * Dan Nicolaescu * Tim Parkin * Scott James Remnant Sponsor ------- * Canonical_ .. _Canonical: http://www.canonical.com/ commit refs/heads/master mark :50 committer 1111220378 +1100 data 38 use "/usr/bin/env python" for shebang" from :49 M 644 inline bzr data 919 #! /usr/bin/env python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, bzrlib, bzrlib.commands if __name__ == '__main__': sys.exit(bzrlib.commands.main(sys.argv)) commit refs/heads/master mark :51 committer 1111276221 +1100 data 30 better notes on how to install from :50 M 644 inline README data 1138 *********************************** Release notes for Bazaar-NG (pre-0) *********************************** mbp@sourcefrog.net, March 2005, Canberra Caveats ------- * There is little locking or transaction control here; if you interrupt it the tree may be arbitrarily broken. This will be fixed. * Don't use this for critical data; at the very least keep separate regular snapshots of your tree. Dependencies ------------ This is mostly developed on Linux (Ubuntu); it should work on Unix, Windows, or OS X with relatively little trouble. bzr requires a fairly recent Python, say after 2.2. 2.4 is recommended. You must install either cElementTree_ or ElementTree_ first. .. _cElementTree: http://effbot.org/zone/celementtree.htm .. _ElementTree: http://effbot.org/zone/element-index.htm Installation ------------ The best way to install bzr is to symlink the ``bzr`` command onto a directory on your path. For example:: ln -s ~/work/bzr/bzr ~/bin/bzr If you use a symlink for this, Python will be able to automatically find the bzr libraries. Otherwise you must ensure they are listed on your $PYTHONPATH. commit refs/heads/master mark :52 committer 1111444189 +1100 data 38 fixup doctest for new module structure from :51 M 644 inline bzrlib/commands.py data 23197 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. """ # not currently working: # bzr info # Show some information about this branch. __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files. Fails if the files are already added. """ Branch('.').add(file_list, verbose=verbose) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): b = Branch('.') print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl is not None: return pl else: return 's' count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %5d %s' % (count_status[fs], name) print ' %5d versioned subdirector%s' % (count_version_dirs, plural(count_version_dirs, 'y', 'ies')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %5d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %5d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %5d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) def cmd_remove(file_list, verbose=False): Branch('.').remove(file_list, verbose=verbose) def cmd_file_id(filename): i = Branch('.').read_working_inventory().path2id(filename) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message, verbose=False): Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_doctest(): """Run internal doctest suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option import doctest, bzrlib.store bzrlib.trace.verbose = False doctest.testmod(bzrlib.store) doctest.testmod(bzrlib.inventory) doctest.testmod(bzrlib.branch) doctest.testmod(bzrlib.osutils) doctest.testmod(bzrlib.tree) # more strenuous tests; import bzrlib.tests doctest.testmod(bzrlib.tests) ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. for ap in argform: argname = ap[:-1] if ap[-1] == '?': assert 0 elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. try: # TODO: Lift into separate function in trace.py # TODO: Also show contents of /etc/lsb-release, if it can be parsed. # Perhaps that should eventually go into the platform library? # TODO: If the file doesn't exist, add a note describing it. t = bzrlib.trace._tracefile t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % format_date(time.time())) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.getfqdn())) t.write(' arguments: %r\n' % argv) starttime = os.times()[4] import platform t.write(' platform: %s\n' % platform.platform()) t.write(' python: %s\n' % platform.python_version()) ret = run_bzr(argv) times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum" % times[:4]) mutter(" %.3f elapsed" % (times[4] - starttime)) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') M 644 inline bzrlib/tests.py data 4737 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # XXX: We might prefer these to be in a text file rather than Python # source, but that only works in doctest from Python 2.4 and later, # which is not present in Warty. r""" Bazaar-NG test cases ******************** These are run by ``bzr.doctest``. >>> import bzrlib, os >>> bzrlib.commands.cmd_rocks() it sure does! Hey, nice place to begin. The basic object is a Branch. We have a special helper class ScratchBranch that automatically makes a directory and cleans itself up, but is in other respects identical. ScratchBranches are initially empty: >>> b = bzrlib.ScratchBranch() >>> b.show_status() New files in that directory are, it is initially unknown: >>> file(b.base + '/hello.c', 'wt').write('int main() {}') >>> b.show_status() ? hello.c That's not quite true; some files (like editor backups) are ignored by default: >>> file(b.base + '/hello.c~', 'wt').write('int main() {}') >>> b.show_status() ? hello.c >>> list(b.unknowns()) ['hello.c'] The ``add`` command marks a file to be added in the next revision: >>> b.add('hello.c') >>> b.show_status() A hello.c You can also add files that otherwise would be ignored. The ignore patterns only apply to files that would be otherwise unknown, so they have no effect once it's added. >>> b.add('hello.c~') >>> b.show_status() A hello.c A hello.c~ It is an error to add a file that isn't present in the working copy: >>> b.add('nothere') Traceback (most recent call last): ... BzrError: ('cannot add: not a regular file or directory: nothere', []) If we add a file and then change our mind, we can either revert it or remove the file. If we revert, we are left with the working copy (in either I or ? state). If we remove, the working copy is gone. Let's do that to the backup, presumably added accidentally. >>> b.remove('hello.c~') >>> b.show_status() A hello.c Now to commit, creating a new revision. (Fake the date and name for reproducibility.) >>> b.commit('start hello world', timestamp=0, committer='foo@nowhere') >>> b.show_status() >>> b.show_status(show_all=True) . hello.c I hello.c~ We can look back at history >>> r = b.get_revision(b.lookup_revision(1)) >>> r.message 'start hello world' >>> b.write_log(show_timezone='utc') ---------------------------------------- revno: 1 committer: foo@nowhere timestamp: Thu 1970-01-01 00:00:00 +0000 message: start hello world (The other fields will be a bit unpredictable, depending on who ran this test and when.) As of 2005-02-21, we can also add subdirectories to the revision! >>> os.mkdir(b.base + "/lib") >>> b.show_status() ? lib/ >>> b.add('lib') >>> b.show_status() A lib/ >>> b.commit('add subdir') >>> b.show_status() >>> b.show_status(show_all=True) . hello.c I hello.c~ . lib/ and we can also add files within subdirectories: >>> file(b.base + '/lib/hello', 'w').write('hello!\n') >>> b.show_status() ? lib/hello Tests for adding subdirectories, etc. >>> b = bzrlib.branch.ScratchBranch() >>> os.mkdir(b._rel('d1')) >>> os.mkdir(b._rel('d2')) >>> os.mkdir(b._rel('d2/d3')) >>> list(b.working_tree().unknowns()) ['d1', 'd2'] Create some files, but they're not seen as unknown yet: >>> file(b._rel('d1/f1'), 'w').close() >>> file(b._rel('d2/f2'), 'w').close() >>> file(b._rel('d2/f3'), 'w').close() >>> [v[0] for v in b.inventory.directories()] [''] >>> list(b.working_tree().unknowns()) ['d1', 'd2'] Adding a directory, and we see the file underneath: >>> b.add('d1') >>> [v[0] for v in b.inventory.directories()] ['', 'd1'] >>> list(b.working_tree().unknowns()) ['d1/f1', 'd2'] >>> # d2 comes first because it's in the top directory >>> b.add('d2') >>> b.commit('add some stuff') >>> list(b.working_tree().unknowns()) ['d1/f1', 'd2/d3', 'd2/f2', 'd2/f3'] """ M 644 inline doc/purpose.txt data 3145 What is the purpose of version control? *************************************** There are several overlapping purposes: * Allowing concurrent development by several people. Primarily, helping resolve changes when they are integrated, but also making each person aware of what the others have been doing, allowing them to talk about changes, etc. * To allow different lines of development, with sensible reintegration. e.g. stable/development, or branches for experimental features. * To record a history of development, so that you can find out why a feature went in or who added it, or which releases might be affected by a bug. * In particular, as a *record of decisions* about the project made by the developers. * Help in writing a gloss on that history; not simply a record of events but an explanation of what happened and why. Such a record may need to be written at several levels: a very detailed explanation of changes to a function; a note that a bug was fixed and why; and then a brief NEWS file for the whole release. While past events can never change, the intepretation placed upon them may change, even after the event. For example, even after a release has gone out, one might want to go back and note that a bug was actually fixed. The system is helping the developers tell a story about the development of the project. * As an aid to thinking about the project. (Much as a personal journal is not merely a record or even analysis of events, but also a chance to reflect and to think of the future.) People can review diffs, and then write a description of what changed, and in doing so perhaps realize something else they should do, or realize they made a mistake. Making branches helps work out the order of feature integration and the stability of different lines of development. * As an 'undo' protection mechanism. This is one reason why version control can be useful on projects that have only a single developer and never branch. * Incidentally, as a backup mechanism. Version control systems, particularly distributed systems, tend to cause code to exist on several machines, which gives some protection against loss of any single copy. It's still a good idea to use a separate backup system as well. * As a file-sharing mechanism: even with just a single developer and line of development it can be useful to keep files synchronized between several machines, which may not always be connected. ---- Steve Berczuk says__: __ http://www.bell-labs.com/cgi-user/OrgPatterns/OrgPatterns?ConfigurationManagementPatterns A successful configuration management process allows: * Developers to work together on a project, sharing common code. * Developers to share development effort on a module. * Developers to have access to the current stable (tested) version of a system. * The ability to back up to a previous stable version (one of a number of NamedStableBases), of a system * The ability of a developer to checkpoint changes to a module and to back off to a previous version of that module M 644 inline doc/random.txt data 8529 I think Ruby's point is right: we need to think about how a tool *feels* as you're using it. Making regular commits gives a nice rhythm to to working; in some ways it's nicer to just commit single files with C-x v v than to build complex changesets. (See gmane.c.v-c.arch.devel post 19 Nov, Tom Lord.) * Would like to generate an activity report, to e.g. mail to your boss or post to your blog. "What did I change today, across all these specified branches?" * It is possibly nice that tla by default forbids you from committing if emacs autosave or lock files exist -- I find it confusing to commit somethin other than what is shown in the editor window because there are unsaved changes. However, grumbling about unknown files is annoying, and requiring people to edit regexps in the id-tagging-method file to fix it is totally unreasonable. Perhaps there should be a preference to abort on unknown files, or perhaps it should be possible to specify forbidden files. Perhaps this is related to a mechanism to detect conflicted files: should refuse to commit if there are any .rej files lying around. *Those who lose history are doomed to recreate it.* -- broked (on #gnu.arch.users) *A universal convention supplies all of maintainability, clarity, consistency, and a foundation for good programming habits too. What it doesn't do is insist that you follow it against your will. That's Python!* -- Tim Peters on comp.lang.python, 2001-06-16 (Bazaar provides mechanism and convention, but it is up to you whether you wish to follow or enforce that convention.) ---- jblack asks for A way to subtract merges, so that you can see the work you've done to a branch since conception. ---- :: now that is a neat idea: advertise branches over zeroconf should make lca fun :-) ---- http://thedailywtf.com/ShowPost.aspx?PostID=24281 Source control is necessary and useful, but in a team of one (or even two) people the setup overhead isn't always worth it--especially if you're going to join source control in a month, and you don't want to have to migrate everything out of your existing (in my case, skunkworks) system before you can use it. At least that was my experience--I putzed with CVS a bit and knew other source control systems pretty well, but in the day-to-day it wasn't worth the bother (granted, I was a bit offended at having to wait to use the mainline source control, but that's another matter). I think Bazaar-NG will have such low setup overhead (just ``init``, ``add``) that it can be easily used for even tiny projects. The ability to merge previously-unrelated trees means they can fold their project in later. ---- From tridge: * cope without $EMAIL better * notes at start of .bzr.log: * you can delete this * or include it in bug reports * should you be able to remove things from the default ignore list? * headers at start of diff, giving some comments, perhaps dates * is diff against /dev/null really OK? I think so. * separate remove/delete commands? * detect files which were removed and now in 'missing' state * should we actually compare files for 'status', or check mtime and size; reading every file in the samba source tree can take a long time. without this, doing a status on a large tree can be very slow. but relying on mtime/size is a bit dangerous. people really do work on trees which take a large chunk of memory and which will not stay in memory * status up-to-date files: not 'U', and don't list without --all * if status does compare file text, then it should be quick when checking just a single file * wrapper for svn that every time run logs - command - all inputs - time it took - sufficient to replay everything - record all files * status crashes if a file is missing * option for -p1 level on diff, etc. perhaps * commit without message should start $EDITOR * don't duplicate all files on commit * start importing tridge-junkcode * perhaps need xdelta storage sooner rather than later, to handle very large file ---- The first operation most people do with a new version-control system is *not* making their own project, but rather getting a checkout of an existing project, building it, and possibly submitting a patch. So those operations should be *extremely* easy. ---- * Way to check that a branch is fully merged, and no longer needed: should mean all its changes have been integrated upstream, no uncommitted changes or rejects or unknown files. * Filter revisions by containing a particular word (as for log). Perhaps have key-value fields that might be used for e.g. line-of-development or bug nr? * List difference in the revisions on one branch vs another. * Perhaps use a partially-readable but still hopefully unique ID for revisions/inventories? * Preview what will happen in a merge before it is applied * When a changeset deletes a file, should have the option to just make it unknown/ignored. Perhaps this is best handled by an interactive merge. If the file is unchanged locally and deleted remotely, it will by default be deleted (but the user has the option to reject the delete, or to make it just unversioned, or to save a copy.) If it is modified locall then the user still needs to choose between those options but there is no default (or perhaps the default is to reject the delete.) * interactive commit, prompting whether each hunk should be sent (as for darcs) * Write up something about detection of unmodified files * Preview a merge so as to get some idea what will happen: * What revisions will be merged (log entries, etc) * What files will be affected? * Are those simple updates, or have they been updated locally as well. * Any renames or metadata clashes? * Show diffs or conflict markers. * Do the merge, but write into a second directory. * "Show me all changesets that touch this file" Can be done by walking back through all revisions, and filtering out those where the file-id either gets a new name or a new text. * Way to commit backdated revisions or pretend to be something by someone else, for the benefit of import tools; in general allow everything taken from the current environment to be overridden. * Cope well when trying to checkout or update over a flaky connection. Passive HTTP possibly helps with this: we can fetch all the file texts first, then the inventory, and can even retry interrupted connections. * Use readline for reading log messages, and store a history of previous commit messages! * Warn when adding huge files(?) - more than say 10MB? On the other hand, why not just cope? * Perhaps allow people to specify a revision-id, much as people have unique but human-assigned names for patches at the moment? ---- 20050218090900.GA2071@opteron.random Subject: Re: [darcs-users] Re: [BK] upgrade will be needed From: Andrea Arcangeli Newsgroups: gmane.linux.kernel Date: Fri, 18 Feb 2005 10:09:00 +0100 On Thu, Feb 17, 2005 at 06:24:53PM -0800, Tupshin Harper wrote: > small to medium sized ones). Last I checked, Arch was still too slow in > some areas, though that might have changed in recent months. Also, many IMHO someone needs to rewrite ARCH using the RCS or SCCS format for the backend and a single file for the changesets and with sane parameters conventions miming SVN. The internal algorithms of arch seems the most advanced possible. It's just the interface and the fs backend that's so bad and doesn't compress in the backups either. SVN bsddb doesn't compress either by default, but at least the new fsfs compresses pretty well, not as good as CVS, but not as badly as bsddb and arch either. I may be completely wrong, so take the above just as a humble suggestion. darcs scares me a bit because it's in haskell, I don't believe very much in functional languages for compute intensive stuff, ram utilization skyrockets sometime (I wouldn't like to need >1G of ram to manage the tree). Other languages like python or perl are much slower than C/C++ too but at least ram utilization can be normally dominated to sane levels with them and they can be greatly optimized easily with C/C++ extensions of the performance critical parts. ----- * Fix up diffs for files without a trailing newline commit refs/heads/master mark :53 committer 1111444792 +1100 data 39 'selftest' command instead of 'doctest' from :52 M 644 inline bzrlib/commands.py data 23226 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. """ # not currently working: # bzr info # Show some information about this branch. __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files. Fails if the files are already added. """ Branch('.').add(file_list, verbose=verbose) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): b = Branch('.') print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl is not None: return pl else: return 's' count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %5d %s' % (count_status[fs], name) print ' %5d versioned subdirector%s' % (count_version_dirs, plural(count_version_dirs, 'y', 'ies')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %5d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %5d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %5d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) def cmd_remove(file_list, verbose=False): Branch('.').remove(file_list, verbose=verbose) def cmd_file_id(filename): i = Branch('.').read_working_inventory().path2id(filename) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message, verbose=False): Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_doctest(): """Run internal doctest suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option import doctest, bzrlib.store bzrlib.trace.verbose = False doctest.testmod(bzrlib.store) doctest.testmod(bzrlib.inventory) doctest.testmod(bzrlib.branch) doctest.testmod(bzrlib.osutils) doctest.testmod(bzrlib.tree) # more strenuous tests; import bzrlib.tests doctest.testmod(bzrlib.tests) cmd_selftest = cmd_doctest ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. for ap in argform: argname = ap[:-1] if ap[-1] == '?': assert 0 elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. try: # TODO: Lift into separate function in trace.py # TODO: Also show contents of /etc/lsb-release, if it can be parsed. # Perhaps that should eventually go into the platform library? # TODO: If the file doesn't exist, add a note describing it. t = bzrlib.trace._tracefile t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % format_date(time.time())) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.getfqdn())) t.write(' arguments: %r\n' % argv) starttime = os.times()[4] import platform t.write(' platform: %s\n' % platform.platform()) t.write(' python: %s\n' % platform.python_version()) ret = run_bzr(argv) times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum" % times[:4]) mutter(" %.3f elapsed" % (times[4] - starttime)) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :54 committer 1111449765 +1100 data 50 suggestions from robert about the inventory format from :53 M 644 inline doc/formats.txt data 8161 ***************** Bazaar-NG formats ***************** .. contents:: Since branches are working directories there is just a single directory format. There is one metadata directory called ``.bzr`` at the top of each tree. Control files inside ``.bzr`` are never touched by patches and should not normally be edited by the user. These files are designed so that repository-level operations are ACID without depending on atomic operations spanning multiple files. There are two particular cases: aborting a transaction in the middle, and contention from multiple processes. We also need to be careful to flush files to disk at appropriate points; even this may not be totally safe if the filesystem does not guarantee ordering between multiple file changes, so we need to be sure to roll back. The design must also be such that the directory can simply be copied and that hardlinked directories will work. (So we must always replace files, never just append.) A cache is kept under here of easily-accessible information about previous revisions. This should be under a single directory so that it can be easily identified, excluded from backups, removed, etc. This might contain pristine tree from previous revisions, manifests and inventories, etc. It might also contain working directories when building a commit, etc. Call this maybe ``cache`` or ``tmp``. I wonder if we should use .zip files for revisions and cacherevs rather than tar files so that random access is easier/more efficient. There is a Python library ``zipfile``. Signing XML files ***************** bzr relies on storing hashes or GPG signatures of various XML files. There can be multiple equivalent representations of the same XML tree, but these will have different byte-by-byte hashes. Once signed files are written out, they must be stored byte-for-byte and never re-encoded or renormalized, because that would break their hash or signature. Branch metadata *************** All inside ``.bzr`` ``README`` Tells people not to touch anything here. ``branch-format`` Identifies the parent as a Bazaar-NG branch; contains the overall branch metadata format as a string. ``pristine-directory`` Identifies that this is a pristine directory and may not be committed to. ``patches/`` Directory containing all patches applied to this branch, one per file. Patches are stored as compressed deltas. We also store the hash of the delta, hash of the before and after manifests, and optionally a GPG signature. ``cache/`` Contains various cached data that can be destroyed and will be recreated. (It should not be modified.) ``cache/pristine/`` Contains cached full trees for selected previous revisions, used when generating diffs, etc. ``cache/inventory/`` Contains cached inventories of previous revisions. ``cache/snapshot/`` Contains tarballs of cached revisions of the tree, named by their revision id. These can also be removed, but ``patch-history`` File containing the UUIDs of all patches taken in this branch, in the order they were taken. Each commit adds exactly one line to this file; lines are never removed or reordered. ``merged-patches`` List of foreign patches that have been merged into this branch. Must have no entries in common with ``patch-history``. Commits that include merges add to this file; lines are never removed or reordered. ``pending-merge-patches`` List of foreign patches that have been merged and are waiting to be committed. ``branch-name`` User-qualified name of the branch, for the purpose of describing the origin of patches, e.g. ``mbp@sourcefrog.net/distcc--main``. ``friends`` List of branches from which we have pulled; file containing a list of pairs of branch-name and location. ``parent`` Default pull/push target. ``pending-inventory`` Mapping from UUIDs to file name in the current working directory. ``branch-lock`` Lock held while modifying the branch, to protect against clashing updates. Locking ******* Is locking a good strategy? Perhaps somekind of read-copy-update or seq-lock based mechanism would work better? If we do use a locking algorithm, is it OK to rely on filesystem locking or do we need our own mechanism? I think most hosts should have reasonable ``flock()`` or equivalent, even on NFS. One risk is that on NFS it is easy to have broken locking and not know it, so it might be better to have something that will fail safe. Filesystem locks go away if the machine crashes or the process is terminated; this can be a feature in that we do not need to deal with stale locks but also a feature in that the lock itself does not indicate cleanup may be needed. robertc points out that tla converged on renaming a directory as a mechanism: this is one thing which is known to be atomic on almost all filesystems. Apparently renaming files, creating directories, making symlinks etc are not good enough. Delta ***** XML document plus a bag of patches, expressing the difference between two revisions. May be a partial delta. * list of entries * entry * parent directory (if any) * before-name or null if new * after-name or null if deleted * uuid * type (dir, file, symlink, ...) * patch type (patch, full-text, xdelta, ...) * patch filename (?) Inventory ********* XML document; series of entries. (Quite similar to the svn ``entries`` file; perhaps should even have that name.) Stored identified by its hash. An inventory is stored for recorded revisions, also a ``pending-inventory`` for a working directory. Revision ******** XML document. Stored identified by its hash. committer RFC-2822-style name of the committer. Should match the key used to sign the revision. comment multi-line free-form text; whitespace and line breaks preserved timestamp As floating-point seconds since epoch. precursor ID of the previous revision on this branch. May be absent (null) if this is the start of a new branch. branch name Name of the branch to which this was originally committed. (I'm not totally satisfied that this is the right way to do it; the results will be a bit wierd when a series of revisions pass through variously named branches.) inventory_hash Acts as a pointer to the inventory for this revision. merged-branches Revision ids of complete branches merged into this revision. If a revision is listed, that revision and transitively its predecessor and all other merged-branches are merged. This is empty except where cherry-picks have occurred. merged-patches Revision ids of cherry-picked patches. Patches whose branches are merged need not be listed here. Listing a revision ID implies that only the change of that particular revision from its predecessor has been merged in. This is empty except where cherry-picks have occurred. The transitive closure avoids Arch's problem of needing to list a large number of previous revisions. As ddaa writes: Continuation revisions (created by tla tag or baz branch) are associated to a patchlog whose New-patches header lists the revisions associated to all the patchlogs present in the tree. That was introduced as an optimisation so the set of patchlogs in any revision could be determined solely by examining the patchlogs of ancestor revisions in the same branch. This behaves well as long as the total count of patchlog is reasonably small or new branches are not very frequent. A continuation revision on $tree currently creates a patchlog of about 500K. This patchlog is present in all descendent of the revision, and all revisions that merges it. It may be useful at some times to keep a cache of all the branches, or all the revisions, present in the history of a branch, so that we do need to walk the whole history of the branch to build this list. ---- Proposed changes **************** * Don't store parent-id in all revisions, but rather have nodes that contain entries for children? * Assign an id to the root of the tree, perhaps listed in the top of the inventory? commit refs/heads/master mark :55 committer 1111450719 +1100 data 39 bzr selftest shows some counts of tests from :54 M 644 inline bzrlib/commands.py data 23488 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. """ # not currently working: # bzr info # Show some information about this branch. __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files. Fails if the files are already added. """ Branch('.').add(file_list, verbose=verbose) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): b = Branch('.') print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl is not None: return pl else: return 's' count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %5d %s' % (count_status[fs], name) print ' %5d versioned subdirector%s' % (count_version_dirs, plural(count_version_dirs, 'y', 'ies')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %5d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %5d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %5d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) def cmd_remove(file_list, verbose=False): Branch('.').remove(file_list, verbose=verbose) def cmd_file_id(filename): i = Branch('.').read_working_inventory().path2id(filename) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message, verbose=False): Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. for ap in argform: argname = ap[:-1] if ap[-1] == '?': assert 0 elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. try: # TODO: Lift into separate function in trace.py # TODO: Also show contents of /etc/lsb-release, if it can be parsed. # Perhaps that should eventually go into the platform library? # TODO: If the file doesn't exist, add a note describing it. t = bzrlib.trace._tracefile t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % format_date(time.time())) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.getfqdn())) t.write(' arguments: %r\n' % argv) starttime = os.times()[4] import platform t.write(' platform: %s\n' % platform.platform()) t.write(' python: %s\n' % platform.python_version()) ret = run_bzr(argv) times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum" % times[:4]) mutter(" %.3f elapsed" % (times[4] - starttime)) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :56 committer 1111450871 +1100 data 14 more add tests from :55 M 644 inline bzrlib/tests.py data 4835 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # XXX: We might prefer these to be in a text file rather than Python # source, but that only works in doctest from Python 2.4 and later, # which is not present in Warty. r""" Bazaar-NG test cases ******************** These are run by ``bzr.doctest``. >>> import bzrlib, os >>> bzrlib.commands.cmd_rocks() it sure does! Hey, nice place to begin. The basic object is a Branch. We have a special helper class ScratchBranch that automatically makes a directory and cleans itself up, but is in other respects identical. ScratchBranches are initially empty: >>> b = bzrlib.ScratchBranch() >>> b.show_status() New files in that directory are, it is initially unknown: >>> file(b.base + '/hello.c', 'wt').write('int main() {}') >>> b.show_status() ? hello.c That's not quite true; some files (like editor backups) are ignored by default: >>> file(b.base + '/hello.c~', 'wt').write('int main() {}') >>> b.show_status() ? hello.c >>> list(b.unknowns()) ['hello.c'] The ``add`` command marks a file to be added in the next revision: >>> b.add('hello.c') >>> b.show_status() A hello.c You can also add files that otherwise would be ignored. The ignore patterns only apply to files that would be otherwise unknown, so they have no effect once it's added. >>> b.add('hello.c~') >>> b.show_status() A hello.c A hello.c~ It is an error to add a file that isn't present in the working copy: >>> b.add('nothere') Traceback (most recent call last): ... BzrError: ('cannot add: not a regular file or directory: nothere', []) If we add a file and then change our mind, we can either revert it or remove the file. If we revert, we are left with the working copy (in either I or ? state). If we remove, the working copy is gone. Let's do that to the backup, presumably added accidentally. >>> b.remove('hello.c~') >>> b.show_status() A hello.c Now to commit, creating a new revision. (Fake the date and name for reproducibility.) >>> b.commit('start hello world', timestamp=0, committer='foo@nowhere') >>> b.show_status() >>> b.show_status(show_all=True) . hello.c I hello.c~ We can look back at history >>> r = b.get_revision(b.lookup_revision(1)) >>> r.message 'start hello world' >>> b.write_log(show_timezone='utc') ---------------------------------------- revno: 1 committer: foo@nowhere timestamp: Thu 1970-01-01 00:00:00 +0000 message: start hello world (The other fields will be a bit unpredictable, depending on who ran this test and when.) As of 2005-02-21, we can also add subdirectories to the revision! >>> os.mkdir(b.base + "/lib") >>> b.show_status() ? lib/ >>> b.add('lib') >>> b.show_status() A lib/ >>> b.commit('add subdir') >>> b.show_status() >>> b.show_status(show_all=True) . hello.c I hello.c~ . lib/ and we can also add files within subdirectories: >>> file(b.base + '/lib/hello', 'w').write('hello!\n') >>> b.show_status() ? lib/hello Tests for adding subdirectories, etc. >>> b = bzrlib.branch.ScratchBranch() >>> os.mkdir(b._rel('d1')) >>> os.mkdir(b._rel('d2')) >>> os.mkdir(b._rel('d2/d3')) >>> list(b.working_tree().unknowns()) ['d1', 'd2'] Create some files, but they're not seen as unknown yet: >>> file(b._rel('d1/f1'), 'w').close() >>> file(b._rel('d2/f2'), 'w').close() >>> file(b._rel('d2/f3'), 'w').close() >>> [v[0] for v in b.inventory.directories()] [''] >>> list(b.working_tree().unknowns()) ['d1', 'd2'] Adding a directory, and we see the file underneath: >>> b.add('d1') >>> [v[0] for v in b.inventory.directories()] ['', 'd1'] >>> list(b.working_tree().unknowns()) ['d1/f1', 'd2'] >>> # d2 comes first because it's in the top directory >>> b.add('d2') >>> b.commit('add some stuff') >>> list(b.working_tree().unknowns()) ['d1/f1', 'd2/d3', 'd2/f2', 'd2/f3'] >>> b.add('d1/f1') >>> list(b.working_tree().unknowns()) ['d2/d3', 'd2/f2', 'd2/f3'] """ commit refs/heads/master mark :57 committer 1111450999 +1100 data 42 error if --message is not given for commit from :56 M 644 inline bzrlib/commands.py data 23564 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. """ # not currently working: # bzr info # Show some information about this branch. __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files. Fails if the files are already added. """ Branch('.').add(file_list, verbose=verbose) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): b = Branch('.') print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl is not None: return pl else: return 's' count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %5d %s' % (count_status[fs], name) print ' %5d versioned subdirector%s' % (count_version_dirs, plural(count_version_dirs, 'y', 'ies')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %5d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %5d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %5d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) def cmd_remove(file_list, verbose=False): Branch('.').remove(file_list, verbose=verbose) def cmd_file_id(filename): i = Branch('.').read_working_inventory().path2id(filename) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. for ap in argform: argname = ap[:-1] if ap[-1] == '?': assert 0 elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. try: # TODO: Lift into separate function in trace.py # TODO: Also show contents of /etc/lsb-release, if it can be parsed. # Perhaps that should eventually go into the platform library? # TODO: If the file doesn't exist, add a note describing it. t = bzrlib.trace._tracefile t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % format_date(time.time())) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.getfqdn())) t.write(' arguments: %r\n' % argv) starttime = os.times()[4] import platform t.write(' platform: %s\n' % platform.platform()) t.write(' python: %s\n' % platform.python_version()) ret = run_bzr(argv) times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum" % times[:4]) mutter(" %.3f elapsed" % (times[4] - starttime)) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :58 committer 1111451054 +1100 data 35 include bzrlib.commands in selftest from :57 M 644 inline bzrlib/commands.py data 23581 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. """ # not currently working: # bzr info # Show some information about this branch. __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files. Fails if the files are already added. """ Branch('.').add(file_list, verbose=verbose) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): b = Branch('.') print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl is not None: return pl else: return 's' count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %5d %s' % (count_status[fs], name) print ' %5d versioned subdirector%s' % (count_version_dirs, plural(count_version_dirs, 'y', 'ies')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %5d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %5d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %5d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) def cmd_remove(file_list, verbose=False): Branch('.').remove(file_list, verbose=verbose) def cmd_file_id(filename): i = Branch('.').read_working_inventory().path2id(filename) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. for ap in argform: argname = ap[:-1] if ap[-1] == '?': assert 0 elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. try: # TODO: Lift into separate function in trace.py # TODO: Also show contents of /etc/lsb-release, if it can be parsed. # Perhaps that should eventually go into the platform library? # TODO: If the file doesn't exist, add a note describing it. t = bzrlib.trace._tracefile t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % format_date(time.time())) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.getfqdn())) t.write(' arguments: %r\n' % argv) starttime = os.times()[4] import platform t.write(' platform: %s\n' % platform.platform()) t.write(' python: %s\n' % platform.python_version()) ret = run_bzr(argv) times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum" % times[:4]) mutter(" %.3f elapsed" % (times[4] - starttime)) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :59 committer 1111451837 +1100 data 32 lift out tracefile creation code from :58 M 644 inline bzrlib/commands.py data 22719 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. """ # not currently working: # bzr info # Show some information about this branch. __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files. Fails if the files are already added. """ Branch('.').add(file_list, verbose=verbose) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): b = Branch('.') print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl is not None: return pl else: return 's' count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %5d %s' % (count_status[fs], name) print ' %5d versioned subdirector%s' % (count_version_dirs, plural(count_version_dirs, 'y', 'ies')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %5d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %5d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %5d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) def cmd_remove(file_list, verbose=False): Branch('.').remove(file_list, verbose=verbose) def cmd_file_id(filename): i = Branch('.').read_working_inventory().path2id(filename) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. for ap in argform: argname = ap[:-1] if ap[-1] == '?': assert 0 elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') M 644 inline bzrlib/trace.py data 2872 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os, time, socket import bzrlib ###################################################################### # messages and logging ## TODO: If --verbose is given then write to both stderr and ## _tracefile; perhaps replace _tracefile with a tee thing. global _tracefile, _starttime # used to have % (os.environ['USER'], time.time(), os.getpid()), 'w') # If false, notes also go to stdout; should replace this with --silent # at some point. silent = False verbose = False def mutter(msg): _tracefile.write(msg) _tracefile.write('\n') _tracefile.flush() if verbose: sys.stderr.write('- ' + msg + '\n') def note(msg): b = '* ' + str(msg) + '\n' if not silent: sys.stderr.write(b) _tracefile.write(b) _tracefile.flush() def log_error(msg): sys.stderr.write(msg) _tracefile.write(msg) _tracefile.flush() def create_tracefile(argv): # TODO: Also show contents of /etc/lsb-release, if it can be parsed. # Perhaps that should eventually go into the platform library? # TODO: If the file doesn't exist, add a note describing it. # Messages are always written to here, so that we have some # information if something goes wrong. In a future version this # file will be removed on successful completion. global _starttime, _tracefile _starttime = os.times()[4] _tracefile = file('.bzr.log', 'at') t = _tracefile t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % bzrlib.osutils.format_date(time.time())) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.getfqdn())) t.write(' arguments: %r\n' % argv) import platform t.write(' platform: %s\n' % platform.platform()) t.write(' python: %s\n' % platform.python_version()) import atexit atexit.register(_close_trace) def _close_trace(): times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum, %.3f elapsed" % (times[:4] + ((times[4] - _starttime),))) commit refs/heads/master mark :60 committer 1111454220 +1100 data 40 put little explanation at top of bzr.log from :59 M 644 inline bzrlib/trace.py data 3287 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os, time, socket, stat import bzrlib ###################################################################### # messages and logging ## TODO: If --verbose is given then write to both stderr and ## _tracefile; perhaps replace _tracefile with a tee thing. global _tracefile, _starttime # used to have % (os.environ['USER'], time.time(), os.getpid()), 'w') # If false, notes also go to stdout; should replace this with --silent # at some point. silent = False verbose = False def mutter(msg): _tracefile.write(msg) _tracefile.write('\n') _tracefile.flush() if verbose: sys.stderr.write('- ' + msg + '\n') def note(msg): b = '* ' + str(msg) + '\n' if not silent: sys.stderr.write(b) _tracefile.write(b) _tracefile.flush() def log_error(msg): sys.stderr.write(msg) _tracefile.write(msg) _tracefile.flush() def create_tracefile(argv): # TODO: Also show contents of /etc/lsb-release, if it can be parsed. # Perhaps that should eventually go into the platform library? # TODO: If the file doesn't exist, add a note describing it. # Messages are always written to here, so that we have some # information if something goes wrong. In a future version this # file will be removed on successful completion. global _starttime, _tracefile _starttime = os.times()[4] _tracefile = file('.bzr.log', 'at') t = _tracefile if os.fstat(t.fileno())[stat.ST_SIZE] == 0: t.write("\nthis is a debug log for diagnosing/reporting problems in bzr\n") t.write("you can delete or truncate this file, or include sections in\n") t.write("bug reports to bazaar-ng@lists.canonical.com\n\n") # TODO: If we failed to create the file, perhaps give a warning # but don't abort; send things to /dev/null instead? t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % bzrlib.osutils.format_date(time.time())) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.getfqdn())) t.write(' arguments: %r\n' % argv) import platform t.write(' platform: %s\n' % platform.platform()) t.write(' python: %s\n' % platform.python_version()) import atexit atexit.register(_close_trace) def _close_trace(): times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum, %.3f elapsed" % (times[:4] + ((times[4] - _starttime),))) commit refs/heads/master mark :61 committer 1111454258 +1100 data 26 ignore generated changelog from :60 M 644 inline .bzrignore data 43 doc/*.html *.py[oc] *~ .* {arch} CHANGELOG commit refs/heads/master mark :62 committer 1111474517 +1100 data 210 - new find_branch_root function; based on suggestion from aaron but cleaned up a bit and should work on non-unix systems - new find-branch-root command to exercise it - Branch constructor does this by default from :61 M 644 inline bzrlib/branch.py data 27173 # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset from store import ImmutableStore from revision import Revision from errors import bailout from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f is None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f last_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be bailout('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. :todo: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. :todo: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. :todo: Keep the on-disk branch locked while the object exists. :todo: mkdir() method. """ def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. :param base: Base directory for the branch. :param init: If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. :param find_root: If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def _rel(self, name): """Return filename relative to branch top""" return os.path.join(self.base, name) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch""" return file(self.controlfilename(file_or_path), mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # read in binary mode to detect newline wierdness. fmt = self.controlfile('branch-format', 'rb').read() if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() inv = Inventory.read_xml(self.controlfile('inventory', 'r')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'w') inv.write_xml(tmpf) tmpf.close() os.rename(tmpfname, self.controlfilename('inventory')) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. This puts the files in the Added state, so that they will be recorded by the next commit. :todo: Perhaps have an option to add the ids even if the files do not (yet) exist. :todo: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. :todo: Option to specify file id. :todo: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self._rel(f)) if isfile(fullpath): kind = 'file' elif isdir(fullpath): kind = 'directory' else: bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if len(fp) > 1: parent_name = joinpath(fp[:-1]) mutter("lookup parent %r" % parent_name) parent_id = inv.path2id(parent_name) if parent_id == None: bailout("cannot add: parent %r is not versioned" % joinpath(fp[:-1])) else: parent_id = None file_id = _gen_file_id(fp[-1]) inv.add(InventoryEntry(file_id, fp[-1], kind=kind, parent_id=parent_id)) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r parent_id={%s}" % (f, file_id, kind, parent_id)) self._write_inventory(inv) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on :todo: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True :todo: Do something useful with directories. :todo: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. :param timestamp: if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self._rel(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = _gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'R' else: state = 'M' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) mutter("append to revision-history") self.controlfile('revision-history', 'at').write(rev_id + '\n') mutter("done!") def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. :todo: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: bailout("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original'): """Write out human-readable log of commits to this branch :param utc: If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l revno += 1 precursor = p def show_status(branch, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b._rel('foo')) >>> b.show_status() D foo :todo: Get state for single files. :todo: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = branch.basis_tree() old_inv = old.inventory new = branch.working_tree() new_inv = new.inventory for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("wierd file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files = []): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def _gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" assert '/' not in name while name[0] == '.': name = name[1:] s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/commands.py data 22982 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. """ # not currently working: # bzr info # Show some information about this branch. __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files. Fails if the files are already added. """ Branch('.').add(file_list, verbose=verbose) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): b = Branch('.') print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl is not None: return pl else: return 's' count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %5d %s' % (count_status[fs], name) print ' %5d versioned subdirector%s' % (count_version_dirs, plural(count_version_dirs, 'y', 'ies')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %5d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %5d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %5d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) def cmd_remove(file_list, verbose=False): Branch('.').remove(file_list, verbose=verbose) def cmd_file_id(filename): i = Branch('.').read_working_inventory().path2id(filename) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_find_branch_root(filename=None): print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'find-branch-root': ['filename?'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :63 committer 1111474927 +1100 data 19 fix up uuid command from :62 M 644 inline bzrlib/commands.py data 22997 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. """ # not currently working: # bzr info # Show some information about this branch. __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files. Fails if the files are already added. """ Branch('.').add(file_list, verbose=verbose) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): b = Branch('.') print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl is not None: return pl else: return 's' count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %5d %s' % (count_status[fs], name) print ' %5d versioned subdirector%s' % (count_version_dirs, plural(count_version_dirs, 'y', 'ies')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %5d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %5d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %5d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) def cmd_remove(file_list, verbose=False): Branch('.').remove(file_list, verbose=verbose) def cmd_file_id(filename): i = Branch('.').read_working_inventory().path2id(filename) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_find_branch_root(filename=None): print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'find-branch-root': ['filename?'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') M 644 inline bzrlib/osutils.py data 7005 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, types, re, time, types from stat import S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE from errors import bailout def make_readonly(filename): """Make a filename read-only.""" # TODO: probably needs to be fixed for windows mod = os.stat(filename).st_mode mod = mod & 0777555 os.chmod(filename, mod) def make_writable(filename): mod = os.stat(filename).st_mode mod = mod | 0200 os.chmod(filename, mod) _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])') def quotefn(f): """Return shell-quoted filename""" ## We could be a bit more terse by using double-quotes etc f = _QUOTE_RE.sub(r'\\\1', f) if f[0] == '~': f[0:1] = r'\~' return f def file_kind(f): mode = os.lstat(f)[ST_MODE] if S_ISREG(mode): return 'file' elif S_ISDIR(mode): return 'directory' elif S_ISLNK(mode): return 'symlink' else: bailout("can't handle file kind with mode %o of %r" % (mode, f)) def isdir(f): """True if f is an accessible directory.""" try: return S_ISDIR(os.lstat(f)[ST_MODE]) except OSError: return False def isfile(f): """True if f is a regular file.""" try: return S_ISREG(os.lstat(f)[ST_MODE]) except OSError: return False def pumpfile(fromfile, tofile): """Copy contents of one file to another.""" tofile.write(fromfile.read()) def uuid(): """Return a new UUID""" ## XXX: Could alternatively read /proc/sys/kernel/random/uuid on ## Linux, but we need something portable for other systems; ## preferably an implementation in Python. try: return chomp(file('/proc/sys/kernel/random/uuid').readline()) except IOError: return chomp(os.popen('uuidgen').readline()) def chomp(s): if s and (s[-1] == '\n'): return s[:-1] else: return s def sha_file(f): import sha ## TODO: Maybe read in chunks to handle big files if hasattr(f, 'tell'): assert f.tell() == 0 s = sha.new() s.update(f.read()) return s.hexdigest() def sha_string(f): import sha s = sha.new() s.update(f) return s.hexdigest() def username(): """Return email-style username. Something similar to 'Martin Pool ' :todo: Check it's reasonably well-formed. :todo: Allow taking it from a dotfile to help people on windows who can't easily set variables. :todo: Cope without pwd module, which is only on unix. """ e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: return e import socket try: import pwd uid = os.getuid() w = pwd.getpwuid(uid) gecos = w.pw_gecos comma = gecos.find(',') if comma == -1: realname = gecos else: realname = gecos[:comma] return '%s <%s@%s>' % (realname, w.pw_name, socket.getfqdn()) except ImportError: pass import getpass, socket return '<%s@%s>' % (getpass.getuser(), socket.getfqdn()) def user_email(): """Return just the email component of a username.""" e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: import re m = re.search(r'[\w+.-]+@[\w+.-]+', e) if not m: bailout('%r is not a reasonable email address' % e) return m.group(0) import getpass, socket return '%s@%s' % (getpass.getuser(), socket.getfqdn()) def compare_files(a, b): """Returns true if equal in contents""" # TODO: don't read the whole thing in one go. result = a.read() == b.read() return result def local_time_offset(t=None): """Return offset of local zone from GMT, either at present or at time t.""" if time.localtime(t).tm_isdst and time.daylight: return -time.altzone else: return -time.timezone def format_date(t, offset=0, timezone='original'): ## TODO: Perhaps a global option to use either universal or local time? ## Or perhaps just let people set $TZ? import time assert isinstance(t, float) if timezone == 'utc': tt = time.gmtime(t) offset = 0 elif timezone == 'original': if offset == None: offset = 0 tt = time.gmtime(t + offset) elif timezone == 'local': tt = time.localtime(t) offset = local_time_offset(t) else: bailout("unsupported timezone format %r", ['options are "utc", "original", "local"']) return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt) + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)) def compact_date(when): return time.strftime('%Y%m%d%H%M%S', time.gmtime(when)) def filesize(f): """Return size of given open file.""" return os.fstat(f.fileno())[ST_SIZE] if hasattr(os, 'urandom'): # python 2.4 and later rand_bytes = os.urandom else: # FIXME: No good on non-Linux _rand_file = file('/dev/urandom', 'rb') rand_bytes = _rand_file.read ## TODO: We could later have path objects that remember their list ## decomposition (might be too tricksy though.) def splitpath(p): """Turn string into list of parts. >>> splitpath('a') ['a'] >>> splitpath('a/b') ['a', 'b'] >>> splitpath('a/./b') ['a', 'b'] >>> splitpath('a/.b') ['a', '.b'] >>> splitpath('a/../b') Traceback (most recent call last): ... BzrError: ("sorry, '..' not allowed in path", []) """ assert isinstance(p, types.StringTypes) ps = [f for f in p.split('/') if f != '.'] for f in ps: if f == '..': bailout("sorry, %r not allowed in path" % f) return ps def joinpath(p): assert isinstance(p, list) for f in p: if (f == '..') or (f is None) or (f == ''): bailout("sorry, %r not allowed in path" % f) return '/'.join(p) def appendpath(p1, p2): if p1 == '': return p2 else: return p1 + '/' + p2 def extern_command(cmd, ignore_errors = False): mutter('external command: %s' % `cmd`) if os.system(cmd): if not ignore_errors: bailout('command failed') commit refs/heads/master mark :64 committer 1111475396 +1100 data 55 - fix up init command for new find-branch-root function from :63 M 644 inline bzrlib/branch.py data 27220 # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset from store import ImmutableStore from revision import Revision from errors import bailout from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f is None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f last_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be bailout('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. :todo: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. :todo: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. :todo: Keep the on-disk branch locked while the object exists. :todo: mkdir() method. """ def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. :param base: Base directory for the branch. :param init: If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. :param find_root: If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def _rel(self, name): """Return filename relative to branch top""" return os.path.join(self.base, name) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch""" return file(self.controlfilename(file_or_path), mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # read in binary mode to detect newline wierdness. fmt = self.controlfile('branch-format', 'rb').read() if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() inv = Inventory.read_xml(self.controlfile('inventory', 'r')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'w') inv.write_xml(tmpf) tmpf.close() os.rename(tmpfname, self.controlfilename('inventory')) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. This puts the files in the Added state, so that they will be recorded by the next commit. :todo: Perhaps have an option to add the ids even if the files do not (yet) exist. :todo: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. :todo: Option to specify file id. :todo: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self._rel(f)) if isfile(fullpath): kind = 'file' elif isdir(fullpath): kind = 'directory' else: bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if len(fp) > 1: parent_name = joinpath(fp[:-1]) mutter("lookup parent %r" % parent_name) parent_id = inv.path2id(parent_name) if parent_id == None: bailout("cannot add: parent %r is not versioned" % joinpath(fp[:-1])) else: parent_id = None file_id = _gen_file_id(fp[-1]) inv.add(InventoryEntry(file_id, fp[-1], kind=kind, parent_id=parent_id)) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r parent_id={%s}" % (f, file_id, kind, parent_id)) self._write_inventory(inv) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on :todo: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True :todo: Do something useful with directories. :todo: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. :param timestamp: if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self._rel(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = _gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'R' else: state = 'M' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) mutter("append to revision-history") self.controlfile('revision-history', 'at').write(rev_id + '\n') mutter("done!") def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. :todo: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: bailout("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original'): """Write out human-readable log of commits to this branch :param utc: If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l revno += 1 precursor = p def show_status(branch, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b._rel('foo')) >>> b.show_status() D foo :todo: Get state for single files. :todo: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = branch.basis_tree() old_inv = old.inventory new = branch.working_tree() new_inv = new.inventory for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("wierd file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files = []): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def _gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" assert '/' not in name while name[0] == '.': name = name[1:] s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :65 committer 1111475538 +1100 data 48 rename 'find-branch-root' command to just 'root' from :64 M 644 inline bzrlib/commands.py data 23018 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. """ # not currently working: # bzr info # Show some information about this branch. __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files. Fails if the files are already added. """ Branch('.').add(file_list, verbose=verbose) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): b = Branch('.') print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl is not None: return pl else: return 's' count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %5d %s' % (count_status[fs], name) print ' %5d versioned subdirector%s' % (count_version_dirs, plural(count_version_dirs, 'y', 'ies')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %5d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %5d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %5d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) def cmd_remove(file_list, verbose=False): Branch('.').remove(file_list, verbose=verbose) def cmd_file_id(filename): i = Branch('.').read_working_inventory().path2id(filename) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'root': ['filename?'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :66 committer 1111476350 +1100 data 49 add command uses the path of the first named file from :65 M 644 inline bzrlib/commands.py data 23074 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. """ # not currently working: # bzr info # Show some information about this branch. __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files. Fails if the files are already added. """ assert file_list b = Branch(file_list[0], find_root=True) b.add(file_list, verbose=verbose) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): b = Branch('.') print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl is not None: return pl else: return 's' count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %5d %s' % (count_status[fs], name) print ' %5d versioned subdirector%s' % (count_version_dirs, plural(count_version_dirs, 'y', 'ies')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %5d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %5d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %5d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) def cmd_remove(file_list, verbose=False): Branch('.').remove(file_list, verbose=verbose) def cmd_file_id(filename): i = Branch('.').read_working_inventory().path2id(filename) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'root': ['filename?'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :67 committer 1111476525 +1100 data 91 use abspath() for the function that makes an absolute path to something in a branch or tree from :66 M 644 inline bzrlib/branch.py data 27238 # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset from store import ImmutableStore from revision import Revision from errors import bailout from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f is None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f last_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be bailout('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. :todo: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. :todo: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. :todo: Keep the on-disk branch locked while the object exists. :todo: mkdir() method. """ def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. :param base: Base directory for the branch. :param init: If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. :param find_root: If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch""" return file(self.controlfilename(file_or_path), mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # read in binary mode to detect newline wierdness. fmt = self.controlfile('branch-format', 'rb').read() if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() inv = Inventory.read_xml(self.controlfile('inventory', 'r')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'w') inv.write_xml(tmpf) tmpf.close() os.rename(tmpfname, self.controlfilename('inventory')) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. This puts the files in the Added state, so that they will be recorded by the next commit. :todo: Perhaps have an option to add the ids even if the files do not (yet) exist. :todo: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. :todo: Option to specify file id. :todo: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) if isfile(fullpath): kind = 'file' elif isdir(fullpath): kind = 'directory' else: bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if len(fp) > 1: parent_name = joinpath(fp[:-1]) mutter("lookup parent %r" % parent_name) parent_id = inv.path2id(parent_name) if parent_id == None: bailout("cannot add: parent %r is not versioned" % joinpath(fp[:-1])) else: parent_id = None file_id = _gen_file_id(fp[-1]) inv.add(InventoryEntry(file_id, fp[-1], kind=kind, parent_id=parent_id)) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r parent_id={%s}" % (f, file_id, kind, parent_id)) self._write_inventory(inv) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on :todo: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True :todo: Do something useful with directories. :todo: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. :param timestamp: if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = _gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'R' else: state = 'M' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) mutter("append to revision-history") self.controlfile('revision-history', 'at').write(rev_id + '\n') mutter("done!") def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. :todo: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: bailout("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original'): """Write out human-readable log of commits to this branch :param utc: If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l revno += 1 precursor = p def show_status(branch, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo :todo: Get state for single files. :todo: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = branch.basis_tree() old_inv = old.inventory new = branch.working_tree() new_inv = new.inventory for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("wierd file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files = []): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def _gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" assert '/' not in name while name[0] == '.': name = name[1:] s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/tests.py data 4853 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # XXX: We might prefer these to be in a text file rather than Python # source, but that only works in doctest from Python 2.4 and later, # which is not present in Warty. r""" Bazaar-NG test cases ******************** These are run by ``bzr.doctest``. >>> import bzrlib, os >>> bzrlib.commands.cmd_rocks() it sure does! Hey, nice place to begin. The basic object is a Branch. We have a special helper class ScratchBranch that automatically makes a directory and cleans itself up, but is in other respects identical. ScratchBranches are initially empty: >>> b = bzrlib.ScratchBranch() >>> b.show_status() New files in that directory are, it is initially unknown: >>> file(b.base + '/hello.c', 'wt').write('int main() {}') >>> b.show_status() ? hello.c That's not quite true; some files (like editor backups) are ignored by default: >>> file(b.base + '/hello.c~', 'wt').write('int main() {}') >>> b.show_status() ? hello.c >>> list(b.unknowns()) ['hello.c'] The ``add`` command marks a file to be added in the next revision: >>> b.add('hello.c') >>> b.show_status() A hello.c You can also add files that otherwise would be ignored. The ignore patterns only apply to files that would be otherwise unknown, so they have no effect once it's added. >>> b.add('hello.c~') >>> b.show_status() A hello.c A hello.c~ It is an error to add a file that isn't present in the working copy: >>> b.add('nothere') Traceback (most recent call last): ... BzrError: ('cannot add: not a regular file or directory: nothere', []) If we add a file and then change our mind, we can either revert it or remove the file. If we revert, we are left with the working copy (in either I or ? state). If we remove, the working copy is gone. Let's do that to the backup, presumably added accidentally. >>> b.remove('hello.c~') >>> b.show_status() A hello.c Now to commit, creating a new revision. (Fake the date and name for reproducibility.) >>> b.commit('start hello world', timestamp=0, committer='foo@nowhere') >>> b.show_status() >>> b.show_status(show_all=True) . hello.c I hello.c~ We can look back at history >>> r = b.get_revision(b.lookup_revision(1)) >>> r.message 'start hello world' >>> b.write_log(show_timezone='utc') ---------------------------------------- revno: 1 committer: foo@nowhere timestamp: Thu 1970-01-01 00:00:00 +0000 message: start hello world (The other fields will be a bit unpredictable, depending on who ran this test and when.) As of 2005-02-21, we can also add subdirectories to the revision! >>> os.mkdir(b.base + "/lib") >>> b.show_status() ? lib/ >>> b.add('lib') >>> b.show_status() A lib/ >>> b.commit('add subdir') >>> b.show_status() >>> b.show_status(show_all=True) . hello.c I hello.c~ . lib/ and we can also add files within subdirectories: >>> file(b.base + '/lib/hello', 'w').write('hello!\n') >>> b.show_status() ? lib/hello Tests for adding subdirectories, etc. >>> b = bzrlib.branch.ScratchBranch() >>> os.mkdir(b.abspath('d1')) >>> os.mkdir(b.abspath('d2')) >>> os.mkdir(b.abspath('d2/d3')) >>> list(b.working_tree().unknowns()) ['d1', 'd2'] Create some files, but they're not seen as unknown yet: >>> file(b.abspath('d1/f1'), 'w').close() >>> file(b.abspath('d2/f2'), 'w').close() >>> file(b.abspath('d2/f3'), 'w').close() >>> [v[0] for v in b.inventory.directories()] [''] >>> list(b.working_tree().unknowns()) ['d1', 'd2'] Adding a directory, and we see the file underneath: >>> b.add('d1') >>> [v[0] for v in b.inventory.directories()] ['', 'd1'] >>> list(b.working_tree().unknowns()) ['d1/f1', 'd2'] >>> # d2 comes first because it's in the top directory >>> b.add('d2') >>> b.commit('add some stuff') >>> list(b.working_tree().unknowns()) ['d1/f1', 'd2/d3', 'd2/f2', 'd2/f3'] >>> b.add('d1/f1') >>> list(b.working_tree().unknowns()) ['d2/d3', 'd2/f2', 'd2/f3'] """ M 644 inline bzrlib/tree.py data 12626 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tree classes, representing directory at point in time. """ from sets import Set import os.path, os, fnmatch from inventory import Inventory from trace import mutter, note from osutils import pumpfile, compare_files, filesize, quotefn, sha_file, \ joinpath, splitpath, appendpath, isdir, isfile, file_kind from errors import bailout import branch from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE import bzrlib class Tree: """Abstract file tree. There are several subclasses: * `WorkingTree` exists as files on disk editable by the user. * `RevisionTree` is a tree as recorded at some point in the past. * `EmptyTree` Trees contain an `Inventory` object, and also know how to retrieve file texts mentioned in the inventory, either from a working directory or from a store. It is possible for trees to contain files that are not described in their inventory or vice versa; for this use `filenames()`. Trees can be compared, etc, regardless of whether they are working trees or versioned trees. """ def has_filename(self, filename): """True if the tree has given filename.""" raise NotImplementedError() def has_id(self, file_id): return self.inventory.has_id(file_id) def id_set(self): """Return set of all ids in this tree.""" return self.inventory.id_set() def id2path(self, file_id): return self.inventory.id2path(file_id) def _get_inventory(self): return self._inventory inventory = property(_get_inventory, doc="Inventory of this Tree") def _check_retrieved(self, ie, f): # TODO: Test this check by damaging the store? if ie.text_size is not None: fs = filesize(f) if fs != ie.text_size: bailout("mismatched size for file %r in %r" % (ie.file_id, self._store), ["inventory expects %d bytes" % ie.text_size, "file is actually %d bytes" % fs, "store is probably damaged/corrupt"]) f_hash = sha_file(f) f.seek(0) if ie.text_sha1 != f_hash: bailout("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store), ["inventory expects %s" % ie.text_sha1, "file is actually %s" % f_hash, "store is probably damaged/corrupt"]) def export(self, dest): """Export this tree to a new directory. `dest` should not exist, and will be created holding the contents of this tree. :todo: To handle subdirectories we need to create the directories first. :note: If the export fails, the destination directory will be left in a half-assed state. """ os.mkdir(dest) mutter('export version %r' % self) inv = self.inventory for dp, ie in inv.iter_entries(): kind = ie.kind fullpath = appendpath(dest, dp) if kind == 'directory': os.mkdir(fullpath) elif kind == 'file': pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb')) else: bailout("don't know how to export {%s} of kind %r", fid, kind) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) class WorkingTree(Tree): """Working copy tree. The inventory is held in the `Branch` working-inventory, and the files are in a directory on disk. It is possible for a `WorkingTree` to have a filename which is not listed in the Inventory and vice versa. """ def __init__(self, basedir, inv): self._inventory = inv self.basedir = basedir self.path2id = inv.path2id def __repr__(self): return "<%s of %s>" % (self.__class__.__name__, self.basedir) def abspath(self, filename): return os.path.join(self.basedir, filename) def has_filename(self, filename): return os.path.exists(self.abspath(filename)) def get_file(self, file_id): return self.get_file_byname(self.id2path(file_id)) def get_file_byname(self, filename): return file(self.abspath(filename), 'rb') def _get_store_filename(self, file_id): return self.abspath(self.id2path(file_id)) def has_id(self, file_id): # files that have been deleted are excluded if not self.inventory.has_id(file_id): return False return os.access(self.abspath(self.inventory.id2path(file_id)), os.F_OK) def get_file_size(self, file_id): return os.stat(self._get_store_filename(file_id))[ST_SIZE] def get_file_sha1(self, file_id): f = self.get_file(file_id) return sha_file(f) def file_class(self, filename): if self.path2id(filename): return 'V' elif self.is_ignored(filename): return 'I' else: return '?' def file_kind(self, filename): if isfile(self.abspath(filename)): return 'file' elif isdir(self.abspath(filename)): return 'directory' else: return 'unknown' def list_files(self): """Recursively list all files as (path, class, kind, id). Lists, but does not descend into unversioned directories. This does not include files that have been deleted in this tree. Skips the control directory. """ inv = self.inventory def descend(from_dir, from_dir_id, dp): ls = os.listdir(dp) ls.sort() for f in ls: if bzrlib.BZRDIR == f: continue # path within tree fp = appendpath(from_dir, f) # absolute path fap = appendpath(dp, f) f_ie = inv.get_child(from_dir_id, f) if f_ie: c = 'V' elif self.is_ignored(fp): c = 'I' else: c = '?' fk = file_kind(fap) if f_ie: if f_ie.kind != fk: bailout("file %r entered as kind %r id %r, now of kind %r" % (fap, f_ie.kind, f_ie.file_id, fk)) yield fp, c, fk, (f_ie and f_ie.file_id) if fk != 'directory': continue if c != 'V': # don't descend unversioned directories continue for ff in descend(fp, f_ie.file_id, fap): yield ff for f in descend('', None, self.basedir): yield f def unknowns(self, path='', dir_id=None): """Yield names of unknown files in this WorkingTree. If there are any unknown directories then only the directory is returned, not all its children. But if there are unknown files under a versioned subdirectory, they are returned. Currently returned depth-first, sorted by name within directories. """ for fpath, fclass, fkind, fid in self.list_files(): if fclass == '?': yield fpath def ignored_files(self): for fpath, fclass, fkind, fid in self.list_files(): if fclass == 'I': yield fpath def get_ignore_list(self): """Return list of ignore patterns.""" if self.has_filename(bzrlib.IGNORE_FILENAME): f = self.get_file_byname(bzrlib.IGNORE_FILENAME) return [line.rstrip("\n\r") for line in f.readlines()] else: return bzrlib.DEFAULT_IGNORE def is_ignored(self, filename): """Check whether the filename matches an ignore pattern. Patterns containing '/' need to match the whole path; others match against only the last component.""" ## TODO: Take them from a file, not hardcoded ## TODO: Use extended zsh-style globs maybe? ## TODO: Use '**' to match directories? for pat in self.get_ignore_list(): if '/' in pat: if fnmatch.fnmatchcase(filename, pat): return True else: if fnmatch.fnmatchcase(splitpath(filename)[-1], pat): return True return False class RevisionTree(Tree): """Tree viewing a previous revision. File text can be retrieved from the text store. :todo: Some kind of `__repr__` method, but a good one probably means knowing the branch and revision number, or at least passing a description to the constructor. """ def __init__(self, store, inv): self._store = store self._inventory = inv def get_file(self, file_id): ie = self._inventory[file_id] f = self._store[ie.text_id] mutter(" get fileid{%s} from %r" % (file_id, self)) fs = filesize(f) if ie.text_size is None: note("warning: no text size recorded on %r" % ie) self._check_retrieved(ie, f) return f def get_file_size(self, file_id): return self._inventory[file_id].text_size def get_file_sha1(self, file_id): ie = self._inventory[file_id] return ie.text_sha1 def has_filename(self, filename): return bool(self.inventory.path2id(filename)) def list_files(self): # The only files returned by this are those from the version for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id class EmptyTree(Tree): def __init__(self): self._inventory = Inventory() def has_filename(self, filename): return False def list_files(self): if False: # just to make it a generator yield None ###################################################################### # diff # TODO: Merge these two functions into a single one that can operate # on either a whole tree or a set of files. # TODO: Return the diff in order by filename, not by category or in # random order. Can probably be done by lock-stepping through the # filenames from both trees. def file_status(filename, old_tree, new_tree): """Return single-letter status, old and new names for a file. The complexity here is in deciding how to represent renames; many complex cases are possible. """ old_inv = old_tree.inventory new_inv = new_tree.inventory new_id = new_inv.path2id(filename) old_id = old_inv.path2id(filename) if not new_id and not old_id: # easy: doesn't exist in either; not versioned at all if new_tree.is_ignored(filename): return 'I', None, None else: return '?', None, None elif new_id: # There is now a file of this name, great. pass else: # There is no longer a file of this name, but we can describe # what happened to the file that used to have # this name. There are two possibilities: either it was # deleted entirely, or renamed. assert old_id if new_inv.has_id(old_id): return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id) else: return 'D', old_inv.id2path(old_id), None # if the file_id is new in this revision, it is added if new_id and not old_inv.has_id(new_id): return 'A' # if there used to be a file of this name, but that ID has now # disappeared, it is deleted if old_id and not new_inv.has_id(old_id): return 'D' return 'wtf?' commit refs/heads/master mark :68 committer 1111478450 +1100 data 34 - new relpath command and function from :67 M 644 inline bzrlib/branch.py data 27656 # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset from store import ImmutableStore from revision import Revision from errors import bailout from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f is None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f last_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be bailout('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. :todo: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. :todo: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. :todo: Keep the on-disk branch locked while the object exists. :todo: mkdir() method. """ def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. :param base: Base directory for the branch. :param init: If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. :param find_root: If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch""" return file(self.controlfilename(file_or_path), mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # read in binary mode to detect newline wierdness. fmt = self.controlfile('branch-format', 'rb').read() if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() inv = Inventory.read_xml(self.controlfile('inventory', 'r')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'w') inv.write_xml(tmpf) tmpf.close() os.rename(tmpfname, self.controlfilename('inventory')) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. This puts the files in the Added state, so that they will be recorded by the next commit. :todo: Perhaps have an option to add the ids even if the files do not (yet) exist. :todo: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. :todo: Option to specify file id. :todo: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) if isfile(fullpath): kind = 'file' elif isdir(fullpath): kind = 'directory' else: bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if len(fp) > 1: parent_name = joinpath(fp[:-1]) mutter("lookup parent %r" % parent_name) parent_id = inv.path2id(parent_name) if parent_id == None: bailout("cannot add: parent %r is not versioned" % joinpath(fp[:-1])) else: parent_id = None file_id = _gen_file_id(fp[-1]) inv.add(InventoryEntry(file_id, fp[-1], kind=kind, parent_id=parent_id)) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r parent_id={%s}" % (f, file_id, kind, parent_id)) self._write_inventory(inv) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on :todo: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True :todo: Do something useful with directories. :todo: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. :param timestamp: if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = _gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'R' else: state = 'M' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) mutter("append to revision-history") self.controlfile('revision-history', 'at').write(rev_id + '\n') mutter("done!") def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. :todo: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: bailout("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original'): """Write out human-readable log of commits to this branch :param utc: If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l revno += 1 precursor = p def show_status(branch, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo :todo: Get state for single files. :todo: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = branch.basis_tree() old_inv = old.inventory new = branch.working_tree() new_inv = new.inventory for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("wierd file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files = []): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def _gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" assert '/' not in name while name[0] == '.': name = name[1:] s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/commands.py data 23192 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. """ # not currently working: # bzr info # Show some information about this branch. __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files. Fails if the files are already added. """ assert file_list b = Branch(file_list[0], find_root=True) b.add(file_list, verbose=verbose) def cmd_relpath(filename): print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): b = Branch('.') print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl is not None: return pl else: return 's' count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %5d %s' % (count_status[fs], name) print ' %5d versioned subdirector%s' % (count_version_dirs, plural(count_version_dirs, 'y', 'ies')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %5d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %5d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %5d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) def cmd_remove(file_list, verbose=False): Branch('.').remove(file_list, verbose=verbose) def cmd_file_id(filename): i = Branch('.').read_working_inventory().path2id(filename) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'root': ['filename?'], 'relpath': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :69 committer Martin Pool 1111491782 +1100 data 86 handle add, remove, file-id being given filenames that are not relative to branch root from :68 M 644 inline bzrlib/commands.py data 23270 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. """ # not currently working: # bzr info # Show some information about this branch. __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files. Fails if the files are already added. """ assert file_list b = Branch(file_list[0], find_root=True) b.add([b.relpath(f) for f in file_list], verbose=verbose) def cmd_relpath(filename): print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): b = Branch('.') print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl is not None: return pl else: return 's' count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %5d %s' % (count_status[fs], name) print ' %5d versioned subdirector%s' % (count_version_dirs, plural(count_version_dirs, 'y', 'ies')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %5d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %5d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %5d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'root': ['filename?'], 'relpath': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :70 committer 1111547390 +1100 data 292 Prepare for smart recursive add. - New Inventory.add_path, so that callers don't need to know so much about path lookup. - Make gen_file_id public. - New add module and smart_add function; does previous add operations more cleanly but not recursive mode yet. - New warning() function. from :69 M 644 inline bzrlib/add.py data 2412 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import types, os, sys, stat import bzrlib from osutils import quotefn from errors import bailout def smart_add(file_list, verbose=False, recurse=False): """Add files to version, optionall recursing into directories. This is designed more towards DWIM for humans than API simplicity. For the specific behaviour see the help for cmd_add(). """ assert file_list assert not isinstance(file_list, types.StringTypes) b = bzrlib.branch.Branch(file_list[0], find_root=True) inv = b.read_working_inventory() dirty = False for f in file_list: rf = b.relpath(f) af = b.abspath(rf) bzrlib.mutter("smart add of %r" % f) if bzrlib.branch.is_control_file(af): bailout("cannot add control file %r" % af) kind = bzrlib.osutils.file_kind(f) if kind == 'file': if inv.path2id(rf): bzrlib.warning("%r is already versioned" % f) continue elif kind == 'directory': if inv.path2id(rf): if not recurse: bzrlib.warning("%r is already versioned" % f) continue else: # TODO: don't add, but recurse down continue else: bailout("can't smart_add file kind %r" % kind) file_id = bzrlib.branch.gen_file_id(rf) inv.add_path(rf, kind=kind, file_id=file_id) bzrlib.mutter("added %r kind %r file_id={%s}" % (rf, kind, file_id)) dirty = True if verbose: bzrlib.textui.show_status('A', kind, quotefn(f)) if dirty: b._write_inventory(inv) M 644 inline bzrlib/__init__.py data 1134 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr library""" from inventory import Inventory, InventoryEntry from branch import Branch, ScratchBranch from osutils import format_date from tree import Tree from diff import diff_trees from trace import mutter, warning import add BZRDIR = ".bzr" DEFAULT_IGNORE = ['.*', '*~', '#*#', '*.tmp', '*.o', '*.a', '*.py[oc]', '{arch}'] IGNORE_FILENAME = ".bzrignore" M 644 inline bzrlib/branch.py data 27509 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset from store import ImmutableStore from revision import Revision from errors import bailout from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f is None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f last_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be bailout('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. :todo: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. :todo: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. :todo: Keep the on-disk branch locked while the object exists. :todo: mkdir() method. """ def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. :param base: Base directory for the branch. :param init: If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. :param find_root: If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch""" return file(self.controlfilename(file_or_path), mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # read in binary mode to detect newline wierdness. fmt = self.controlfile('branch-format', 'rb').read() if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() inv = Inventory.read_xml(self.controlfile('inventory', 'r')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'w') inv.write_xml(tmpf) tmpf.close() os.rename(tmpfname, self.controlfilename('inventory')) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. This puts the files in the Added state, so that they will be recorded by the next commit. :todo: Perhaps have an option to add the ids even if the files do not (yet) exist. :todo: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. :todo: Option to specify file id. :todo: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on :todo: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True :todo: Do something useful with directories. :todo: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. :param timestamp: if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'R' else: state = 'M' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) mutter("append to revision-history") self.controlfile('revision-history', 'at').write(rev_id + '\n') mutter("done!") def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. :todo: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: bailout("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original'): """Write out human-readable log of commits to this branch :param utc: If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l revno += 1 precursor = p def show_status(branch, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo :todo: Get state for single files. :todo: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = branch.basis_tree() old_inv = old.inventory new = branch.working_tree() new_inv = new.inventory for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("wierd file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files = []): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/commands.py data 24065 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. """ # not currently working: # bzr info # Show some information about this branch. __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ if True: bzrlib.add.smart_add(file_list, verbose) else: # old way assert file_list b = Branch(file_list[0], find_root=True) b.add([b.relpath(f) for f in file_list], verbose=verbose) def cmd_relpath(filename): print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): b = Branch('.') print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl is not None: return pl else: return 's' count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %5d %s' % (count_status[fs], name) print ' %5d versioned subdirector%s' % (count_version_dirs, plural(count_version_dirs, 'y', 'ies')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %5d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %5d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %5d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'root': ['filename?'], 'relpath': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') M 644 inline bzrlib/inventory.py data 15542 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Inventories map files to their name in a revision.""" __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os.path, types from sets import Set try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from xml import XMLMixin from errors import bailout import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') >>> i.add(InventoryEntry('123', 'src', kind='directory')) >>> i.add(InventoryEntry('2323', 'hello.c', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id=None)) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', parent_id='123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', parent_id='123')) >>> i.add(InventoryEntry('2325', 'wibble', parent_id='123', kind='directory')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', parent_id='2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' :todo: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ def __init__(self, file_id, name, kind='file', text_id=None, parent_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c') >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c') Traceback (most recent call last): BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", []) """ if len(splitpath(name)) != 1: bailout('InventoryEntry name is not a simple filename: %r' % name) self.file_id = file_id self.name = name assert kind in ['file', 'directory'] self.kind = kind self.text_id = text_id self.parent_id = parent_id self.text_sha1 = None self.text_size = None def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.text_id, self.parent_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size is not None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1', 'parent_id']: v = getattr(self, f) if v is not None: e.set(f, v) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind')) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') self.parent_id = elt.get('parent_id') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __cmp__(self, other): if self is other: return 0 if not isinstance(other, InventoryEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.name, other.name) \ or cmp(self.text_sha1, other.text_sha1) \ or cmp(self.text_size, other.text_size) \ or cmp(self.text_id, other.text_id) \ or cmp(self.parent_id, other.parent_id) \ or cmp(self.kind, other.kind) class Inventory(XMLMixin): """Inventory of versioned files in a tree. An Inventory acts like a set of InventoryEntry items. You can also look files up by their file_id or name. May be read from and written to a metadata file in a tree. To manipulate the inventory (for example to add a file), it is read in, modified, and then written back out. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c')) >>> inv['123-123'].name 'hello.c' >>> for file_id in inv: print file_id ... 123-123 May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ ## TODO: Clear up handling of files in subdirectories; we probably ## do want to be able to just look them up by name but this ## probably means gradually walking down the path, looking up as we go. ## TODO: Make sure only canonical filenames are stored. ## TODO: Do something sensible about the possible collisions on ## case-losing filesystems. Perhaps we should just always forbid ## such collisions. ## _tree should probably just be stored as ## InventoryEntry._children on each directory. def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. """ self._byid = dict() # _tree is indexed by parent_id; at each level a map from name # to ie. The None entry is the root. self._tree = {None: {}} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, parent_id=None): """Return (path, entry) pairs, in order by name.""" kids = self._tree[parent_id].items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(parent_id=ie.file_id): yield joinpath([name, cn]), cie def directories(self, include_root=True): """Return (path, entry) pairs for all directories. """ if include_root: yield '', None for path, entry in self.iter_entries(): if entry.kind == 'directory': yield path, entry def children(self, parent_id): """Return entries that are direct children of parent_id.""" return self._tree[parent_id] # TODO: return all paths and entries def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c')) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c')) >>> inv['123123'].name 'hello.c' """ return self._byid[file_id] def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self: bailout("inventory already contains entry with id {%s}" % entry.file_id) if entry.parent_id != None: if entry.parent_id not in self: bailout("parent_id %s of new entry not found in inventory" % entry.parent_id) if self._tree[entry.parent_id].has_key(entry.name): bailout("%s is already versioned" % appendpath(self.id2path(entry.parent_id), entry.name)) self._byid[entry.file_id] = entry self._tree[entry.parent_id][entry.name] = entry if entry.kind == 'directory': self._tree[entry.file_id] = {} def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: bailout("cannot re-add root of inventory") if file_id is None: file_id = bzrlib.branch.gen_file_id(relpath) parent_id = self.path2id(parts[:-1]) ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c')) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self._tree[ie.parent_id][ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in self._tree[file_id].values(): del self[cie.file_id] del self._tree[file_id] del self._byid[file_id] del self._tree[ie.parent_id][ie.name] def id_set(self): return Set(self._byid) def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c')) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __cmp__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo')) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo')) >>> i1 == i2 True """ if self is other: return 0 if not isinstance(other, Inventory): return NotImplemented if self.id_set() ^ other.id_set(): return 1 for file_id in self._byid: c = cmp(self[file_id], other[file_id]) if c: return c return 0 def id2path(self, file_id): """Return as a list the path to file_id.""" p = [] while file_id != None: ie = self[file_id] p = [ie.name] + p file_id = ie.parent_id return joinpath(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. """ if isinstance(name, types.StringTypes): name = splitpath(name) parent_id = None for f in name: try: cie = self._tree[parent_id][f] assert cie.name == f parent_id = cie.file_id except KeyError: # or raise an error? return None return parent_id def get_child(self, parent_id, child_name): return self._tree[parent_id].get(child_name) def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): assert isinstance(file_id, str) return self._byid.has_key(file_id) if __name__ == '__main__': import doctest, inventory doctest.testmod(inventory) M 644 inline bzrlib/trace.py data 3415 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os, time, socket, stat import bzrlib ###################################################################### # messages and logging ## TODO: If --verbose is given then write to both stderr and ## _tracefile; perhaps replace _tracefile with a tee thing. global _tracefile, _starttime # used to have % (os.environ['USER'], time.time(), os.getpid()), 'w') # If false, notes also go to stdout; should replace this with --silent # at some point. silent = False verbose = False def warning(msg): b = 'bzr: warning: ' + msg + '\n' sys.stderr.write(b) _tracefile.write(b) _tracefile.flush() def mutter(msg): _tracefile.write(msg) _tracefile.write('\n') _tracefile.flush() if verbose: sys.stderr.write('- ' + msg + '\n') def note(msg): b = '* ' + str(msg) + '\n' if not silent: sys.stderr.write(b) _tracefile.write(b) _tracefile.flush() def log_error(msg): sys.stderr.write(msg) _tracefile.write(msg) _tracefile.flush() def create_tracefile(argv): # TODO: Also show contents of /etc/lsb-release, if it can be parsed. # Perhaps that should eventually go into the platform library? # TODO: If the file doesn't exist, add a note describing it. # Messages are always written to here, so that we have some # information if something goes wrong. In a future version this # file will be removed on successful completion. global _starttime, _tracefile _starttime = os.times()[4] _tracefile = file('.bzr.log', 'at') t = _tracefile if os.fstat(t.fileno())[stat.ST_SIZE] == 0: t.write("\nthis is a debug log for diagnosing/reporting problems in bzr\n") t.write("you can delete or truncate this file, or include sections in\n") t.write("bug reports to bazaar-ng@lists.canonical.com\n\n") # TODO: If we failed to create the file, perhaps give a warning # but don't abort; send things to /dev/null instead? t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % bzrlib.osutils.format_date(time.time())) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.getfqdn())) t.write(' arguments: %r\n' % argv) import platform t.write(' platform: %s\n' % platform.platform()) t.write(' python: %s\n' % platform.python_version()) import atexit atexit.register(_close_trace) def _close_trace(): times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum, %.3f elapsed" % (times[:4] + ((times[4] - _starttime),))) M 644 inline bzrlib/tree.py data 12614 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tree classes, representing directory at point in time. """ from sets import Set import os.path, os, fnmatch from inventory import Inventory from trace import mutter, note from osutils import pumpfile, compare_files, filesize, quotefn, sha_file, \ joinpath, splitpath, appendpath, isdir, isfile, file_kind from errors import bailout import branch from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE import bzrlib class Tree: """Abstract file tree. There are several subclasses: * `WorkingTree` exists as files on disk editable by the user. * `RevisionTree` is a tree as recorded at some point in the past. * `EmptyTree` Trees contain an `Inventory` object, and also know how to retrieve file texts mentioned in the inventory, either from a working directory or from a store. It is possible for trees to contain files that are not described in their inventory or vice versa; for this use `filenames()`. Trees can be compared, etc, regardless of whether they are working trees or versioned trees. """ def has_filename(self, filename): """True if the tree has given filename.""" raise NotImplementedError() def has_id(self, file_id): return self.inventory.has_id(file_id) def id_set(self): """Return set of all ids in this tree.""" return self.inventory.id_set() def id2path(self, file_id): return self.inventory.id2path(file_id) def _get_inventory(self): return self._inventory inventory = property(_get_inventory, doc="Inventory of this Tree") def _check_retrieved(self, ie, f): # TODO: Test this check by damaging the store? if ie.text_size is not None: fs = filesize(f) if fs != ie.text_size: bailout("mismatched size for file %r in %r" % (ie.file_id, self._store), ["inventory expects %d bytes" % ie.text_size, "file is actually %d bytes" % fs, "store is probably damaged/corrupt"]) f_hash = sha_file(f) f.seek(0) if ie.text_sha1 != f_hash: bailout("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store), ["inventory expects %s" % ie.text_sha1, "file is actually %s" % f_hash, "store is probably damaged/corrupt"]) def export(self, dest): """Export this tree to a new directory. `dest` should not exist, and will be created holding the contents of this tree. :todo: To handle subdirectories we need to create the directories first. :note: If the export fails, the destination directory will be left in a half-assed state. """ os.mkdir(dest) mutter('export version %r' % self) inv = self.inventory for dp, ie in inv.iter_entries(): kind = ie.kind fullpath = appendpath(dest, dp) if kind == 'directory': os.mkdir(fullpath) elif kind == 'file': pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb')) else: bailout("don't know how to export {%s} of kind %r", fid, kind) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) class WorkingTree(Tree): """Working copy tree. The inventory is held in the `Branch` working-inventory, and the files are in a directory on disk. It is possible for a `WorkingTree` to have a filename which is not listed in the Inventory and vice versa. """ def __init__(self, basedir, inv): self._inventory = inv self.basedir = basedir self.path2id = inv.path2id def __repr__(self): return "<%s of %s>" % (self.__class__.__name__, self.basedir) def abspath(self, filename): return os.path.join(self.basedir, filename) def has_filename(self, filename): return os.path.exists(self.abspath(filename)) def get_file(self, file_id): return self.get_file_byname(self.id2path(file_id)) def get_file_byname(self, filename): return file(self.abspath(filename), 'rb') def _get_store_filename(self, file_id): return self.abspath(self.id2path(file_id)) def has_id(self, file_id): # files that have been deleted are excluded if not self.inventory.has_id(file_id): return False return os.access(self.abspath(self.inventory.id2path(file_id)), os.F_OK) def get_file_size(self, file_id): return os.stat(self._get_store_filename(file_id))[ST_SIZE] def get_file_sha1(self, file_id): f = self.get_file(file_id) return sha_file(f) def file_class(self, filename): if self.path2id(filename): return 'V' elif self.is_ignored(filename): return 'I' else: return '?' def file_kind(self, filename): if isfile(self.abspath(filename)): return 'file' elif isdir(self.abspath(filename)): return 'directory' else: return 'unknown' def list_files(self): """Recursively list all files as (path, class, kind, id). Lists, but does not descend into unversioned directories. This does not include files that have been deleted in this tree. Skips the control directory. """ inv = self.inventory def descend(from_dir, from_dir_id, dp): ls = os.listdir(dp) ls.sort() for f in ls: if bzrlib.BZRDIR == f: continue # path within tree fp = appendpath(from_dir, f) # absolute path fap = appendpath(dp, f) f_ie = inv.get_child(from_dir_id, f) if f_ie: c = 'V' elif self.is_ignored(fp): c = 'I' else: c = '?' fk = file_kind(fap) if f_ie: if f_ie.kind != fk: bailout("file %r entered as kind %r id %r, now of kind %r" % (fap, f_ie.kind, f_ie.file_id, fk)) yield fp, c, fk, (f_ie and f_ie.file_id) if fk != 'directory': continue if c != 'V': # don't descend unversioned directories continue for ff in descend(fp, f_ie.file_id, fap): yield ff for f in descend('', None, self.basedir): yield f def unknowns(self, path='', dir_id=None): """Yield names of unknown files in this WorkingTree. If there are any unknown directories then only the directory is returned, not all its children. But if there are unknown files under a versioned subdirectory, they are returned. Currently returned depth-first, sorted by name within directories. """ for fpath, fclass, fkind, fid in self.list_files(): if fclass == '?': yield fpath def ignored_files(self): for fpath, fclass, fkind, fid in self.list_files(): if fclass == 'I': yield fpath def get_ignore_list(self): """Return list of ignore patterns.""" if self.has_filename(bzrlib.IGNORE_FILENAME): f = self.get_file_byname(bzrlib.IGNORE_FILENAME) return [line.rstrip("\n\r") for line in f.readlines()] else: return bzrlib.DEFAULT_IGNORE def is_ignored(self, filename): """Check whether the filename matches an ignore pattern. Patterns containing '/' need to match the whole path; others match against only the last component.""" ## TODO: Take them from a file, not hardcoded ## TODO: Use extended zsh-style globs maybe? ## TODO: Use '**' to match directories? for pat in self.get_ignore_list(): if '/' in pat: if fnmatch.fnmatchcase(filename, pat): return True else: if fnmatch.fnmatchcase(splitpath(filename)[-1], pat): return True return False class RevisionTree(Tree): """Tree viewing a previous revision. File text can be retrieved from the text store. :todo: Some kind of `__repr__` method, but a good one probably means knowing the branch and revision number, or at least passing a description to the constructor. """ def __init__(self, store, inv): self._store = store self._inventory = inv def get_file(self, file_id): ie = self._inventory[file_id] f = self._store[ie.text_id] mutter(" get fileid{%s} from %r" % (file_id, self)) fs = filesize(f) if ie.text_size is None: note("warning: no text size recorded on %r" % ie) self._check_retrieved(ie, f) return f def get_file_size(self, file_id): return self._inventory[file_id].text_size def get_file_sha1(self, file_id): ie = self._inventory[file_id] return ie.text_sha1 def has_filename(self, filename): return bool(self.inventory.path2id(filename)) def list_files(self): # The only files returned by this are those from the version for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id class EmptyTree(Tree): def __init__(self): self._inventory = Inventory() def has_filename(self, filename): return False def list_files(self): if False: # just to make it a generator yield None ###################################################################### # diff # TODO: Merge these two functions into a single one that can operate # on either a whole tree or a set of files. # TODO: Return the diff in order by filename, not by category or in # random order. Can probably be done by lock-stepping through the # filenames from both trees. def file_status(filename, old_tree, new_tree): """Return single-letter status, old and new names for a file. The complexity here is in deciding how to represent renames; many complex cases are possible. """ old_inv = old_tree.inventory new_inv = new_tree.inventory new_id = new_inv.path2id(filename) old_id = old_inv.path2id(filename) if not new_id and not old_id: # easy: doesn't exist in either; not versioned at all if new_tree.is_ignored(filename): return 'I', None, None else: return '?', None, None elif new_id: # There is now a file of this name, great. pass else: # There is no longer a file of this name, but we can describe # what happened to the file that used to have # this name. There are two possibilities: either it was # deleted entirely, or renamed. assert old_id if new_inv.has_id(old_id): return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id) else: return 'D', old_inv.id2path(old_id), None # if the file_id is new in this revision, it is added if new_id and not old_inv.has_id(new_id): return 'A' # if there used to be a file of this name, but that ID has now # disappeared, it is deleted if old_id and not new_inv.has_id(old_id): return 'D' return 'wtf?' commit refs/heads/master mark :71 committer 1111557403 +1100 data 13 Add NEWS file from :70 M 644 inline NEWS data 164 bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. commit refs/heads/master mark :72 committer 1111559155 +1100 data 56 - import a subset of elementtree for easier installation from :71 M 644 inline elementtree/ElementTree.py data 36765 # # ElementTree # $Id: ElementTree.py 1862 2004-06-18 07:31:02Z Fredrik $ # # light-weight XML support for Python 1.5.2 and later. # # this is a stripped-down version of Secret Labs' effDOM library (part # of xmlToolkit). compared to effDOM, this implementation has: # # - no support for observers # - no html-specific extensions (e.g. entity preload) # - no custom entities, doctypes, etc # - no accelerator module # # history: # 2001-10-20 fl created (from various sources) # 2001-11-01 fl return root from parse method # 2002-02-16 fl sort attributes in lexical order # 2002-04-06 fl TreeBuilder refactoring, added PythonDoc markup # 2002-05-01 fl finished TreeBuilder refactoring # 2002-07-14 fl added basic namespace support to ElementTree.write # 2002-07-25 fl added QName attribute support # 2002-10-20 fl fixed encoding in write # 2002-11-24 fl changed default encoding to ascii; fixed attribute encoding # 2002-11-27 fl accept file objects or file names for parse/write # 2002-12-04 fl moved XMLTreeBuilder back to this module # 2003-01-11 fl fixed entity encoding glitch for us-ascii # 2003-02-13 fl added XML literal factory # 2003-02-21 fl added ProcessingInstruction/PI factory # 2003-05-11 fl added tostring/fromstring helpers # 2003-05-26 fl added ElementPath support # 2003-07-05 fl added makeelement factory method # 2003-07-28 fl added more well-known namespace prefixes # 2003-08-15 fl fixed typo in ElementTree.findtext (Thomas Dartsch) # 2003-09-04 fl fall back on emulator if ElementPath is not installed # 2003-10-31 fl markup updates # 2003-11-15 fl fixed nested namespace bug # 2004-03-28 fl added XMLID helper # 2004-06-02 fl added default support to findtext # 2004-06-08 fl fixed encoding of non-ascii element/attribute names # # Copyright (c) 1999-2004 by Fredrik Lundh. All rights reserved. # # fredrik@pythonware.com # http://www.pythonware.com # # -------------------------------------------------------------------- # The ElementTree toolkit is # # Copyright (c) 1999-2004 by Fredrik Lundh # # By obtaining, using, and/or copying this software and/or its # associated documentation, you agree that you have read, understood, # and will comply with the following terms and conditions: # # Permission to use, copy, modify, and distribute this software and # its associated documentation for any purpose and without fee is # hereby granted, provided that the above copyright notice appears in # all copies, and that both that copyright notice and this permission # notice appear in supporting documentation, and that the name of # Secret Labs AB or the author not be used in advertising or publicity # pertaining to distribution of the software without specific, written # prior permission. # # SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD # TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- # ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR # BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS # ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE # OF THIS SOFTWARE. # -------------------------------------------------------------------- __all__ = [ # public symbols "Comment", "dump", "Element", "ElementTree", "fromstring", "iselement", "parse", "PI", "ProcessingInstruction", "QName", "SubElement", "tostring", "TreeBuilder", "VERSION", "XML", "XMLTreeBuilder", ] ## # The Element type is a flexible container object, designed to # store hierarchical data structures in memory. The type can be # described as a cross between a list and a dictionary. #

# Each element has a number of properties associated with it: #

    #
  • a tag. This is a string identifying what kind of data # this element represents (the element type, in other words).
  • #
  • a number of attributes, stored in a Python dictionary.
  • #
  • a text string.
  • #
  • an optional tail string.
  • #
  • a number of child elements, stored in a Python sequence
  • #
# # To create an element instance, use the {@link #Element} or {@link # #SubElement} factory functions. #

# The {@link #ElementTree} class can be used to wrap an element # structure, and convert it from and to XML. ## import string, sys, re class _SimpleElementPath: # emulate pre-1.2 find/findtext/findall behaviour def find(self, element, tag): for elem in element: if elem.tag == tag: return elem return None def findtext(self, element, tag, default=None): for elem in element: if elem.tag == tag: return elem.text or "" return default def findall(self, element, tag): if tag[:3] == ".//": return element.getiterator(tag[3:]) result = [] for elem in element: if elem.tag == tag: result.append(elem) return result try: import ElementPath except ImportError: # FIXME: issue warning in this case? ElementPath = _SimpleElementPath() # TODO: add support for custom namespace resolvers/default namespaces # TODO: add improved support for incremental parsing VERSION = "1.2" ## # Internal element class. This class defines the Element interface, # and provides a reference implementation of this interface. #

# You should not create instances of this class directly. Use the # appropriate factory functions instead, such as {@link #Element} # and {@link #SubElement}. # # @see Element # @see SubElement # @see Comment # @see ProcessingInstruction class _ElementInterface: # text...tail ## # (Attribute) Element tag. tag = None ## # (Attribute) Element attribute dictionary. Where possible, use # {@link #_ElementInterface.get}, # {@link #_ElementInterface.set}, # {@link #_ElementInterface.keys}, and # {@link #_ElementInterface.items} to access # element attributes. attrib = None ## # (Attribute) Text before first subelement. This is either a # string or the value None, if there was no text. text = None ## # (Attribute) Text after this element's end tag, but before the # next sibling element's start tag. This is either a string or # the value None, if there was no text. tail = None # text after end tag, if any def __init__(self, tag, attrib): self.tag = tag self.attrib = attrib self._children = [] def __repr__(self): return "" % (self.tag, id(self)) ## # Creates a new element object of the same type as this element. # # @param tag Element tag. # @param attrib Element attributes, given as a dictionary. # @return A new element instance. def makeelement(self, tag, attrib): return Element(tag, attrib) ## # Returns the number of subelements. # # @return The number of subelements. def __len__(self): return len(self._children) ## # Returns the given subelement. # # @param index What subelement to return. # @return The given subelement. # @exception IndexError If the given element does not exist. def __getitem__(self, index): return self._children[index] ## # Replaces the given subelement. # # @param index What subelement to replace. # @param element The new element value. # @exception IndexError If the given element does not exist. # @exception AssertionError If element is not a valid object. def __setitem__(self, index, element): assert iselement(element) self._children[index] = element ## # Deletes the given subelement. # # @param index What subelement to delete. # @exception IndexError If the given element does not exist. def __delitem__(self, index): del self._children[index] ## # Returns a list containing subelements in the given range. # # @param start The first subelement to return. # @param stop The first subelement that shouldn't be returned. # @return A sequence object containing subelements. def __getslice__(self, start, stop): return self._children[start:stop] ## # Replaces a number of subelements with elements from a sequence. # # @param start The first subelement to replace. # @param stop The first subelement that shouldn't be replaced. # @param elements A sequence object with zero or more elements. # @exception AssertionError If a sequence member is not a valid object. def __setslice__(self, start, stop, elements): for element in elements: assert iselement(element) self._children[start:stop] = list(elements) ## # Deletes a number of subelements. # # @param start The first subelement to delete. # @param stop The first subelement to leave in there. def __delslice__(self, start, stop): del self._children[start:stop] ## # Adds a subelement to the end of this element. # # @param element The element to add. # @exception AssertionError If a sequence member is not a valid object. def append(self, element): assert iselement(element) self._children.append(element) ## # Inserts a subelement at the given position in this element. # # @param index Where to insert the new subelement. # @exception AssertionError If the element is not a valid object. def insert(self, index, element): assert iselement(element) self._children.insert(index, element) ## # Removes a matching subelement. Unlike the find methods, # this method compares elements based on identity, not on tag # value or contents. # # @param element What element to remove. # @exception ValueError If a matching element could not be found. # @exception AssertionError If the element is not a valid object. def remove(self, element): assert iselement(element) self._children.remove(element) ## # Returns all subelements. The elements are returned in document # order. # # @return A list of subelements. # @defreturn list of Element instances def getchildren(self): return self._children ## # Finds the first matching subelement, by tag name or path. # # @param path What element to look for. # @return The first matching element, or None if no element was found. # @defreturn Element or None def find(self, path): return ElementPath.find(self, path) ## # Finds text for the first matching subelement, by tag name or path. # # @param path What element to look for. # @param default What to return if the element was not found. # @return The text content of the first matching element, or the # default value no element was found. Note that if the element # has is found, but has no text content, this method returns an # empty string. # @defreturn string def findtext(self, path, default=None): return ElementPath.findtext(self, path, default) ## # Finds all matching subelements, by tag name or path. # # @param path What element to look for. # @return A list or iterator containing all matching elements, # in document order. # @defreturn list of Element instances def findall(self, path): return ElementPath.findall(self, path) ## # Resets an element. This function removes all subelements, clears # all attributes, and sets the text and tail attributes to None. def clear(self): self.attrib.clear() self._children = [] self.text = self.tail = None ## # Gets an element attribute. # # @param key What attribute to look for. # @param default What to return if the attribute was not found. # @return The attribute value, or the default value, if the # attribute was not found. # @defreturn string or None def get(self, key, default=None): return self.attrib.get(key, default) ## # Sets an element attribute. # # @param key What attribute to set. # @param value The attribute value. def set(self, key, value): self.attrib[key] = value ## # Gets a list of attribute names. The names are returned in an # arbitrary order (just like for an ordinary Python dictionary). # # @return A list of element attribute names. # @defreturn list of strings def keys(self): return self.attrib.keys() ## # Gets element attributes, as a sequence. The attributes are # returned in an arbitrary order. # # @return A list of (name, value) tuples for all attributes. # @defreturn list of (string, string) tuples def items(self): return self.attrib.items() ## # Creates a tree iterator. The iterator loops over this element # and all subelements, in document order, and returns all elements # with a matching tag. #

# If the tree structure is modified during iteration, the result # is undefined. # # @param tag What tags to look for (default is to return all elements). # @return A list or iterator containing all the matching elements. # @defreturn list or iterator def getiterator(self, tag=None): nodes = [] if tag == "*": tag = None if tag is None or self.tag == tag: nodes.append(self) for node in self._children: nodes.extend(node.getiterator(tag)) return nodes # compatibility _Element = _ElementInterface ## # Element factory. This function returns an object implementing the # standard Element interface. The exact class or type of that object # is implementation dependent, but it will always be compatible with # the {@link #_ElementInterface} class in this module. #

# The element name, attribute names, and attribute values can be # either 8-bit ASCII strings or Unicode strings. # # @param tag The element name. # @param attrib An optional dictionary, containing element attributes. # @param **extra Additional attributes, given as keyword arguments. # @return An element instance. # @defreturn Element def Element(tag, attrib={}, **extra): attrib = attrib.copy() attrib.update(extra) return _ElementInterface(tag, attrib) ## # Subelement factory. This function creates an element instance, and # appends it to an existing element. #

# The element name, attribute names, and attribute values can be # either 8-bit ASCII strings or Unicode strings. # # @param parent The parent element. # @param tag The subelement name. # @param attrib An optional dictionary, containing element attributes. # @param **extra Additional attributes, given as keyword arguments. # @return An element instance. # @defreturn Element def SubElement(parent, tag, attrib={}, **extra): attrib = attrib.copy() attrib.update(extra) element = parent.makeelement(tag, attrib) parent.append(element) return element ## # Comment element factory. This factory function creates a special # element that will be serialized as an XML comment. #

# The comment string can be either an 8-bit ASCII string or a Unicode # string. # # @param text A string containing the comment string. # @return An element instance, representing a comment. # @defreturn Element def Comment(text=None): element = Element(Comment) element.text = text return element ## # PI element factory. This factory function creates a special element # that will be serialized as an XML processing instruction. # # @param target A string containing the PI target. # @param text A string containing the PI contents, if any. # @return An element instance, representing a PI. # @defreturn Element def ProcessingInstruction(target, text=None): element = Element(ProcessingInstruction) element.text = target if text: element.text = element.text + " " + text return element PI = ProcessingInstruction ## # QName wrapper. This can be used to wrap a QName attribute value, in # order to get proper namespace handling on output. # # @param text A string containing the QName value, in the form {uri}local, # or, if the tag argument is given, the URI part of a QName. # @param tag Optional tag. If given, the first argument is interpreted as # an URI, and this argument is interpreted as a local name. # @return An opaque object, representing the QName. class QName: def __init__(self, text_or_uri, tag=None): if tag: text_or_uri = "{%s}%s" % (text_or_uri, tag) self.text = text_or_uri def __str__(self): return self.text def __hash__(self): return hash(self.text) def __cmp__(self, other): if isinstance(other, QName): return cmp(self.text, other.text) return cmp(self.text, other) ## # ElementTree wrapper class. This class represents an entire element # hierarchy, and adds some extra support for serialization to and from # standard XML. # # @param element Optional root element. # @keyparam file Optional file handle or name. If given, the # tree is initialized with the contents of this XML file. class ElementTree: def __init__(self, element=None, file=None): assert element is None or iselement(element) self._root = element # first node if file: self.parse(file) ## # Gets the root element for this tree. # # @return An element instance. # @defreturn Element def getroot(self): return self._root ## # Replaces the root element for this tree. This discards the # current contents of the tree, and replaces it with the given # element. Use with care. # # @param element An element instance. def _setroot(self, element): assert iselement(element) self._root = element ## # Loads an external XML document into this element tree. # # @param source A file name or file object. # @param parser An optional parser instance. If not given, the # standard {@link XMLTreeBuilder} parser is used. # @return The document root element. # @defreturn Element def parse(self, source, parser=None): if not hasattr(source, "read"): source = open(source, "rb") if not parser: parser = XMLTreeBuilder() while 1: data = source.read(32768) if not data: break parser.feed(data) self._root = parser.close() return self._root ## # Creates a tree iterator for the root element. The iterator loops # over all elements in this tree, in document order. # # @param tag What tags to look for (default is to return all elements) # @return An iterator. # @defreturn iterator def getiterator(self, tag=None): assert self._root is not None return self._root.getiterator(tag) ## # Finds the first toplevel element with given tag. # Same as getroot().find(path). # # @param path What element to look for. # @return The first matching element, or None if no element was found. # @defreturn Element or None def find(self, path): assert self._root is not None if path[:1] == "/": path = "." + path return self._root.find(path) ## # Finds the element text for the first toplevel element with given # tag. Same as getroot().findtext(path). # # @param path What toplevel element to look for. # @param default What to return if the element was not found. # @return The text content of the first matching element, or the # default value no element was found. Note that if the element # has is found, but has no text content, this method returns an # empty string. # @defreturn string def findtext(self, path, default=None): assert self._root is not None if path[:1] == "/": path = "." + path return self._root.findtext(path, default) ## # Finds all toplevel elements with the given tag. # Same as getroot().findall(path). # # @param path What element to look for. # @return A list or iterator containing all matching elements, # in document order. # @defreturn list of Element instances def findall(self, path): assert self._root is not None if path[:1] == "/": path = "." + path return self._root.findall(path) ## # Writes the element tree to a file, as XML. # # @param file A file name, or a file object opened for writing. # @param encoding Optional output encoding (default is US-ASCII). def write(self, file, encoding="us-ascii"): assert self._root is not None if not hasattr(file, "write"): file = open(file, "wb") if not encoding: encoding = "us-ascii" elif encoding != "utf-8" and encoding != "us-ascii": file.write("\n" % encoding) self._write(file, self._root, encoding, {}) def _write(self, file, node, encoding, namespaces): # write XML to file tag = node.tag if tag is Comment: file.write("" % _escape_cdata(node.text, encoding)) elif tag is ProcessingInstruction: file.write("" % _escape_cdata(node.text, encoding)) else: items = node.items() xmlns_items = [] # new namespaces in this scope try: if isinstance(tag, QName) or tag[:1] == "{": tag, xmlns = fixtag(tag, namespaces) if xmlns: xmlns_items.append(xmlns) except TypeError: _raise_serialization_error(tag) file.write("<" + _encode(tag, encoding)) if items or xmlns_items: items.sort() # lexical order for k, v in items: try: if isinstance(k, QName) or k[:1] == "{": k, xmlns = fixtag(k, namespaces) if xmlns: xmlns_items.append(xmlns) except TypeError: _raise_serialization_error(k) try: if isinstance(v, QName): v, xmlns = fixtag(v, namespaces) if xmlns: xmlns_items.append(xmlns) except TypeError: _raise_serialization_error(v) file.write(" %s=\"%s\"" % (_encode(k, encoding), _escape_attrib(v, encoding))) for k, v in xmlns_items: file.write(" %s=\"%s\"" % (_encode(k, encoding), _escape_attrib(v, encoding))) if node.text or node: file.write(">") if node.text: file.write(_escape_cdata(node.text, encoding)) for n in node: self._write(file, n, encoding, namespaces) file.write("") else: file.write(" />") for k, v in xmlns_items: del namespaces[v] if node.tail: file.write(_escape_cdata(node.tail, encoding)) # -------------------------------------------------------------------- # helpers ## # Checks if an object appears to be a valid element object. # # @param An element instance. # @return A true value if this is an element object. # @defreturn flag def iselement(element): # FIXME: not sure about this; might be a better idea to look # for tag/attrib/text attributes return isinstance(element, _ElementInterface) or hasattr(element, "tag") ## # Writes an element tree or element structure to sys.stdout. This # function should be used for debugging only. #

# The exact output format is implementation dependent. In this # version, it's written as an ordinary XML file. # # @param elem An element tree or an individual element. def dump(elem): # debugging if not isinstance(elem, ElementTree): elem = ElementTree(elem) elem.write(sys.stdout) tail = elem.getroot().tail if not tail or tail[-1] != "\n": sys.stdout.write("\n") def _encode(s, encoding): try: return s.encode(encoding) except AttributeError: return s # 1.5.2: assume the string uses the right encoding if sys.version[:3] == "1.5": _escape = re.compile(r"[&<>\"\x80-\xff]+") # 1.5.2 else: _escape = re.compile(eval(r'u"[&<>\"\u0080-\uffff]+"')) _escape_map = { "&": "&", "<": "<", ">": ">", '"': """, } _namespace_map = { # "well-known" namespace prefixes "http://www.w3.org/XML/1998/namespace": "xml", "http://www.w3.org/1999/xhtml": "html", "http://www.w3.org/1999/02/22-rdf-syntax-ns#": "rdf", "http://schemas.xmlsoap.org/wsdl/": "wsdl", } def _raise_serialization_error(text): raise TypeError( "cannot serialize %r (type %s)" % (text, type(text).__name__) ) def _encode_entity(text, pattern=_escape): # map reserved and non-ascii characters to numerical entities def escape_entities(m, map=_escape_map): out = [] append = out.append for char in m.group(): text = map.get(char) if text is None: text = "&#%d;" % ord(char) append(text) return string.join(out, "") try: return _encode(pattern.sub(escape_entities, text), "ascii") except TypeError: _raise_serialization_error(text) # # the following functions assume an ascii-compatible encoding # (or "utf-16") def _escape_cdata(text, encoding=None, replace=string.replace): # escape character data try: if encoding: try: text = _encode(text, encoding) except UnicodeError: return _encode_entity(text) text = replace(text, "&", "&") text = replace(text, "<", "<") text = replace(text, ">", ">") return text except (TypeError, AttributeError): _raise_serialization_error(text) def _escape_attrib(text, encoding=None, replace=string.replace): # escape attribute value try: if encoding: try: text = _encode(text, encoding) except UnicodeError: return _encode_entity(text) text = replace(text, "&", "&") text = replace(text, "'", "'") # FIXME: overkill text = replace(text, "\"", """) text = replace(text, "<", "<") text = replace(text, ">", ">") return text except (TypeError, AttributeError): _raise_serialization_error(text) def fixtag(tag, namespaces): # given a decorated tag (of the form {uri}tag), return prefixed # tag and namespace declaration, if any if isinstance(tag, QName): tag = tag.text namespace_uri, tag = string.split(tag[1:], "}", 1) prefix = namespaces.get(namespace_uri) if prefix is None: prefix = _namespace_map.get(namespace_uri) if prefix is None: prefix = "ns%d" % len(namespaces) namespaces[namespace_uri] = prefix if prefix == "xml": xmlns = None else: xmlns = ("xmlns:%s" % prefix, namespace_uri) else: xmlns = None return "%s:%s" % (prefix, tag), xmlns ## # Parses an XML document into an element tree. # # @param source A filename or file object containing XML data. # @param parser An optional parser instance. If not given, the # standard {@link XMLTreeBuilder} parser is used. # @return An ElementTree instance def parse(source, parser=None): tree = ElementTree() tree.parse(source, parser) return tree ## # Parses an XML document from a string constant. This function can # be used to embed "XML literals" in Python code. # # @param source A string containing XML data. # @return An Element instance. # @defreturn Element def XML(text): parser = XMLTreeBuilder() parser.feed(text) return parser.close() ## # Parses an XML document from a string constant, and also returns # a dictionary which maps from element id:s to elements. # # @param source A string containing XML data. # @return A tuple containing an Element instance and a dictionary. # @defreturn (Element, dictionary) def XMLID(text): parser = XMLTreeBuilder() parser.feed(text) tree = parser.close() ids = {} for elem in tree.getiterator(): id = elem.get("id") if id: ids[id] = elem return tree, ids ## # Parses an XML document from a string constant. Same as {@link #XML}. # # @def fromstring(text) # @param source A string containing XML data. # @return An Element instance. # @defreturn Element fromstring = XML ## # Generates a string representation of an XML element, including all # subelements. # # @param element An Element instance. # @return An encoded string containing the XML data. # @defreturn string def tostring(element, encoding=None): class dummy: pass data = [] file = dummy() file.write = data.append ElementTree(element).write(file, encoding) return string.join(data, "") ## # Generic element structure builder. This builder converts a sequence # of {@link #TreeBuilder.start}, {@link #TreeBuilder.data}, and {@link # #TreeBuilder.end} method calls to a well-formed element structure. #

# You can use this class to build an element structure using a custom XML # parser, or a parser for some other XML-like format. # # @param element_factory Optional element factory. This factory # is called to create new Element instances, as necessary. class TreeBuilder: def __init__(self, element_factory=None): self._data = [] # data collector self._elem = [] # element stack self._last = None # last element self._tail = None # true if we're after an end tag if element_factory is None: element_factory = _ElementInterface self._factory = element_factory ## # Flushes the parser buffers, and returns the toplevel documen # element. # # @return An Element instance. # @defreturn Element def close(self): assert len(self._elem) == 0, "missing end tags" assert self._last != None, "missing toplevel element" return self._last def _flush(self): if self._data: if self._last is not None: text = string.join(self._data, "") if self._tail: assert self._last.tail is None, "internal error (tail)" self._last.tail = text else: assert self._last.text is None, "internal error (text)" self._last.text = text self._data = [] ## # Adds text to the current element. # # @param data A string. This should be either an 8-bit string # containing ASCII text, or a Unicode string. def data(self, data): self._data.append(data) ## # Opens a new element. # # @param tag The element name. # @param attrib A dictionary containing element attributes. # @return The opened element. # @defreturn Element def start(self, tag, attrs): self._flush() self._last = elem = self._factory(tag, attrs) if self._elem: self._elem[-1].append(elem) self._elem.append(elem) self._tail = 0 return elem ## # Closes the current element. # # @param tag The element name. # @return The closed element. # @defreturn Element def end(self, tag): self._flush() self._last = self._elem.pop() assert self._last.tag == tag,\ "end tag mismatch (expected %s, got %s)" % ( self._last.tag, tag) self._tail = 1 return self._last ## # Element structure builder for XML source data, based on the # expat parser. # # @keyparam target Target object. If omitted, the builder uses an # instance of the standard {@link #TreeBuilder} class. # @keyparam html Predefine HTML entities. This flag is not supported # by the current implementation. # @see #ElementTree # @see #TreeBuilder class XMLTreeBuilder: def __init__(self, html=0, target=None): from xml.parsers import expat self._parser = parser = expat.ParserCreate(None, "}") if target is None: target = TreeBuilder() self._target = target self._names = {} # name memo cache parser.DefaultHandler = self._default parser.StartElementHandler = self._start parser.EndElementHandler = self._end parser.CharacterDataHandler = self._data encoding = None if not parser.returns_unicode: encoding = "utf-8" # target.xml(encoding, None) self._doctype = None self.entity = {} def _fixtext(self, text): # convert text string to ascii, if possible try: return str(text) # what if the default encoding is changed? except UnicodeError: return text def _fixname(self, key): # expand qname, and convert name string to ascii, if possible try: name = self._names[key] except KeyError: name = key if "}" in name: name = "{" + name self._names[key] = name = self._fixtext(name) return name def _start(self, tag, attrib_in): fixname = self._fixname tag = fixname(tag) attrib = {} for key, value in attrib_in.items(): attrib[fixname(key)] = self._fixtext(value) return self._target.start(tag, attrib) def _data(self, text): return self._target.data(self._fixtext(text)) def _end(self, tag): return self._target.end(self._fixname(tag)) def _default(self, text): prefix = text[:1] if prefix == "&": # deal with undefined entities try: self._target.data(self.entity[text[1:-1]]) except KeyError: from xml.parsers import expat raise expat.error( "undefined entity %s: line %d, column %d" % (text, self._parser.ErrorLineNumber, self._parser.ErrorColumnNumber) ) elif prefix == "<" and text[:9] == "": self._doctype = None return text = string.strip(text) if not text: return self._doctype.append(text) n = len(self._doctype) if n > 2: type = self._doctype[1] if type == "PUBLIC" and n == 4: name, type, pubid, system = self._doctype elif type == "SYSTEM" and n == 3: name, type, system = self._doctype pubid = None else: return if pubid: pubid = pubid[1:-1] self.doctype(name, pubid, system[1:-1]) self._doctype = None ## # Handles a doctype declaration. # # @param name Doctype name. # @param pubid Public identifier. # @param system System identifier. def doctype(self, name, pubid, system): pass ## # Feeds data to the parser. # # @param data Encoded data. def feed(self, data): self._parser.Parse(data, 0) ## # Finishes feeding data to the parser. # # @return An element structure. # @defreturn Element def close(self): self._parser.Parse("", 1) # end of data tree = self._target.close() del self._target, self._parser # get rid of circular references return tree M 644 inline elementtree/__init__.py data 1491 # $Id: __init__.py 1821 2004-06-03 16:57:49Z fredrik $ # elementtree package # -------------------------------------------------------------------- # The ElementTree toolkit is # # Copyright (c) 1999-2004 by Fredrik Lundh # # By obtaining, using, and/or copying this software and/or its # associated documentation, you agree that you have read, understood, # and will comply with the following terms and conditions: # # Permission to use, copy, modify, and distribute this software and # its associated documentation for any purpose and without fee is # hereby granted, provided that the above copyright notice appears in # all copies, and that both that copyright notice and this permission # notice appear in supporting documentation, and that the name of # Secret Labs AB or the author not be used in advertising or publicity # pertaining to distribution of the software without specific, written # prior permission. # # SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD # TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- # ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR # BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS # ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE # OF THIS SOFTWARE. # -------------------------------------------------------------------- M 644 inline NEWS data 298 bzr-0.0.1 NOT RELEASED YET PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. commit refs/heads/master mark :73 committer 1111559850 +1100 data 38 fix time.localtime call for python 2.3 from :72 M 644 inline NEWS data 385 bzr-0.0.1 NOT RELEASED YET PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/osutils.py data 7100 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, types, re, time, types from stat import S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE from errors import bailout def make_readonly(filename): """Make a filename read-only.""" # TODO: probably needs to be fixed for windows mod = os.stat(filename).st_mode mod = mod & 0777555 os.chmod(filename, mod) def make_writable(filename): mod = os.stat(filename).st_mode mod = mod | 0200 os.chmod(filename, mod) _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])') def quotefn(f): """Return shell-quoted filename""" ## We could be a bit more terse by using double-quotes etc f = _QUOTE_RE.sub(r'\\\1', f) if f[0] == '~': f[0:1] = r'\~' return f def file_kind(f): mode = os.lstat(f)[ST_MODE] if S_ISREG(mode): return 'file' elif S_ISDIR(mode): return 'directory' elif S_ISLNK(mode): return 'symlink' else: bailout("can't handle file kind with mode %o of %r" % (mode, f)) def isdir(f): """True if f is an accessible directory.""" try: return S_ISDIR(os.lstat(f)[ST_MODE]) except OSError: return False def isfile(f): """True if f is a regular file.""" try: return S_ISREG(os.lstat(f)[ST_MODE]) except OSError: return False def pumpfile(fromfile, tofile): """Copy contents of one file to another.""" tofile.write(fromfile.read()) def uuid(): """Return a new UUID""" ## XXX: Could alternatively read /proc/sys/kernel/random/uuid on ## Linux, but we need something portable for other systems; ## preferably an implementation in Python. try: return chomp(file('/proc/sys/kernel/random/uuid').readline()) except IOError: return chomp(os.popen('uuidgen').readline()) def chomp(s): if s and (s[-1] == '\n'): return s[:-1] else: return s def sha_file(f): import sha ## TODO: Maybe read in chunks to handle big files if hasattr(f, 'tell'): assert f.tell() == 0 s = sha.new() s.update(f.read()) return s.hexdigest() def sha_string(f): import sha s = sha.new() s.update(f) return s.hexdigest() def username(): """Return email-style username. Something similar to 'Martin Pool ' :todo: Check it's reasonably well-formed. :todo: Allow taking it from a dotfile to help people on windows who can't easily set variables. :todo: Cope without pwd module, which is only on unix. """ e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: return e import socket try: import pwd uid = os.getuid() w = pwd.getpwuid(uid) gecos = w.pw_gecos comma = gecos.find(',') if comma == -1: realname = gecos else: realname = gecos[:comma] return '%s <%s@%s>' % (realname, w.pw_name, socket.getfqdn()) except ImportError: pass import getpass, socket return '<%s@%s>' % (getpass.getuser(), socket.getfqdn()) def user_email(): """Return just the email component of a username.""" e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: import re m = re.search(r'[\w+.-]+@[\w+.-]+', e) if not m: bailout('%r is not a reasonable email address' % e) return m.group(0) import getpass, socket return '%s@%s' % (getpass.getuser(), socket.getfqdn()) def compare_files(a, b): """Returns true if equal in contents""" # TODO: don't read the whole thing in one go. result = a.read() == b.read() return result def local_time_offset(t=None): """Return offset of local zone from GMT, either at present or at time t.""" # python2.3 localtime() can't take None if t is None: t = time.time() if time.localtime(t).tm_isdst and time.daylight: return -time.altzone else: return -time.timezone def format_date(t, offset=0, timezone='original'): ## TODO: Perhaps a global option to use either universal or local time? ## Or perhaps just let people set $TZ? import time assert isinstance(t, float) if timezone == 'utc': tt = time.gmtime(t) offset = 0 elif timezone == 'original': if offset == None: offset = 0 tt = time.gmtime(t + offset) elif timezone == 'local': tt = time.localtime(t) offset = local_time_offset(t) else: bailout("unsupported timezone format %r", ['options are "utc", "original", "local"']) return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt) + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)) def compact_date(when): return time.strftime('%Y%m%d%H%M%S', time.gmtime(when)) def filesize(f): """Return size of given open file.""" return os.fstat(f.fileno())[ST_SIZE] if hasattr(os, 'urandom'): # python 2.4 and later rand_bytes = os.urandom else: # FIXME: No good on non-Linux _rand_file = file('/dev/urandom', 'rb') rand_bytes = _rand_file.read ## TODO: We could later have path objects that remember their list ## decomposition (might be too tricksy though.) def splitpath(p): """Turn string into list of parts. >>> splitpath('a') ['a'] >>> splitpath('a/b') ['a', 'b'] >>> splitpath('a/./b') ['a', 'b'] >>> splitpath('a/.b') ['a', '.b'] >>> splitpath('a/../b') Traceback (most recent call last): ... BzrError: ("sorry, '..' not allowed in path", []) """ assert isinstance(p, types.StringTypes) ps = [f for f in p.split('/') if f != '.'] for f in ps: if f == '..': bailout("sorry, %r not allowed in path" % f) return ps def joinpath(p): assert isinstance(p, list) for f in p: if (f == '..') or (f is None) or (f == ''): bailout("sorry, %r not allowed in path" % f) return '/'.join(p) def appendpath(p1, p2): if p1 == '': return p2 else: return p1 + '/' + p2 def extern_command(cmd, ignore_errors = False): mutter('external command: %s' % `cmd`) if os.system(cmd): if not ignore_errors: bailout('command failed') commit refs/heads/master mark :74 committer 1111581039 +1100 data 77 compare_files: read in one page at a time rather than loading the whole file from :73 M 644 inline bzrlib/osutils.py data 7232 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, types, re, time, types from stat import S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE from errors import bailout def make_readonly(filename): """Make a filename read-only.""" # TODO: probably needs to be fixed for windows mod = os.stat(filename).st_mode mod = mod & 0777555 os.chmod(filename, mod) def make_writable(filename): mod = os.stat(filename).st_mode mod = mod | 0200 os.chmod(filename, mod) _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])') def quotefn(f): """Return shell-quoted filename""" ## We could be a bit more terse by using double-quotes etc f = _QUOTE_RE.sub(r'\\\1', f) if f[0] == '~': f[0:1] = r'\~' return f def file_kind(f): mode = os.lstat(f)[ST_MODE] if S_ISREG(mode): return 'file' elif S_ISDIR(mode): return 'directory' elif S_ISLNK(mode): return 'symlink' else: bailout("can't handle file kind with mode %o of %r" % (mode, f)) def isdir(f): """True if f is an accessible directory.""" try: return S_ISDIR(os.lstat(f)[ST_MODE]) except OSError: return False def isfile(f): """True if f is a regular file.""" try: return S_ISREG(os.lstat(f)[ST_MODE]) except OSError: return False def pumpfile(fromfile, tofile): """Copy contents of one file to another.""" tofile.write(fromfile.read()) def uuid(): """Return a new UUID""" ## XXX: Could alternatively read /proc/sys/kernel/random/uuid on ## Linux, but we need something portable for other systems; ## preferably an implementation in Python. try: return chomp(file('/proc/sys/kernel/random/uuid').readline()) except IOError: return chomp(os.popen('uuidgen').readline()) def chomp(s): if s and (s[-1] == '\n'): return s[:-1] else: return s def sha_file(f): import sha ## TODO: Maybe read in chunks to handle big files if hasattr(f, 'tell'): assert f.tell() == 0 s = sha.new() s.update(f.read()) return s.hexdigest() def sha_string(f): import sha s = sha.new() s.update(f) return s.hexdigest() def username(): """Return email-style username. Something similar to 'Martin Pool ' :todo: Check it's reasonably well-formed. :todo: Allow taking it from a dotfile to help people on windows who can't easily set variables. :todo: Cope without pwd module, which is only on unix. """ e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: return e import socket try: import pwd uid = os.getuid() w = pwd.getpwuid(uid) gecos = w.pw_gecos comma = gecos.find(',') if comma == -1: realname = gecos else: realname = gecos[:comma] return '%s <%s@%s>' % (realname, w.pw_name, socket.getfqdn()) except ImportError: pass import getpass, socket return '<%s@%s>' % (getpass.getuser(), socket.getfqdn()) def user_email(): """Return just the email component of a username.""" e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: import re m = re.search(r'[\w+.-]+@[\w+.-]+', e) if not m: bailout('%r is not a reasonable email address' % e) return m.group(0) import getpass, socket return '%s@%s' % (getpass.getuser(), socket.getfqdn()) def compare_files(a, b): """Returns true if equal in contents""" # TODO: don't read the whole thing in one go. BUFSIZE = 4096 while True: ai = a.read(BUFSIZE) bi = b.read(BUFSIZE) if ai != bi: return False if ai == '': return True def local_time_offset(t=None): """Return offset of local zone from GMT, either at present or at time t.""" # python2.3 localtime() can't take None if t is None: t = time.time() if time.localtime(t).tm_isdst and time.daylight: return -time.altzone else: return -time.timezone def format_date(t, offset=0, timezone='original'): ## TODO: Perhaps a global option to use either universal or local time? ## Or perhaps just let people set $TZ? import time assert isinstance(t, float) if timezone == 'utc': tt = time.gmtime(t) offset = 0 elif timezone == 'original': if offset == None: offset = 0 tt = time.gmtime(t + offset) elif timezone == 'local': tt = time.localtime(t) offset = local_time_offset(t) else: bailout("unsupported timezone format %r", ['options are "utc", "original", "local"']) return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt) + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)) def compact_date(when): return time.strftime('%Y%m%d%H%M%S', time.gmtime(when)) def filesize(f): """Return size of given open file.""" return os.fstat(f.fileno())[ST_SIZE] if hasattr(os, 'urandom'): # python 2.4 and later rand_bytes = os.urandom else: # FIXME: No good on non-Linux _rand_file = file('/dev/urandom', 'rb') rand_bytes = _rand_file.read ## TODO: We could later have path objects that remember their list ## decomposition (might be too tricksy though.) def splitpath(p): """Turn string into list of parts. >>> splitpath('a') ['a'] >>> splitpath('a/b') ['a', 'b'] >>> splitpath('a/./b') ['a', 'b'] >>> splitpath('a/.b') ['a', '.b'] >>> splitpath('a/../b') Traceback (most recent call last): ... BzrError: ("sorry, '..' not allowed in path", []) """ assert isinstance(p, types.StringTypes) ps = [f for f in p.split('/') if f != '.'] for f in ps: if f == '..': bailout("sorry, %r not allowed in path" % f) return ps def joinpath(p): assert isinstance(p, list) for f in p: if (f == '..') or (f is None) or (f == ''): bailout("sorry, %r not allowed in path" % f) return '/'.join(p) def appendpath(p1, p2): if p1 == '': return p2 else: return p1 + '/' + p2 def extern_command(cmd, ignore_errors = False): mutter('external command: %s' % `cmd`) if os.system(cmd): if not ignore_errors: bailout('command failed') commit refs/heads/master mark :75 committer 1111621930 +1100 data 19 more notes on darcs from :74 M 644 inline doc/darcs.txt data 2638 Darcs compared to Arch ====================== Simpler to use; perhaps harder to completely understand. Always local; always fast. Patch commution is slow and perhaps doesn't clearly do what people want. Too slow! Can't reliably get back to any previous point. Explicitly not addressing source archive/librarian function. Loads everything into memory. Written in Haskell. A really simple pre-commit check hook is remarkably useful. http://www.scannedinavian.org/DarcsWiki/DifferencesFromArch Sometimes useful to be able to set email per-branch, for people who work on different projects under different personas. Token replace ------------- Very cute; possibly handy; not absolutely necessary in most places. Somewhat limited by the requirement that it be reversible. This is one of very few cases where it does seem necessary that we store deltas, rather than tree states. But that seems to cause other problems in terms of being able to reliably sign revisions. This can perhaps be inferred by a smart 3-way merge tool. Certainly you could have it do sub-line merges. Partial commit -------------- darcs allows you to commit only some of the changes to a single file. This is like the common feature of commiting only a subset of changed files, but taken to a higher level. It is useful more often than one might think: it is common to fix some documentation 'on the wing' and while strictly it should be in a separate commit it is not always worth the hassle to back out changes, fix the docs, then do the real change. Similarly for making a separate branch. Although the idea is very good, the current darcs implementation is limited to selecting by patch hunk, which means that neighbouring changes cannot be separated. Fixing this probably means having some kind of pluggable GUI to build the file-to-be-committed or an edited patch, possibly using something like meld, emacs, or dirdiff. Another approach some people might like is editing the diff file to chop out hunks. I don't think this needs to be on by default, as it is in darcs. It is usual to commit all the changes. For this to work safely, it is good to have a commit hook that builds/tests the tree. Of course this needs to be evaluated against the tree as it will be committed (taking account of partial commits), not the working tree. Schwern ------- * http://www.scannedinavian.org/DarcsWiki/SchwernLikesDarcs * http://www.scannedinavian.org/DarcsWiki/SchwernHatesDarcs I think we hit most of these; more evidence for the hypothesis that what people really like about darcs is the simple interface not the patch-commutation model. commit refs/heads/master mark :76 committer 1111622085 +1100 data 32 mention "info" in top-level help from :75 M 644 inline bzrlib/commands.py data 24031 #! /usr/bin/python # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ if True: bzrlib.add.smart_add(file_list, verbose) else: # old way assert file_list b = Branch(file_list[0], find_root=True) b.add([b.relpath(f) for f in file_list], verbose=verbose) def cmd_relpath(filename): print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): b = Branch('.') print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl is not None: return pl else: return 's' count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %5d %s' % (count_status[fs], name) print ' %5d versioned subdirector%s' % (count_version_dirs, plural(count_version_dirs, 'y', 'ies')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %5d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %5d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %5d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'root': ['filename?'], 'relpath': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :77 committer 1111623187 +1100 data 43 - split info command out into separate file from :76 M 644 inline bzrlib/info.py data 2765 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import time import bzrlib from osutils import format_date def show_info(b): print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl is not None: return pl else: return 's' count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %5d %s' % (count_status[fs], name) print ' %5d versioned subdirector%s' % (count_version_dirs, plural(count_version_dirs, 'y', 'ies')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %5d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %5d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %5d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) M 644 inline bzrlib/commands.py data 22154 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ if True: bzrlib.add.smart_add(file_list, verbose) else: # old way assert file_list b = Branch(file_list[0], find_root=True) b.add([b.relpath(f) for f in file_list], verbose=verbose) def cmd_relpath(filename): print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'root': ['filename?'], 'relpath': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :78 committer 1111623334 +1100 data 27 align fields in info output from :77 M 644 inline bzrlib/info.py data 2767 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import time import bzrlib from osutils import format_date def show_info(b): print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl is not None: return pl else: return 's' count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %5d %s' % (count_status[fs], name) print ' %5d versioned subdirector%s' % (count_version_dirs, plural(count_version_dirs, 'y', 'ies')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %5d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %5d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %5d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) commit refs/heads/master mark :79 committer 1111623695 +1100 data 4 todo from :78 M 644 inline bzrlib/info.py data 2873 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import time import bzrlib from osutils import format_date def show_info(b): # TODO: Maybe show space used by working tree, versioned files, # unknown files, text store. print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl is not None: return pl else: return 's' count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %5d %s' % (count_status[fs], name) print ' %5d versioned subdirector%s' % (count_version_dirs, plural(count_version_dirs, 'y', 'ies')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %5d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %5d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %5d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) commit refs/heads/master mark :80 committer 1111623998 +1100 data 54 show_info: Show number of entries in the branch stores from :79 M 644 inline bzrlib/info.py data 3143 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import time import bzrlib from osutils import format_date def show_info(b): # TODO: Maybe show space used by working tree, versioned files, # unknown files, text store. print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl is not None: return pl else: return 's' count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %5d %s' % (count_status[fs], name) print ' %5d versioned subdirector%s' % (count_version_dirs, plural(count_version_dirs, 'y', 'ies')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %5d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %5d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %5d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) print print 'text store:' print ' %5d file texts' % len(b.text_store) print print 'revision store:' print ' %5d revisions' % len(b.revision_store) print print 'inventory store:' print ' %5d inventories' % len(b.inventory_store) M 644 inline bzrlib/store.py data 4465 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Stores are the main data-storage mechanism for Bazaar-NG. A store is a simple write-once container indexed by a universally unique ID, which is typically the SHA-1 of the content.""" __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import os, tempfile, types, osutils from StringIO import StringIO from trace import mutter ###################################################################### # stores class StoreError(Exception): pass class ImmutableStore: """Store that holds files indexed by unique names. Files can be added, but not modified once they are in. Typically the hash is used as the name, or something else known to be unique, such as a UUID. >>> st = ImmutableScratchStore() >>> st.add(StringIO('hello'), 'aa') >>> 'aa' in st True >>> 'foo' in st False You are not allowed to add an id that is already present. Entries can be retrieved as files, which may then be read. >>> st.add(StringIO('goodbye'), '123123') >>> st['123123'].read() 'goodbye' :todo: Atomic add by writing to a temporary file and renaming. :todo: Perhaps automatically transform to/from XML in a method? Would just need to tell the constructor what class to use... :todo: Even within a simple disk store like this, we could gzip the files. But since many are less than one disk block, that might not help a lot. """ def __init__(self, basedir): """ImmutableStore constructor.""" self._basedir = basedir def _path(self, id): return os.path.join(self._basedir, id) def __repr__(self): return "%s(%r)" % (self.__class__.__name__, self._basedir) def add(self, f, fileid): """Add contents of a file into the store. :param f: An open file, or file-like object.""" # FIXME: Only works on smallish files # TODO: Can be optimized by copying at the same time as # computing the sum. mutter("add store entry %r" % (fileid)) if isinstance(f, types.StringTypes): content = f else: content = f.read() if fileid not in self: filename = self._path(fileid) f = file(filename, 'wb') f.write(content) f.flush() os.fsync(f.fileno()) f.close() osutils.make_readonly(filename) def __contains__(self, fileid): """""" return os.access(self._path(fileid), os.R_OK) def __iter__(self): return iter(os.listdir(self._basedir)) def __len__(self): return len(os.listdir(self._basedir)) def __getitem__(self, fileid): """Returns a file reading from a particular entry.""" return file(self._path(fileid), 'rb') def delete_all(self): for fileid in self: self.delete(fileid) def delete(self, fileid): """Remove nominated store entry. Most stores will be add-only.""" filename = self._path(fileid) ## osutils.make_writable(filename) os.remove(filename) def destroy(self): """Remove store; only allowed if it is empty.""" os.rmdir(self._basedir) mutter("%r destroyed" % self) class ImmutableScratchStore(ImmutableStore): """Self-destructing test subclass of ImmutableStore. The Store only exists for the lifetime of the Python object. Obviously you should not put anything precious in it. """ def __init__(self): ImmutableStore.__init__(self, tempfile.mkdtemp()) def __del__(self): self.delete_all() self.destroy() commit refs/heads/master mark :81 committer 1111625058 +1100 data 55 show space usage for various stores in the info command from :80 M 644 inline NEWS data 445 bzr-0.0.1 NOT RELEASED YET ENHANCEMENTS: * More information from info command. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/info.py data 3304 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import time import bzrlib from osutils import format_date def show_info(b): # TODO: Maybe show space used by working tree, versioned files, # unknown files, text store. print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl is not None: return pl else: return 's' count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %5d %s' % (count_status[fs], name) print ' %5d versioned subdirector%s' % (count_version_dirs, plural(count_version_dirs, 'y', 'ies')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %5d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %5d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %5d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) print print 'text store:' c, t = b.text_store.total_size() print ' %5d file texts' % c print ' %5d kB' % (t/1024) print print 'revision store:' c, t = b.revision_store.total_size() print ' %5d revisions' % c print ' %5d kB' % (t/1024) print print 'inventory store:' c, t = b.inventory_store.total_size() print ' %5d inventories' % c print ' %5d kB' % (t/1024) M 644 inline bzrlib/store.py data 4720 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Stores are the main data-storage mechanism for Bazaar-NG. A store is a simple write-once container indexed by a universally unique ID, which is typically the SHA-1 of the content.""" __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import os, tempfile, types, osutils from stat import ST_SIZE from StringIO import StringIO from trace import mutter ###################################################################### # stores class StoreError(Exception): pass class ImmutableStore: """Store that holds files indexed by unique names. Files can be added, but not modified once they are in. Typically the hash is used as the name, or something else known to be unique, such as a UUID. >>> st = ImmutableScratchStore() >>> st.add(StringIO('hello'), 'aa') >>> 'aa' in st True >>> 'foo' in st False You are not allowed to add an id that is already present. Entries can be retrieved as files, which may then be read. >>> st.add(StringIO('goodbye'), '123123') >>> st['123123'].read() 'goodbye' :todo: Atomic add by writing to a temporary file and renaming. :todo: Perhaps automatically transform to/from XML in a method? Would just need to tell the constructor what class to use... :todo: Even within a simple disk store like this, we could gzip the files. But since many are less than one disk block, that might not help a lot. """ def __init__(self, basedir): """ImmutableStore constructor.""" self._basedir = basedir def _path(self, id): return os.path.join(self._basedir, id) def __repr__(self): return "%s(%r)" % (self.__class__.__name__, self._basedir) def add(self, f, fileid): """Add contents of a file into the store. :param f: An open file, or file-like object.""" # FIXME: Only works on smallish files # TODO: Can be optimized by copying at the same time as # computing the sum. mutter("add store entry %r" % (fileid)) if isinstance(f, types.StringTypes): content = f else: content = f.read() if fileid not in self: filename = self._path(fileid) f = file(filename, 'wb') f.write(content) f.flush() os.fsync(f.fileno()) f.close() osutils.make_readonly(filename) def __contains__(self, fileid): """""" return os.access(self._path(fileid), os.R_OK) def __iter__(self): return iter(os.listdir(self._basedir)) def __len__(self): return len(os.listdir(self._basedir)) def __getitem__(self, fileid): """Returns a file reading from a particular entry.""" return file(self._path(fileid), 'rb') def total_size(self): """Return (count, bytes)""" total = 0 count = 0 for fid in self: count += 1 total += os.stat(self._path(fid))[ST_SIZE] return count, total def delete_all(self): for fileid in self: self.delete(fileid) def delete(self, fileid): """Remove nominated store entry. Most stores will be add-only.""" filename = self._path(fileid) ## osutils.make_writable(filename) os.remove(filename) def destroy(self): """Remove store; only allowed if it is empty.""" os.rmdir(self._basedir) mutter("%r destroyed" % self) class ImmutableScratchStore(ImmutableStore): """Self-destructing test subclass of ImmutableStore. The Store only exists for the lifetime of the Python object. Obviously you should not put anything precious in it. """ def __init__(self): ImmutableStore.__init__(self, tempfile.mkdtemp()) def __del__(self): self.delete_all() self.destroy() commit refs/heads/master mark :82 committer 1111625196 +1100 data 4 todo from :81 M 644 inline bzrlib/commands.py data 22253 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ if True: bzrlib.add.smart_add(file_list, verbose) else: # old way assert file_list b = Branch(file_list[0], find_root=True) b.add([b.relpath(f) for f in file_list], verbose=verbose) def cmd_relpath(filename): print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(): # TODO: Specific help for particular commands print __doc__ def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'init': [], 'add': ['file+'], 'commit': [], 'diff': [], 'file-id': ['filename'], 'root': ['filename?'], 'relpath': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'log': [], 'lookup-revision': ['revno'], 'export': ['revno', 'dest'], 'remove': ['file+'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :83 committer 1111626830 +1100 data 53 Can now say "bzr help COMMAND" for more detailed help from :82 M 644 inline NEWS data 507 bzr-0.0.1 NOT RELEASED YET ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 22607 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __docformat__ = "restructuredtext en" __version__ = '0.0.0' import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ if True: bzrlib.add.smart_add(file_list, verbose) else: # old way assert file_list b = Branch(file_list[0], find_root=True) b.add([b.relpath(f) for f in file_list], verbose=verbose) def cmd_relpath(filename): print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = cmdfn.__doc__ if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % __version__ print __copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'commit': [], 'diff': [], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'relpath': ['filename'], 'remove': ['file+'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :84 committer 1111713406 +1100 data 42 - update version string - put it in bzrlib from :83 M 644 inline bzrlib/__init__.py data 1267 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr library""" from inventory import Inventory, InventoryEntry from branch import Branch, ScratchBranch from osutils import format_date from tree import Tree from diff import diff_trees from trace import mutter, warning import add BZRDIR = ".bzr" DEFAULT_IGNORE = ['.*', '*~', '#*#', '*.tmp', '*.o', '*.a', '*.py[oc]', '{arch}'] IGNORE_FILENAME = ".bzrignore" __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __version__ = '0.0.1pre' M 644 inline bzrlib/commands.py data 22453 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * No handling of subdirectories, symlinks or any non-text files. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help Show summary help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ if True: bzrlib.add.smart_add(file_list, verbose) else: # old way assert file_list b = Branch(file_list[0], find_root=True) b.add([b.relpath(f) for f in file_list], verbose=verbose) def cmd_relpath(filename): print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = cmdfn.__doc__ if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'commit': [], 'diff': [], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'relpath': ['filename'], 'remove': ['file+'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :85 committer 1111713475 +1100 data 20 improved help string from :84 M 644 inline bzrlib/commands.py data 22389 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ if True: bzrlib.add.smart_add(file_list, verbose) else: # old way assert file_list b = Branch(file_list[0], find_root=True) b.add([b.relpath(f) for f in file_list], verbose=verbose) def cmd_relpath(filename): print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = cmdfn.__doc__ if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'commit': [], 'diff': [], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'relpath': ['filename'], 'remove': ['file+'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :86 committer 1111719514 +1100 data 26 first cut at recursive add from :85 M 644 inline bzrlib/add.py data 2979 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import types, os, sys, stat import bzrlib from osutils import quotefn, appendpath from errors import bailout from trace import mutter def smart_add(file_list, verbose=False, recurse=True): """Add files to version, optionall recursing into directories. This is designed more towards DWIM for humans than API simplicity. For the specific behaviour see the help for cmd_add(). """ assert file_list assert not isinstance(file_list, types.StringTypes) b = bzrlib.branch.Branch(file_list[0], find_root=True) inv = b.read_working_inventory() tree = b.working_tree() dirty = False def add_one(rf, kind): file_id = bzrlib.branch.gen_file_id(rf) inv.add_path(rf, kind=kind, file_id=file_id) bzrlib.mutter("added %r kind %r file_id={%s}" % (rf, kind, file_id)) dirty = True if verbose: bzrlib.textui.show_status('A', kind, quotefn(f)) for f in file_list: rf = b.relpath(f) af = b.abspath(rf) bzrlib.mutter("smart add of %r" % f) if bzrlib.branch.is_control_file(af): bailout("cannot add control file %r" % af) kind = bzrlib.osutils.file_kind(f) versioned = (inv.path2id(rf) != None) ## TODO: It's OK to add '.' but only in recursive mode if kind == 'file': if versioned: bzrlib.warning("%r is already versioned" % f) continue else: add_one(rf, kind) elif kind == 'directory': if versioned and not recurse: bzrlib.warning("%r is already versioned" % f) continue if not versioned: add_one(rf, kind) if recurse: for subf in os.listdir(af): subp = appendpath(rf, subf) if tree.is_ignored(subp): mutter("skip ignored sub-file %r" % subp) else: mutter("queue to add sub-file %r" % (subp)) file_list.append(subp) else: bailout("can't smart_add file kind %r" % kind) if dirty: b._write_inventory(inv) commit refs/heads/master mark :87 committer 1111720183 +1100 data 130 - clean up smart_add code, and make it commit the inventory when done. still not perfect handling of already-versioned files. from :86 M 644 inline bzrlib/add.py data 2703 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import types, os, sys, stat import bzrlib from osutils import quotefn, appendpath from errors import bailout from trace import mutter def smart_add(file_list, verbose=False, recurse=True): """Add files to version, optionall recursing into directories. This is designed more towards DWIM for humans than API simplicity. For the specific behaviour see the help for cmd_add(). """ assert file_list assert not isinstance(file_list, types.StringTypes) b = bzrlib.branch.Branch(file_list[0], find_root=True) inv = b.read_working_inventory() tree = b.working_tree() count = 0 for f in file_list: rf = b.relpath(f) af = b.abspath(rf) ## TODO: It's OK to add root but only in recursive mode bzrlib.mutter("smart add of %r" % f) if bzrlib.branch.is_control_file(af): bailout("cannot add control file %r" % af) kind = bzrlib.osutils.file_kind(f) if kind != 'file' and kind != 'directory': bailout("can't add file of kind %r" % kind) versioned = (inv.path2id(rf) != None) if versioned: bzrlib.warning("%r is already versioned" % f) else: file_id = bzrlib.branch.gen_file_id(rf) inv.add_path(rf, kind=kind, file_id=file_id) bzrlib.mutter("added %r kind %r file_id={%s}" % (rf, kind, file_id)) count += 1 if verbose: bzrlib.textui.show_status('A', kind, quotefn(f)) if kind == 'directory' and recurse: for subf in os.listdir(af): subp = appendpath(rf, subf) if tree.is_ignored(subp): mutter("skip ignored sub-file %r" % subp) else: mutter("queue to add sub-file %r" % (subp)) file_list.append(subp) if count > 0: print '* added %d' % count b._write_inventory(inv) M 644 inline bzrlib/commands.py data 22248 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = cmdfn.__doc__ if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'commit': [], 'diff': [], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'relpath': ['filename'], 'remove': ['file+'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :88 committer 1111720701 +1100 data 45 smart_add: no warning for already-added files from :87 M 644 inline bzrlib/add.py data 2688 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import types, os, sys, stat import bzrlib from osutils import quotefn, appendpath from errors import bailout from trace import mutter def smart_add(file_list, verbose=False, recurse=True): """Add files to version, optionall recursing into directories. This is designed more towards DWIM for humans than API simplicity. For the specific behaviour see the help for cmd_add(). """ assert file_list assert not isinstance(file_list, basestring) b = bzrlib.branch.Branch(file_list[0], find_root=True) inv = b.read_working_inventory() tree = b.working_tree() count = 0 for f in file_list: rf = b.relpath(f) af = b.abspath(rf) ## TODO: It's OK to add root but only in recursive mode bzrlib.mutter("smart add of %r" % f) if bzrlib.branch.is_control_file(af): bailout("cannot add control file %r" % af) kind = bzrlib.osutils.file_kind(f) if kind != 'file' and kind != 'directory': bailout("can't add file of kind %r" % kind) versioned = (inv.path2id(rf) != None) if versioned: mutter("%r is already versioned" % f) else: file_id = bzrlib.branch.gen_file_id(rf) inv.add_path(rf, kind=kind, file_id=file_id) bzrlib.mutter("added %r kind %r file_id={%s}" % (rf, kind, file_id)) count += 1 if verbose: bzrlib.textui.show_status('A', kind, quotefn(f)) if kind == 'directory' and recurse: for subf in os.listdir(af): subp = appendpath(rf, subf) if tree.is_ignored(subp): mutter("skip ignored sub-file %r" % subp) else: mutter("queue to add sub-file %r" % (subp)) file_list.append(subp) if count > 0: print '* added %d' % count b._write_inventory(inv) commit refs/heads/master mark :89 committer 1111721087 +1100 data 41 - don't flush the debug log file so often from :88 M 644 inline bzrlib/trace.py data 3422 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os, time, socket, stat import bzrlib ###################################################################### # messages and logging ## TODO: If --verbose is given then write to both stderr and ## _tracefile; perhaps replace _tracefile with a tee thing. global _tracefile, _starttime # used to have % (os.environ['USER'], time.time(), os.getpid()), 'w') # If false, notes also go to stdout; should replace this with --silent # at some point. silent = False verbose = False def warning(msg): b = 'bzr: warning: ' + msg + '\n' sys.stderr.write(b) _tracefile.write(b) #_tracefile.flush() def mutter(msg): _tracefile.write(msg) _tracefile.write('\n') # _tracefile.flush() if verbose: sys.stderr.write('- ' + msg + '\n') def note(msg): b = '* ' + str(msg) + '\n' if not silent: sys.stderr.write(b) _tracefile.write(b) # _tracefile.flush() def log_error(msg): sys.stderr.write(msg) _tracefile.write(msg) # _tracefile.flush() def create_tracefile(argv): # TODO: Also show contents of /etc/lsb-release, if it can be parsed. # Perhaps that should eventually go into the platform library? # TODO: If the file doesn't exist, add a note describing it. # Messages are always written to here, so that we have some # information if something goes wrong. In a future version this # file will be removed on successful completion. global _starttime, _tracefile _starttime = os.times()[4] _tracefile = file('.bzr.log', 'at') t = _tracefile if os.fstat(t.fileno())[stat.ST_SIZE] == 0: t.write("\nthis is a debug log for diagnosing/reporting problems in bzr\n") t.write("you can delete or truncate this file, or include sections in\n") t.write("bug reports to bazaar-ng@lists.canonical.com\n\n") # TODO: If we failed to create the file, perhaps give a warning # but don't abort; send things to /dev/null instead? t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % bzrlib.osutils.format_date(time.time())) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.getfqdn())) t.write(' arguments: %r\n' % argv) import platform t.write(' platform: %s\n' % platform.platform()) t.write(' python: %s\n' % platform.python_version()) import atexit atexit.register(_close_trace) def _close_trace(): times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum, %.3f elapsed" % (times[:4] + ((times[4] - _starttime),))) M 644 inline notes/performance.txt data 6209 For a tree holding 2.4.18 (two copies), 2.4.19, 2.4.20 With gzip -9: mbp@hope% du .bzr 195110 .bzr/text-store 20 .bzr/revision-store 12355 .bzr/inventory-store 216325 .bzr mbp@hope% du -s . 523128 . Without gzip: This is actually a pretty bad example because of deleting and re-importing 2.4.18, but still not totally unreasonable. ---- linux-2.4.0: 116399 kB after addding everything: 119505kB bzr status 2.68s user 0.13s system 84% cpu 3.330 total bzr commit 'import 2.4.0' 4.41s user 2.15s system 11% cpu 59.490 total 242446 . 122068 .bzr ---- Performance (2005-03-01) To add all files from linux-2.4.18: about 70s, mostly inventory serialization/deserialization. To commit: - finished, 6.520u/3.870s cpu, 33.940u/10.730s cum - 134.040 elapsed Interesting that it spends so long on external processing! I wonder if this is for running uuidgen? Let's try generating things internally. Great, this cuts it to 17.15s user 0.61s system 83% cpu 21.365 total to add, with no external command time. The commit now seems to spend most of its time copying to disk. - finished, 6.550u/3.320s cpu, 35.050u/9.870s cum - 89.650 elapsed I wonder where the external time is now? We were also using uuids() for revisions. Let's remove everything and re-add. Detecting everything was removed takes - finished, 2.460u/0.110s cpu, 0.000u/0.000s cum - 3.430 elapsed which may be mostly XML deserialization? Just getting the previous revision takes about this long: bzr invoked at Tue 2005-03-01 15:53:05.183741 EST +1100 by mbp@sourcefrog.net on hope arguments: ['/home/mbp/bin/bzr', 'get-revision-inventory', 'mbp@sourcefrog.net-20050301044608-8513202ab179aff4-44e8cd52a41aa705'] platform: Linux-2.6.10-4-686-i686-with-debian-3.1 - finished, 3.910u/0.390s cpu, 0.000u/0.000s cum - 6.690 elapsed Now committing the revision which removes all files should be fast. - finished, 1.280u/0.030s cpu, 0.000u/0.000s cum - 1.320 elapsed Now re-add with new code that doesn't call uuidgen: - finished, 1.990u/0.030s cpu, 0.000u/0.000s cum - 2.040 elapsed 16.61s user 0.55s system 74% cpu 22.965 total Status:: - finished, 2.500u/0.110s cpu, 0.010u/0.000s cum - 3.350 elapsed And commit:: Now patch up to 2.4.19. There were some bugs in handling missing directories, but with that fixed we do much better:: bzr status 5.86s user 1.06s system 10% cpu 1:05.55 total This is slow because it's diffing every file; we should use mtimes etc to make this faster. The cpu time is reasonable. I see difflib is pure Python; it might be faster to shell out to GNU diff when we need it. Export is very fast:: - finished, 4.220u/1.480s cpu, 0.010u/0.000s cum - 10.810 elapsed bzr export 1 ../linux-2.4.18.export1 3.92s user 1.72s system 21% cpu 26.030 total Now to find and add the new changes:: - finished, 2.190u/0.030s cpu, 0.000u/0.000s cum - 2.300 elapsed :: bzr commit 'import 2.4.19' 9.36s user 1.91s system 23% cpu 47.127 total And the result is exactly right. Try exporting:: mbp@hope% bzr export 4 ../linux-2.4.19.export4 bzr export 4 ../linux-2.4.19.export4 4.21s user 1.70s system 18% cpu 32.304 total and the export is exactly the same as the tarball. Now we can optimize the diff a bit more by not comparing files that have the right SHA-1 from within the commit For comparison:: patch -p1 < ../kernel.pkg/patch-2.4.20 1.61s user 1.03s system 13% cpu 19.106 total Now status after applying the .20 patch. With full-text verification:: bzr status 7.07s user 1.32s system 13% cpu 1:04.29 total with that turned off:: bzr status 5.86s user 0.56s system 25% cpu 25.577 total After adding: bzr status 6.14s user 0.61s system 25% cpu 26.583 total Should add some kind of profile counter for quick compares vs slow compares. bzr commit 'import 2.4.20' 7.57s user 1.36s system 20% cpu 43.568 total export: finished, 3.940u/1.820s cpu, 0.000u/0.000s cum, 50.990 elapsed also exports correctly now .21 bzr commit 'import 2.4.1' 5.59s user 0.51s system 60% cpu 10.122 total 265520 . 137704 .bzr import 2.4.2 317758 . 183463 .bzr with everything through to 2.4.29 imported, the .bzr directory is 1132MB, compared to 185MB for one tree. The .bzr.log is 100MB!. So the storage is 6.1 times larger, although we're holding 30 versions. It's pretty large but I think not ridiculous. By contrast the tarball for 2.4.0 is 104MB, and the tarball plus uncompressed patches are 315MB. Uncompressed, the text store is 1041MB. So it is only three times worse than patches, and could be compressed at presumably roughly equal efficiency. It is large, but also a very simple design and perhaps adequate for the moment. The text store with each file individually gziped is 264MB, which is also a very simple format and makes it less than twice the size of the source tree. This is actually rather pessimistic because I think there are some orphaned texts in there. Measured by du, the compressed full-text store is 363MB; also probably tolerable. The real fix is perhaps to use some kind of weave, not so much for storage efficiency as for fast annotation and therefore possible annotation-based merge. ----- 2005-03-25 Now we have recursive add, add is much faster. Adding all of the linux 2.4.19 kernel tree takes only finished, 5.460u/0.610s cpu, 0.010u/0.000s cum, 6.710 elapsed However, the store code currently flushes to disk after every write, which is probably excessive. So a commit takes finished, 8.740u/3.950s cpu, 0.010u/0.000s cum, 156.420 elapsed Status is now also quite fast, depsite that it still has to read all the working copies: mbp@hope% bzr status ~/work/linux-2.4.19 bzr status 5.51s user 0.79s system 99% cpu 6.337 total strace shows much of this is in write(2), probably because of logging. With more buffering on that file, removing all the explicit flushes, that is reduced to mbp@hope% time bzr status bzr status 5.23s user 0.42s system 97% cpu 5.780 total which is mostly opening, stating and reading files, as it should be. Still a few too many stat calls. commit refs/heads/master mark :90 committer 1111721313 +1100 data 43 - don't fsync files when written into store from :89 M 644 inline bzrlib/store.py data 4726 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Stores are the main data-storage mechanism for Bazaar-NG. A store is a simple write-once container indexed by a universally unique ID, which is typically the SHA-1 of the content.""" __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import os, tempfile, types, osutils from stat import ST_SIZE from StringIO import StringIO from trace import mutter ###################################################################### # stores class StoreError(Exception): pass class ImmutableStore: """Store that holds files indexed by unique names. Files can be added, but not modified once they are in. Typically the hash is used as the name, or something else known to be unique, such as a UUID. >>> st = ImmutableScratchStore() >>> st.add(StringIO('hello'), 'aa') >>> 'aa' in st True >>> 'foo' in st False You are not allowed to add an id that is already present. Entries can be retrieved as files, which may then be read. >>> st.add(StringIO('goodbye'), '123123') >>> st['123123'].read() 'goodbye' :todo: Atomic add by writing to a temporary file and renaming. :todo: Perhaps automatically transform to/from XML in a method? Would just need to tell the constructor what class to use... :todo: Even within a simple disk store like this, we could gzip the files. But since many are less than one disk block, that might not help a lot. """ def __init__(self, basedir): """ImmutableStore constructor.""" self._basedir = basedir def _path(self, id): return os.path.join(self._basedir, id) def __repr__(self): return "%s(%r)" % (self.__class__.__name__, self._basedir) def add(self, f, fileid): """Add contents of a file into the store. :param f: An open file, or file-like object.""" # FIXME: Only works on smallish files # TODO: Can be optimized by copying at the same time as # computing the sum. mutter("add store entry %r" % (fileid)) if isinstance(f, types.StringTypes): content = f else: content = f.read() if fileid not in self: filename = self._path(fileid) f = file(filename, 'wb') f.write(content) ## f.flush() ## os.fsync(f.fileno()) f.close() osutils.make_readonly(filename) def __contains__(self, fileid): """""" return os.access(self._path(fileid), os.R_OK) def __iter__(self): return iter(os.listdir(self._basedir)) def __len__(self): return len(os.listdir(self._basedir)) def __getitem__(self, fileid): """Returns a file reading from a particular entry.""" return file(self._path(fileid), 'rb') def total_size(self): """Return (count, bytes)""" total = 0 count = 0 for fid in self: count += 1 total += os.stat(self._path(fid))[ST_SIZE] return count, total def delete_all(self): for fileid in self: self.delete(fileid) def delete(self, fileid): """Remove nominated store entry. Most stores will be add-only.""" filename = self._path(fileid) ## osutils.make_writable(filename) os.remove(filename) def destroy(self): """Remove store; only allowed if it is empty.""" os.rmdir(self._basedir) mutter("%r destroyed" % self) class ImmutableScratchStore(ImmutableStore): """Self-destructing test subclass of ImmutableStore. The Store only exists for the lifetime of the Python object. Obviously you should not put anything precious in it. """ def __init__(self): ImmutableStore.__init__(self, tempfile.mkdtemp()) def __del__(self): self.delete_all() self.destroy() commit refs/heads/master mark :91 committer 1111721470 +1100 data 52 special handling of root directory for recursive add from :90 M 644 inline bzrlib/add.py data 2770 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import types, os, sys, stat import bzrlib from osutils import quotefn, appendpath from errors import bailout from trace import mutter def smart_add(file_list, verbose=False, recurse=True): """Add files to version, optionall recursing into directories. This is designed more towards DWIM for humans than API simplicity. For the specific behaviour see the help for cmd_add(). """ assert file_list assert not isinstance(file_list, basestring) b = bzrlib.branch.Branch(file_list[0], find_root=True) inv = b.read_working_inventory() tree = b.working_tree() count = 0 for f in file_list: rf = b.relpath(f) af = b.abspath(rf) ## TODO: It's OK to add root but only in recursive mode bzrlib.mutter("smart add of %r" % f) if bzrlib.branch.is_control_file(af): bailout("cannot add control file %r" % af) kind = bzrlib.osutils.file_kind(f) if kind != 'file' and kind != 'directory': bailout("can't add file of kind %r" % kind) versioned = (inv.path2id(rf) != None) if rf == '': mutter("branch root doesn't need to be added") elif versioned: mutter("%r is already versioned" % f) else: file_id = bzrlib.branch.gen_file_id(rf) inv.add_path(rf, kind=kind, file_id=file_id) bzrlib.mutter("added %r kind %r file_id={%s}" % (rf, kind, file_id)) count += 1 if verbose: bzrlib.textui.show_status('A', kind, quotefn(f)) if kind == 'directory' and recurse: for subf in os.listdir(af): subp = appendpath(rf, subf) if tree.is_ignored(subp): mutter("skip ignored sub-file %r" % subp) else: mutter("queue to add sub-file %r" % (subp)) file_list.append(subp) if count > 0: print '* added %d' % count b._write_inventory(inv) commit refs/heads/master mark :92 committer 1111722431 +1100 data 28 more performance measurement from :91 M 644 inline NEWS data 606 bzr-0.0.1 NOT RELEASED YET ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline notes/performance.txt data 7331 For a tree holding 2.4.18 (two copies), 2.4.19, 2.4.20 With gzip -9: mbp@hope% du .bzr 195110 .bzr/text-store 20 .bzr/revision-store 12355 .bzr/inventory-store 216325 .bzr mbp@hope% du -s . 523128 . Without gzip: This is actually a pretty bad example because of deleting and re-importing 2.4.18, but still not totally unreasonable. ---- linux-2.4.0: 116399 kB after addding everything: 119505kB bzr status 2.68s user 0.13s system 84% cpu 3.330 total bzr commit 'import 2.4.0' 4.41s user 2.15s system 11% cpu 59.490 total 242446 . 122068 .bzr ---- Performance (2005-03-01) To add all files from linux-2.4.18: about 70s, mostly inventory serialization/deserialization. To commit: - finished, 6.520u/3.870s cpu, 33.940u/10.730s cum - 134.040 elapsed Interesting that it spends so long on external processing! I wonder if this is for running uuidgen? Let's try generating things internally. Great, this cuts it to 17.15s user 0.61s system 83% cpu 21.365 total to add, with no external command time. The commit now seems to spend most of its time copying to disk. - finished, 6.550u/3.320s cpu, 35.050u/9.870s cum - 89.650 elapsed I wonder where the external time is now? We were also using uuids() for revisions. Let's remove everything and re-add. Detecting everything was removed takes - finished, 2.460u/0.110s cpu, 0.000u/0.000s cum - 3.430 elapsed which may be mostly XML deserialization? Just getting the previous revision takes about this long: bzr invoked at Tue 2005-03-01 15:53:05.183741 EST +1100 by mbp@sourcefrog.net on hope arguments: ['/home/mbp/bin/bzr', 'get-revision-inventory', 'mbp@sourcefrog.net-20050301044608-8513202ab179aff4-44e8cd52a41aa705'] platform: Linux-2.6.10-4-686-i686-with-debian-3.1 - finished, 3.910u/0.390s cpu, 0.000u/0.000s cum - 6.690 elapsed Now committing the revision which removes all files should be fast. - finished, 1.280u/0.030s cpu, 0.000u/0.000s cum - 1.320 elapsed Now re-add with new code that doesn't call uuidgen: - finished, 1.990u/0.030s cpu, 0.000u/0.000s cum - 2.040 elapsed 16.61s user 0.55s system 74% cpu 22.965 total Status:: - finished, 2.500u/0.110s cpu, 0.010u/0.000s cum - 3.350 elapsed And commit:: Now patch up to 2.4.19. There were some bugs in handling missing directories, but with that fixed we do much better:: bzr status 5.86s user 1.06s system 10% cpu 1:05.55 total This is slow because it's diffing every file; we should use mtimes etc to make this faster. The cpu time is reasonable. I see difflib is pure Python; it might be faster to shell out to GNU diff when we need it. Export is very fast:: - finished, 4.220u/1.480s cpu, 0.010u/0.000s cum - 10.810 elapsed bzr export 1 ../linux-2.4.18.export1 3.92s user 1.72s system 21% cpu 26.030 total Now to find and add the new changes:: - finished, 2.190u/0.030s cpu, 0.000u/0.000s cum - 2.300 elapsed :: bzr commit 'import 2.4.19' 9.36s user 1.91s system 23% cpu 47.127 total And the result is exactly right. Try exporting:: mbp@hope% bzr export 4 ../linux-2.4.19.export4 bzr export 4 ../linux-2.4.19.export4 4.21s user 1.70s system 18% cpu 32.304 total and the export is exactly the same as the tarball. Now we can optimize the diff a bit more by not comparing files that have the right SHA-1 from within the commit For comparison:: patch -p1 < ../kernel.pkg/patch-2.4.20 1.61s user 1.03s system 13% cpu 19.106 total Now status after applying the .20 patch. With full-text verification:: bzr status 7.07s user 1.32s system 13% cpu 1:04.29 total with that turned off:: bzr status 5.86s user 0.56s system 25% cpu 25.577 total After adding: bzr status 6.14s user 0.61s system 25% cpu 26.583 total Should add some kind of profile counter for quick compares vs slow compares. bzr commit 'import 2.4.20' 7.57s user 1.36s system 20% cpu 43.568 total export: finished, 3.940u/1.820s cpu, 0.000u/0.000s cum, 50.990 elapsed also exports correctly now .21 bzr commit 'import 2.4.1' 5.59s user 0.51s system 60% cpu 10.122 total 265520 . 137704 .bzr import 2.4.2 317758 . 183463 .bzr with everything through to 2.4.29 imported, the .bzr directory is 1132MB, compared to 185MB for one tree. The .bzr.log is 100MB!. So the storage is 6.1 times larger, although we're holding 30 versions. It's pretty large but I think not ridiculous. By contrast the tarball for 2.4.0 is 104MB, and the tarball plus uncompressed patches are 315MB. Uncompressed, the text store is 1041MB. So it is only three times worse than patches, and could be compressed at presumably roughly equal efficiency. It is large, but also a very simple design and perhaps adequate for the moment. The text store with each file individually gziped is 264MB, which is also a very simple format and makes it less than twice the size of the source tree. This is actually rather pessimistic because I think there are some orphaned texts in there. Measured by du, the compressed full-text store is 363MB; also probably tolerable. The real fix is perhaps to use some kind of weave, not so much for storage efficiency as for fast annotation and therefore possible annotation-based merge. ----- 2005-03-25 Now we have recursive add, add is much faster. Adding all of the linux 2.4.19 kernel tree takes only finished, 5.460u/0.610s cpu, 0.010u/0.000s cum, 6.710 elapsed However, the store code currently flushes to disk after every write, which is probably excessive. So a commit takes finished, 8.740u/3.950s cpu, 0.010u/0.000s cum, 156.420 elapsed Status is now also quite fast, depsite that it still has to read all the working copies: mbp@hope% bzr status ~/work/linux-2.4.19 bzr status 5.51s user 0.79s system 99% cpu 6.337 total strace shows much of this is in write(2), probably because of logging. With more buffering on that file, removing all the explicit flushes, that is reduced to mbp@hope% time bzr status bzr status 5.23s user 0.42s system 97% cpu 5.780 total which is mostly opening, stating and reading files, as it should be. Still a few too many stat calls. Now fixed up handling of root directory. Without flushing everything to disk as it goes into the store: mbp@hope% bzr commit -m 'import linux 2.4.19' bzr commit -m 'import linux 2.4.19' 8.15s user 2.09s system 53% cpu 19.295 total mbp@hope% time bzr diff bzr diff 5.80s user 0.52s system 69% cpu 9.128 total mbp@hope% time bzr status bzr status 5.64s user 0.43s system 68% cpu 8.848 total patch -p1 < ../linux.pkg/patch-2.4.20 1.67s user 0.96s system 90% cpu 2.905 total The diff changes 3462 files according to diffstat. branch format: Bazaar-NG branch, format 0.0.4 in the working tree: 8674 unchanged 2463 modified 818 added 229 removed 0 renamed 0 unknown 4 ignored 614 versioned subdirectories That is, 3510 entries have changed, but there are 48 changed directories so the count is exactly right! bzr commit -v -m 'import 2.4.20' 8.23s user 1.09s system 48% cpu 19.411 total Kind of strange that this takes as much time as committing the whole thing; I suppose it has to read every file. This shows many files as being renamed; I don't know why that would be. commit refs/heads/master mark :93 committer 1111722470 +1100 data 54 Fix inverted display of 'R' and 'M' during 'commit -v' from :92 M 644 inline NEWS data 683 bzr-0.0.1 NOT RELEASED YET ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/branch.py data 27509 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset from store import ImmutableStore from revision import Revision from errors import bailout from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f is None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f last_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be bailout('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. :todo: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. :todo: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. :todo: Keep the on-disk branch locked while the object exists. :todo: mkdir() method. """ def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. :param base: Base directory for the branch. :param init: If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. :param find_root: If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch""" return file(self.controlfilename(file_or_path), mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # read in binary mode to detect newline wierdness. fmt = self.controlfile('branch-format', 'rb').read() if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() inv = Inventory.read_xml(self.controlfile('inventory', 'r')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'w') inv.write_xml(tmpf) tmpf.close() os.rename(tmpfname, self.controlfilename('inventory')) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. This puts the files in the Added state, so that they will be recorded by the next commit. :todo: Perhaps have an option to add the ids even if the files do not (yet) exist. :todo: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. :todo: Option to specify file id. :todo: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on :todo: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True :todo: Do something useful with directories. :todo: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. :param timestamp: if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) mutter("append to revision-history") self.controlfile('revision-history', 'at').write(rev_id + '\n') mutter("done!") def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. :todo: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: bailout("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original'): """Write out human-readable log of commits to this branch :param utc: If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l revno += 1 precursor = p def show_status(branch, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo :todo: Get state for single files. :todo: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = branch.basis_tree() old_inv = old.inventory new = branch.working_tree() new_inv = new.inventory for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("wierd file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files = []): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :94 committer 1111738880 +1100 data 22 more performance notes from :93 M 644 inline notes/performance.txt data 7608 For a tree holding 2.4.18 (two copies), 2.4.19, 2.4.20 With gzip -9: mbp@hope% du .bzr 195110 .bzr/text-store 20 .bzr/revision-store 12355 .bzr/inventory-store 216325 .bzr mbp@hope% du -s . 523128 . Without gzip: This is actually a pretty bad example because of deleting and re-importing 2.4.18, but still not totally unreasonable. ---- linux-2.4.0: 116399 kB after addding everything: 119505kB bzr status 2.68s user 0.13s system 84% cpu 3.330 total bzr commit 'import 2.4.0' 4.41s user 2.15s system 11% cpu 59.490 total 242446 . 122068 .bzr ---- Performance (2005-03-01) To add all files from linux-2.4.18: about 70s, mostly inventory serialization/deserialization. To commit: - finished, 6.520u/3.870s cpu, 33.940u/10.730s cum - 134.040 elapsed Interesting that it spends so long on external processing! I wonder if this is for running uuidgen? Let's try generating things internally. Great, this cuts it to 17.15s user 0.61s system 83% cpu 21.365 total to add, with no external command time. The commit now seems to spend most of its time copying to disk. - finished, 6.550u/3.320s cpu, 35.050u/9.870s cum - 89.650 elapsed I wonder where the external time is now? We were also using uuids() for revisions. Let's remove everything and re-add. Detecting everything was removed takes - finished, 2.460u/0.110s cpu, 0.000u/0.000s cum - 3.430 elapsed which may be mostly XML deserialization? Just getting the previous revision takes about this long: bzr invoked at Tue 2005-03-01 15:53:05.183741 EST +1100 by mbp@sourcefrog.net on hope arguments: ['/home/mbp/bin/bzr', 'get-revision-inventory', 'mbp@sourcefrog.net-20050301044608-8513202ab179aff4-44e8cd52a41aa705'] platform: Linux-2.6.10-4-686-i686-with-debian-3.1 - finished, 3.910u/0.390s cpu, 0.000u/0.000s cum - 6.690 elapsed Now committing the revision which removes all files should be fast. - finished, 1.280u/0.030s cpu, 0.000u/0.000s cum - 1.320 elapsed Now re-add with new code that doesn't call uuidgen: - finished, 1.990u/0.030s cpu, 0.000u/0.000s cum - 2.040 elapsed 16.61s user 0.55s system 74% cpu 22.965 total Status:: - finished, 2.500u/0.110s cpu, 0.010u/0.000s cum - 3.350 elapsed And commit:: Now patch up to 2.4.19. There were some bugs in handling missing directories, but with that fixed we do much better:: bzr status 5.86s user 1.06s system 10% cpu 1:05.55 total This is slow because it's diffing every file; we should use mtimes etc to make this faster. The cpu time is reasonable. I see difflib is pure Python; it might be faster to shell out to GNU diff when we need it. Export is very fast:: - finished, 4.220u/1.480s cpu, 0.010u/0.000s cum - 10.810 elapsed bzr export 1 ../linux-2.4.18.export1 3.92s user 1.72s system 21% cpu 26.030 total Now to find and add the new changes:: - finished, 2.190u/0.030s cpu, 0.000u/0.000s cum - 2.300 elapsed :: bzr commit 'import 2.4.19' 9.36s user 1.91s system 23% cpu 47.127 total And the result is exactly right. Try exporting:: mbp@hope% bzr export 4 ../linux-2.4.19.export4 bzr export 4 ../linux-2.4.19.export4 4.21s user 1.70s system 18% cpu 32.304 total and the export is exactly the same as the tarball. Now we can optimize the diff a bit more by not comparing files that have the right SHA-1 from within the commit For comparison:: patch -p1 < ../kernel.pkg/patch-2.4.20 1.61s user 1.03s system 13% cpu 19.106 total Now status after applying the .20 patch. With full-text verification:: bzr status 7.07s user 1.32s system 13% cpu 1:04.29 total with that turned off:: bzr status 5.86s user 0.56s system 25% cpu 25.577 total After adding: bzr status 6.14s user 0.61s system 25% cpu 26.583 total Should add some kind of profile counter for quick compares vs slow compares. bzr commit 'import 2.4.20' 7.57s user 1.36s system 20% cpu 43.568 total export: finished, 3.940u/1.820s cpu, 0.000u/0.000s cum, 50.990 elapsed also exports correctly now .21 bzr commit 'import 2.4.1' 5.59s user 0.51s system 60% cpu 10.122 total 265520 . 137704 .bzr import 2.4.2 317758 . 183463 .bzr with everything through to 2.4.29 imported, the .bzr directory is 1132MB, compared to 185MB for one tree. The .bzr.log is 100MB!. So the storage is 6.1 times larger, although we're holding 30 versions. It's pretty large but I think not ridiculous. By contrast the tarball for 2.4.0 is 104MB, and the tarball plus uncompressed patches are 315MB. Uncompressed, the text store is 1041MB. So it is only three times worse than patches, and could be compressed at presumably roughly equal efficiency. It is large, but also a very simple design and perhaps adequate for the moment. The text store with each file individually gziped is 264MB, which is also a very simple format and makes it less than twice the size of the source tree. This is actually rather pessimistic because I think there are some orphaned texts in there. Measured by du, the compressed full-text store is 363MB; also probably tolerable. The real fix is perhaps to use some kind of weave, not so much for storage efficiency as for fast annotation and therefore possible annotation-based merge. ----- 2005-03-25 Now we have recursive add, add is much faster. Adding all of the linux 2.4.19 kernel tree takes only finished, 5.460u/0.610s cpu, 0.010u/0.000s cum, 6.710 elapsed However, the store code currently flushes to disk after every write, which is probably excessive. So a commit takes finished, 8.740u/3.950s cpu, 0.010u/0.000s cum, 156.420 elapsed Status is now also quite fast, depsite that it still has to read all the working copies: mbp@hope% bzr status ~/work/linux-2.4.19 bzr status 5.51s user 0.79s system 99% cpu 6.337 total strace shows much of this is in write(2), probably because of logging. With more buffering on that file, removing all the explicit flushes, that is reduced to mbp@hope% time bzr status bzr status 5.23s user 0.42s system 97% cpu 5.780 total which is mostly opening, stating and reading files, as it should be. Still a few too many stat calls. Now fixed up handling of root directory. Without flushing everything to disk as it goes into the store: mbp@hope% bzr commit -m 'import linux 2.4.19' bzr commit -m 'import linux 2.4.19' 8.15s user 2.09s system 53% cpu 19.295 total mbp@hope% time bzr diff bzr diff 5.80s user 0.52s system 69% cpu 9.128 total mbp@hope% time bzr status bzr status 5.64s user 0.43s system 68% cpu 8.848 total patch -p1 < ../linux.pkg/patch-2.4.20 1.67s user 0.96s system 90% cpu 2.905 total The diff changes 3462 files according to diffstat. branch format: Bazaar-NG branch, format 0.0.4 in the working tree: 8674 unchanged 2463 modified 818 added 229 removed 0 renamed 0 unknown 4 ignored 614 versioned subdirectories That is, 3510 entries have changed, but there are 48 changed directories so the count is exactly right! bzr commit -v -m 'import 2.4.20' 8.23s user 1.09s system 48% cpu 19.411 total Kind of strange that this takes as much time as committing the whole thing; I suppose it has to read every file. This shows many files as being renamed; I don't know why that would be. Patch to 2.4.21: 2969 files changed, 366643 insertions(+), 147759 deletions(-) After auto-add: 2969 files changed, 372168 insertions(+), 153284 deletions(-) I wonder why it is not exactly the same? Maybe because the python diff algorithm is a bit differnt to GNU diff. commit refs/heads/master mark :95 committer 1111738956 +1100 data 45 only show count of added files with --verbose from :94 M 644 inline bzrlib/add.py data 2798 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import types, os, sys, stat import bzrlib from osutils import quotefn, appendpath from errors import bailout from trace import mutter, note def smart_add(file_list, verbose=False, recurse=True): """Add files to version, optionall recursing into directories. This is designed more towards DWIM for humans than API simplicity. For the specific behaviour see the help for cmd_add(). """ assert file_list assert not isinstance(file_list, basestring) b = bzrlib.branch.Branch(file_list[0], find_root=True) inv = b.read_working_inventory() tree = b.working_tree() count = 0 for f in file_list: rf = b.relpath(f) af = b.abspath(rf) ## TODO: It's OK to add root but only in recursive mode bzrlib.mutter("smart add of %r" % f) if bzrlib.branch.is_control_file(af): bailout("cannot add control file %r" % af) kind = bzrlib.osutils.file_kind(f) if kind != 'file' and kind != 'directory': bailout("can't add file of kind %r" % kind) versioned = (inv.path2id(rf) != None) if rf == '': mutter("branch root doesn't need to be added") elif versioned: mutter("%r is already versioned" % f) else: file_id = bzrlib.branch.gen_file_id(rf) inv.add_path(rf, kind=kind, file_id=file_id) bzrlib.mutter("added %r kind %r file_id={%s}" % (rf, kind, file_id)) count += 1 if verbose: bzrlib.textui.show_status('A', kind, quotefn(f)) if kind == 'directory' and recurse: for subf in os.listdir(af): subp = appendpath(rf, subf) if tree.is_ignored(subp): mutter("skip ignored sub-file %r" % subp) else: mutter("queue to add sub-file %r" % (subp)) file_list.append(subp) if count > 0: if verbose: note('added %d' % count) b._write_inventory(inv) commit refs/heads/master mark :96 committer 1111739196 +1100 data 46 with commit -v, show committed revision number from :95 M 644 inline NEWS data 737 bzr-0.0.1 NOT RELEASED YET ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/branch.py data 27585 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset from store import ImmutableStore from revision import Revision from errors import bailout from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f is None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f last_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be bailout('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. :todo: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. :todo: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. :todo: Keep the on-disk branch locked while the object exists. :todo: mkdir() method. """ def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. :param base: Base directory for the branch. :param init: If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. :param find_root: If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch""" return file(self.controlfilename(file_or_path), mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # read in binary mode to detect newline wierdness. fmt = self.controlfile('branch-format', 'rb').read() if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() inv = Inventory.read_xml(self.controlfile('inventory', 'r')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'w') inv.write_xml(tmpf) tmpf.close() os.rename(tmpfname, self.controlfilename('inventory')) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. This puts the files in the Added state, so that they will be recorded by the next commit. :todo: Perhaps have an option to add the ids even if the files do not (yet) exist. :todo: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. :todo: Option to specify file id. :todo: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on :todo: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True :todo: Do something useful with directories. :todo: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. :param timestamp: if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) mutter("append to revision-history") f = self.controlfile('revision-history', 'at') f.write(rev_id + '\n') f.close() if verbose: note("commited r%d" % self.revno()) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. :todo: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: bailout("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original'): """Write out human-readable log of commits to this branch :param utc: If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l revno += 1 precursor = p def show_status(branch, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo :todo: Get state for single files. :todo: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = branch.basis_tree() old_inv = old.inventory new = branch.working_tree() new_inv = new.inventory for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("wierd file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files = []): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :97 committer 1111806149 +1100 data 18 - more commit help from :96 M 644 inline bzrlib/commands.py data 22660 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """Show diff from basis to working copy. :todo: Take one or two revision arguments, look up those trees, and diff them. :todo: Allow diff across branches. :todo: Mangle filenames in diff to be more relevant. :todo: Shouldn't be in the cmd function. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. """ b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = cmdfn.__doc__ if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'commit': [], 'diff': [], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'relpath': ['filename'], 'remove': ['file+'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :98 committer 1111806208 +1100 data 22 - update release notes from :97 M 644 inline README data 1019 *********************************** Release notes for Bazaar-NG (pre-0) *********************************** mbp@sourcefrog.net, March 2005, Canberra Caveats ------- * There is little locking or transaction control here; if you interrupt it the tree may be arbitrarily broken. This will be fixed. * Don't use this for critical data; at the very least keep separate regular snapshots of your tree. Dependencies ------------ This is mostly developed on Linux (Ubuntu); it should work on Unix, Windows, or OS X with relatively little trouble. The only dependency is Python, at least 2.3 and preferably 2.4. You may optionally install cElementTree to speed up some operations. Installation ------------ The best way to install bzr is to symlink the ``bzr`` command onto a directory on your path. For example:: ln -s ~/work/bzr/bzr ~/bin/bzr If you use a symlink for this, Python will be able to automatically find the bzr libraries. Otherwise you must ensure they are listed on your $PYTHONPATH. commit refs/heads/master mark :99 committer 1111806607 +1100 data 24 update for release 0.0.1 from :98 M 644 inline NEWS data 731 bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/__init__.py data 1264 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr library""" from inventory import Inventory, InventoryEntry from branch import Branch, ScratchBranch from osutils import format_date from tree import Tree from diff import diff_trees from trace import mutter, warning import add BZRDIR = ".bzr" DEFAULT_IGNORE = ['.*', '*~', '#*#', '*.tmp', '*.o', '*.a', '*.py[oc]', '{arch}'] IGNORE_FILENAME = ".bzrignore" __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __version__ = '0.0.1' commit refs/heads/master mark :100 committer 1111844513 +1100 data 32 - add test case for ignore files from :99 M 644 inline bzrlib/branch.py data 27666 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset from store import ImmutableStore from revision import Revision from errors import bailout from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f is None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f last_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be bailout('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. :todo: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. :todo: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. :todo: Keep the on-disk branch locked while the object exists. :todo: mkdir() method. """ def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. :param base: Base directory for the branch. :param init: If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. :param find_root: If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch""" return file(self.controlfilename(file_or_path), mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # read in binary mode to detect newline wierdness. fmt = self.controlfile('branch-format', 'rb').read() if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() inv = Inventory.read_xml(self.controlfile('inventory', 'r')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'w') inv.write_xml(tmpf) tmpf.close() os.rename(tmpfname, self.controlfilename('inventory')) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. This puts the files in the Added state, so that they will be recorded by the next commit. :todo: Perhaps have an option to add the ids even if the files do not (yet) exist. :todo: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. :todo: Option to specify file id. :todo: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on :todo: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True :todo: Do something useful with directories. :todo: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. :param timestamp: if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) mutter("append to revision-history") f = self.controlfile('revision-history', 'at') f.write(rev_id + '\n') f.close() if verbose: note("commited r%d" % self.revno()) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. :todo: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: bailout("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original'): """Write out human-readable log of commits to this branch :param utc: If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l revno += 1 precursor = p def show_status(branch, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo :todo: Get state for single files. :todo: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = branch.basis_tree() old_inv = old.inventory new = branch.working_tree() new_inv = new.inventory for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("wierd file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/tests.py data 5574 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # XXX: We might prefer these to be in a text file rather than Python # source, but that only works in doctest from Python 2.4 and later, # which is not present in Warty. r""" Bazaar-NG test cases ******************** These are run by ``bzr.doctest``. >>> import bzrlib, os >>> from bzrlib import ScratchBranch >>> bzrlib.commands.cmd_rocks() it sure does! Hey, nice place to begin. The basic object is a Branch. We have a special helper class ScratchBranch that automatically makes a directory and cleans itself up, but is in other respects identical. ScratchBranches are initially empty: >>> b = bzrlib.ScratchBranch() >>> b.show_status() New files in that directory are, it is initially unknown: >>> file(b.base + '/hello.c', 'wt').write('int main() {}') >>> b.show_status() ? hello.c That's not quite true; some files (like editor backups) are ignored by default: >>> file(b.base + '/hello.c~', 'wt').write('int main() {}') >>> b.show_status() ? hello.c >>> list(b.unknowns()) ['hello.c'] The ``add`` command marks a file to be added in the next revision: >>> b.add('hello.c') >>> b.show_status() A hello.c You can also add files that otherwise would be ignored. The ignore patterns only apply to files that would be otherwise unknown, so they have no effect once it's added. >>> b.add('hello.c~') >>> b.show_status() A hello.c A hello.c~ It is an error to add a file that isn't present in the working copy: >>> b.add('nothere') Traceback (most recent call last): ... BzrError: ('cannot add: not a regular file or directory: nothere', []) If we add a file and then change our mind, we can either revert it or remove the file. If we revert, we are left with the working copy (in either I or ? state). If we remove, the working copy is gone. Let's do that to the backup, presumably added accidentally. >>> b.remove('hello.c~') >>> b.show_status() A hello.c Now to commit, creating a new revision. (Fake the date and name for reproducibility.) >>> b.commit('start hello world', timestamp=0, committer='foo@nowhere') >>> b.show_status() >>> b.show_status(show_all=True) . hello.c I hello.c~ We can look back at history >>> r = b.get_revision(b.lookup_revision(1)) >>> r.message 'start hello world' >>> b.write_log(show_timezone='utc') ---------------------------------------- revno: 1 committer: foo@nowhere timestamp: Thu 1970-01-01 00:00:00 +0000 message: start hello world (The other fields will be a bit unpredictable, depending on who ran this test and when.) As of 2005-02-21, we can also add subdirectories to the revision! >>> os.mkdir(b.base + "/lib") >>> b.show_status() ? lib/ >>> b.add('lib') >>> b.show_status() A lib/ >>> b.commit('add subdir') >>> b.show_status() >>> b.show_status(show_all=True) . hello.c I hello.c~ . lib/ and we can also add files within subdirectories: >>> file(b.base + '/lib/hello', 'w').write('hello!\n') >>> b.show_status() ? lib/hello Tests for adding subdirectories, etc. >>> b = bzrlib.branch.ScratchBranch() >>> os.mkdir(b.abspath('d1')) >>> os.mkdir(b.abspath('d2')) >>> os.mkdir(b.abspath('d2/d3')) >>> list(b.working_tree().unknowns()) ['d1', 'd2'] Create some files, but they're not seen as unknown yet: >>> file(b.abspath('d1/f1'), 'w').close() >>> file(b.abspath('d2/f2'), 'w').close() >>> file(b.abspath('d2/f3'), 'w').close() >>> [v[0] for v in b.inventory.directories()] [''] >>> list(b.working_tree().unknowns()) ['d1', 'd2'] Adding a directory, and we see the file underneath: >>> b.add('d1') >>> [v[0] for v in b.inventory.directories()] ['', 'd1'] >>> list(b.working_tree().unknowns()) ['d1/f1', 'd2'] >>> # d2 comes first because it's in the top directory >>> b.add('d2') >>> b.commit('add some stuff') >>> list(b.working_tree().unknowns()) ['d1/f1', 'd2/d3', 'd2/f2', 'd2/f3'] >>> b.add('d1/f1') >>> list(b.working_tree().unknowns()) ['d2/d3', 'd2/f2', 'd2/f3'] Tests for ignored files and patterns: >>> b = ScratchBranch(dirs=['src', 'doc'], ... files=['configure.in', 'configure', ... 'doc/configure', 'foo.c', ... 'foo']) >>> list(b.unknowns()) ['configure', 'configure.in', 'doc', 'foo', 'foo.c', 'src'] >>> b.add(['doc', 'foo.c', 'src', 'configure.in']) >>> list(b.unknowns()) ['configure', 'doc/configure', 'foo'] >>> f = file(b.abspath('.bzrignore'), 'w') >>> f.write('./configure\n' ... './foo\n') >>> f.close() >>> b.add('.bzrignore') >>> list(b.unknowns()) ['configure', 'doc/configure', 'foo'] """ commit refs/heads/master mark :101 committer 1111914885 +1000 data 26 change default ignore list from :100 M 644 inline NEWS data 898 bzr-0.0.2 NOT RELEASED YET CHANGES: * Default ignore list changed to .bzr.log, *~, #*#, *.tmp, *.bak, *.o, *.a, *.py[oc], {arch}, CVS, .svn, _darcs. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/__init__.py data 1312 # (C) 2005 Canonical Development Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr library""" from inventory import Inventory, InventoryEntry from branch import Branch, ScratchBranch from osutils import format_date from tree import Tree from diff import diff_trees from trace import mutter, warning import add BZRDIR = ".bzr" DEFAULT_IGNORE = ['.bzr.log', '*~', '#*#', '*.tmp', '*.bak', '*.o', '*.a', '*.py[oc]', '{arch}', 'CVS', '.svn', '_darcs'] IGNORE_FILENAME = ".bzrignore" __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __version__ = '0.0.1' commit refs/heads/master mark :102 committer 1111917034 +1000 data 64 - cache ignore list in Tree - bzrignore adds to default patterns from :101 M 644 inline NEWS data 1071 bzr-0.0.2 NOT RELEASED YET CHANGES: * Default ignore list changed to .bzr.log, *~, #*#, *.tmp, *.bak, *.o, *.a, *.py[oc], {arch}, CVS, .svn, _darcs. * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. ENHANCEMENTS: * Ignore list is memoized after first request. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/tree.py data 12735 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tree classes, representing directory at point in time. """ from sets import Set import os.path, os, fnmatch from inventory import Inventory from trace import mutter, note from osutils import pumpfile, compare_files, filesize, quotefn, sha_file, \ joinpath, splitpath, appendpath, isdir, isfile, file_kind from errors import bailout import branch from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE import bzrlib class Tree: """Abstract file tree. There are several subclasses: * `WorkingTree` exists as files on disk editable by the user. * `RevisionTree` is a tree as recorded at some point in the past. * `EmptyTree` Trees contain an `Inventory` object, and also know how to retrieve file texts mentioned in the inventory, either from a working directory or from a store. It is possible for trees to contain files that are not described in their inventory or vice versa; for this use `filenames()`. Trees can be compared, etc, regardless of whether they are working trees or versioned trees. """ def has_filename(self, filename): """True if the tree has given filename.""" raise NotImplementedError() def has_id(self, file_id): return self.inventory.has_id(file_id) def id_set(self): """Return set of all ids in this tree.""" return self.inventory.id_set() def id2path(self, file_id): return self.inventory.id2path(file_id) def _get_inventory(self): return self._inventory inventory = property(_get_inventory, doc="Inventory of this Tree") def _check_retrieved(self, ie, f): # TODO: Test this check by damaging the store? if ie.text_size is not None: fs = filesize(f) if fs != ie.text_size: bailout("mismatched size for file %r in %r" % (ie.file_id, self._store), ["inventory expects %d bytes" % ie.text_size, "file is actually %d bytes" % fs, "store is probably damaged/corrupt"]) f_hash = sha_file(f) f.seek(0) if ie.text_sha1 != f_hash: bailout("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store), ["inventory expects %s" % ie.text_sha1, "file is actually %s" % f_hash, "store is probably damaged/corrupt"]) def export(self, dest): """Export this tree to a new directory. `dest` should not exist, and will be created holding the contents of this tree. :todo: To handle subdirectories we need to create the directories first. :note: If the export fails, the destination directory will be left in a half-assed state. """ os.mkdir(dest) mutter('export version %r' % self) inv = self.inventory for dp, ie in inv.iter_entries(): kind = ie.kind fullpath = appendpath(dest, dp) if kind == 'directory': os.mkdir(fullpath) elif kind == 'file': pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb')) else: bailout("don't know how to export {%s} of kind %r", fid, kind) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) class WorkingTree(Tree): """Working copy tree. The inventory is held in the `Branch` working-inventory, and the files are in a directory on disk. It is possible for a `WorkingTree` to have a filename which is not listed in the Inventory and vice versa. """ def __init__(self, basedir, inv): self._inventory = inv self.basedir = basedir self.path2id = inv.path2id def __repr__(self): return "<%s of %s>" % (self.__class__.__name__, self.basedir) def abspath(self, filename): return os.path.join(self.basedir, filename) def has_filename(self, filename): return os.path.exists(self.abspath(filename)) def get_file(self, file_id): return self.get_file_byname(self.id2path(file_id)) def get_file_byname(self, filename): return file(self.abspath(filename), 'rb') def _get_store_filename(self, file_id): return self.abspath(self.id2path(file_id)) def has_id(self, file_id): # files that have been deleted are excluded if not self.inventory.has_id(file_id): return False return os.access(self.abspath(self.inventory.id2path(file_id)), os.F_OK) def get_file_size(self, file_id): return os.stat(self._get_store_filename(file_id))[ST_SIZE] def get_file_sha1(self, file_id): f = self.get_file(file_id) return sha_file(f) def file_class(self, filename): if self.path2id(filename): return 'V' elif self.is_ignored(filename): return 'I' else: return '?' def file_kind(self, filename): if isfile(self.abspath(filename)): return 'file' elif isdir(self.abspath(filename)): return 'directory' else: return 'unknown' def list_files(self): """Recursively list all files as (path, class, kind, id). Lists, but does not descend into unversioned directories. This does not include files that have been deleted in this tree. Skips the control directory. """ inv = self.inventory def descend(from_dir, from_dir_id, dp): ls = os.listdir(dp) ls.sort() for f in ls: if bzrlib.BZRDIR == f: continue # path within tree fp = appendpath(from_dir, f) # absolute path fap = appendpath(dp, f) f_ie = inv.get_child(from_dir_id, f) if f_ie: c = 'V' elif self.is_ignored(fp): c = 'I' else: c = '?' fk = file_kind(fap) if f_ie: if f_ie.kind != fk: bailout("file %r entered as kind %r id %r, now of kind %r" % (fap, f_ie.kind, f_ie.file_id, fk)) yield fp, c, fk, (f_ie and f_ie.file_id) if fk != 'directory': continue if c != 'V': # don't descend unversioned directories continue for ff in descend(fp, f_ie.file_id, fap): yield ff for f in descend('', None, self.basedir): yield f def unknowns(self, path='', dir_id=None): """Yield names of unknown files in this WorkingTree. If there are any unknown directories then only the directory is returned, not all its children. But if there are unknown files under a versioned subdirectory, they are returned. Currently returned depth-first, sorted by name within directories. """ for fpath, fclass, fkind, fid in self.list_files(): if fclass == '?': yield fpath def ignored_files(self): for fpath, fclass, fkind, fid in self.list_files(): if fclass == 'I': yield fpath def get_ignore_list(self): """Return list of ignore patterns. Cached in the Tree object after the first call. """ if hasattr(self, '_ignorelist'): return self._ignorelist l = bzrlib.DEFAULT_IGNORE[:] if self.has_filename(bzrlib.IGNORE_FILENAME): f = self.get_file_byname(bzrlib.IGNORE_FILENAME) l.extend([line.rstrip("\n\r") for line in f.readlines()]) self._ignorelist = l return l def is_ignored(self, filename): """Check whether the filename matches an ignore pattern. Patterns containing '/' need to match the whole path; others match against only the last component.""" ## TODO: Use extended zsh-style globs maybe? ## TODO: Use '**' to match directories? for pat in self.get_ignore_list(): if '/' in pat: if fnmatch.fnmatchcase(filename, pat): return True else: if fnmatch.fnmatchcase(splitpath(filename)[-1], pat): return True return False class RevisionTree(Tree): """Tree viewing a previous revision. File text can be retrieved from the text store. :todo: Some kind of `__repr__` method, but a good one probably means knowing the branch and revision number, or at least passing a description to the constructor. """ def __init__(self, store, inv): self._store = store self._inventory = inv def get_file(self, file_id): ie = self._inventory[file_id] f = self._store[ie.text_id] mutter(" get fileid{%s} from %r" % (file_id, self)) fs = filesize(f) if ie.text_size is None: note("warning: no text size recorded on %r" % ie) self._check_retrieved(ie, f) return f def get_file_size(self, file_id): return self._inventory[file_id].text_size def get_file_sha1(self, file_id): ie = self._inventory[file_id] return ie.text_sha1 def has_filename(self, filename): return bool(self.inventory.path2id(filename)) def list_files(self): # The only files returned by this are those from the version for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id class EmptyTree(Tree): def __init__(self): self._inventory = Inventory() def has_filename(self, filename): return False def list_files(self): if False: # just to make it a generator yield None ###################################################################### # diff # TODO: Merge these two functions into a single one that can operate # on either a whole tree or a set of files. # TODO: Return the diff in order by filename, not by category or in # random order. Can probably be done by lock-stepping through the # filenames from both trees. def file_status(filename, old_tree, new_tree): """Return single-letter status, old and new names for a file. The complexity here is in deciding how to represent renames; many complex cases are possible. """ old_inv = old_tree.inventory new_inv = new_tree.inventory new_id = new_inv.path2id(filename) old_id = old_inv.path2id(filename) if not new_id and not old_id: # easy: doesn't exist in either; not versioned at all if new_tree.is_ignored(filename): return 'I', None, None else: return '?', None, None elif new_id: # There is now a file of this name, great. pass else: # There is no longer a file of this name, but we can describe # what happened to the file that used to have # this name. There are two possibilities: either it was # deleted entirely, or renamed. assert old_id if new_inv.has_id(old_id): return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id) else: return 'D', old_inv.id2path(old_id), None # if the file_id is new in this revision, it is added if new_id and not old_inv.has_id(new_id): return 'A' # if there used to be a file of this name, but that ID has now # disappeared, it is deleted if old_id and not new_inv.has_id(old_id): return 'D' return 'wtf?' commit refs/heads/master mark :103 committer 1111917182 +1000 data 41 Skip control directories in recursive add from :102 M 644 inline bzrlib/add.py data 2905 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import types, os, sys, stat import bzrlib from osutils import quotefn, appendpath from errors import bailout from trace import mutter, note def smart_add(file_list, verbose=False, recurse=True): """Add files to version, optionall recursing into directories. This is designed more towards DWIM for humans than API simplicity. For the specific behaviour see the help for cmd_add(). """ assert file_list assert not isinstance(file_list, basestring) b = bzrlib.branch.Branch(file_list[0], find_root=True) inv = b.read_working_inventory() tree = b.working_tree() count = 0 for f in file_list: rf = b.relpath(f) af = b.abspath(rf) ## TODO: It's OK to add root but only in recursive mode bzrlib.mutter("smart add of %r" % f) if bzrlib.branch.is_control_file(af): bailout("cannot add control file %r" % af) kind = bzrlib.osutils.file_kind(f) if kind != 'file' and kind != 'directory': bailout("can't add file of kind %r" % kind) versioned = (inv.path2id(rf) != None) if rf == '': mutter("branch root doesn't need to be added") elif versioned: mutter("%r is already versioned" % f) else: file_id = bzrlib.branch.gen_file_id(rf) inv.add_path(rf, kind=kind, file_id=file_id) bzrlib.mutter("added %r kind %r file_id={%s}" % (rf, kind, file_id)) count += 1 if verbose: bzrlib.textui.show_status('A', kind, quotefn(f)) if kind == 'directory' and recurse: for subf in os.listdir(af): subp = appendpath(rf, subf) if subf == bzrlib.BZRDIR: mutter("skip control directory %r" % subp) elif tree.is_ignored(subp): mutter("skip ignored sub-file %r" % subp) else: mutter("queue to add sub-file %r" % (subp)) file_list.append(subp) if count > 0: if verbose: note('added %d' % count) b._write_inventory(inv) commit refs/heads/master mark :104 committer 1111917976 +1000 data 36 avoid slow platform module functions from :103 M 644 inline bzrlib/trace.py data 3446 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os, time, socket, stat import bzrlib ###################################################################### # messages and logging ## TODO: If --verbose is given then write to both stderr and ## _tracefile; perhaps replace _tracefile with a tee thing. global _tracefile, _starttime # used to have % (os.environ['USER'], time.time(), os.getpid()), 'w') # If false, notes also go to stdout; should replace this with --silent # at some point. silent = False verbose = False def warning(msg): b = 'bzr: warning: ' + msg + '\n' sys.stderr.write(b) _tracefile.write(b) #_tracefile.flush() def mutter(msg): _tracefile.write(msg) _tracefile.write('\n') # _tracefile.flush() if verbose: sys.stderr.write('- ' + msg + '\n') def note(msg): b = '* ' + str(msg) + '\n' if not silent: sys.stderr.write(b) _tracefile.write(b) # _tracefile.flush() def log_error(msg): sys.stderr.write(msg) _tracefile.write(msg) # _tracefile.flush() def create_tracefile(argv): # TODO: Also show contents of /etc/lsb-release, if it can be parsed. # Perhaps that should eventually go into the platform library? # TODO: If the file doesn't exist, add a note describing it. # Messages are always written to here, so that we have some # information if something goes wrong. In a future version this # file will be removed on successful completion. global _starttime, _tracefile _starttime = os.times()[4] _tracefile = file('.bzr.log', 'at') t = _tracefile if os.fstat(t.fileno())[stat.ST_SIZE] == 0: t.write("\nthis is a debug log for diagnosing/reporting problems in bzr\n") t.write("you can delete or truncate this file, or include sections in\n") t.write("bug reports to bazaar-ng@lists.canonical.com\n\n") # TODO: If we failed to create the file, perhaps give a warning # but don't abort; send things to /dev/null instead? t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % bzrlib.osutils.format_date(time.time())) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.getfqdn())) t.write(' arguments: %r\n' % argv) # This causes a vfork; I don't care enough about it. t.write(' platform: %s\n' % sys.platform) t.write(' python: %s\n' % (sys.version_info,)) import atexit atexit.register(_close_trace) def _close_trace(): times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum, %.3f elapsed" % (times[:4] + ((times[4] - _starttime),))) commit refs/heads/master mark :105 committer 1111968801 +1000 data 30 show bzr version in trace file from :104 M 644 inline bzrlib/trace.py data 3498 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os, time, socket, stat import bzrlib ###################################################################### # messages and logging ## TODO: If --verbose is given then write to both stderr and ## _tracefile; perhaps replace _tracefile with a tee thing. global _tracefile, _starttime # used to have % (os.environ['USER'], time.time(), os.getpid()), 'w') # If false, notes also go to stdout; should replace this with --silent # at some point. silent = False verbose = False def warning(msg): b = 'bzr: warning: ' + msg + '\n' sys.stderr.write(b) _tracefile.write(b) #_tracefile.flush() def mutter(msg): _tracefile.write(msg) _tracefile.write('\n') # _tracefile.flush() if verbose: sys.stderr.write('- ' + msg + '\n') def note(msg): b = '* ' + str(msg) + '\n' if not silent: sys.stderr.write(b) _tracefile.write(b) # _tracefile.flush() def log_error(msg): sys.stderr.write(msg) _tracefile.write(msg) # _tracefile.flush() def create_tracefile(argv): # TODO: Also show contents of /etc/lsb-release, if it can be parsed. # Perhaps that should eventually go into the platform library? # TODO: If the file doesn't exist, add a note describing it. # Messages are always written to here, so that we have some # information if something goes wrong. In a future version this # file will be removed on successful completion. global _starttime, _tracefile _starttime = os.times()[4] _tracefile = file('.bzr.log', 'at') t = _tracefile if os.fstat(t.fileno())[stat.ST_SIZE] == 0: t.write("\nthis is a debug log for diagnosing/reporting problems in bzr\n") t.write("you can delete or truncate this file, or include sections in\n") t.write("bug reports to bazaar-ng@lists.canonical.com\n\n") # TODO: If we failed to create the file, perhaps give a warning # but don't abort; send things to /dev/null instead? t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % bzrlib.osutils.format_date(time.time())) t.write(' version: %s\n' % bzrlib.__version__) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.getfqdn())) t.write(' arguments: %r\n' % argv) # This causes a vfork; I don't care enough about it. t.write(' platform: %s\n' % sys.platform) t.write(' python: %s\n' % (sys.version_info,)) import atexit atexit.register(_close_trace) def _close_trace(): times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum, %.3f elapsed" % (times[:4] + ((times[4] - _starttime),))) commit refs/heads/master mark :106 committer 1111968844 +1000 data 32 design notes for ignore patterns from :105 M 644 inline doc/ignore.txt data 560 Ignore patterns *************** Ignore patterns need to be flexible but simple. Based on the system from rsync, which is in turn based on that of CVS. First build a list by concatenating: * per-user configuration * per-tree configuration * command line options Just one ``.bzrignore`` at tree root? File can contain '+ ' and '- ' prefixes to include and exclude things, also '#' for comments. Should have a ``--ignore`` command line option that specifies ignore patterns; by putting a ! in this you can turn off any other includes. (Similar to CVS.) M 644 inline doc/index.txt data 5790 Bazaar-NG ********* .. These documents are formatted as ReStructuredText. You can .. .. convert them to HTML, PDF, etc using the ``python-docutils`` .. .. package. .. *Bazaar-NG* (``bzr``) is a project of `Canonical Ltd`__ to develop an open source distributed version control system that is powerful, friendly, and scalable. The project is at an early stage of development. __ http://canonical.com/ **Note:** These documents are in a very preliminary state, and so may be internally or externally inconsistent or redundant. Comments are still very welcome. Please send them to . For more information, see the homepage at http://bazaar-ng.org/ User documentation ------------------ * `Project overview/introduction `__ * `Command reference `__ -- intended to be user documentation, and gives the best overview at the moment of what the system will feel like to use. Fairly complete. * `Quick reference `__ -- single page description of how to use, intended to check it's adequately simple. Incomplete. * `FAQ `__ -- mostly user-oriented FAQ. Requirements and general design ------------------------------- * `Various purposes of a VCS `__ -- taking snapshots and helping with merges is not the whole story. * `Requirements `__ * `Costs `__ of various factors: time, disk, network, etc. * `Deadly sins `__ that gcc maintainers suggest we avoid. * `Overview of the whole design `__ and miscellaneous small design points. * `File formats `__ * `Random observations `__ that don't fit anywhere else yet. Design of particular features ----------------------------- * `Automatic generation of ChangeLogs `__ * `Cherry picking `__ -- merge just selected non-contiguous changes from a branch. * `Common changeset format `__ for interchange format between VCS. * `Compression `__ of file text for more efficient storage. * `Config specs `__ assemble a tree from several places. * `Conflicts `_ that can occur during merge-like operations. * `Ignored files `__ * `Recovering from interrupted operations `__ * `Inventory command `__ * `Branch joins `__ represent that all the changes from one branch are integrated into another. * `Kill a version `__ to fix a broken commit or wrong message, or to remove confidential information from the history. * `Hash collisions `__ and weaknesses, and the security implications thereof. * `Layers `__ within the design * `Library interface `__ for Python. * `Merge `__ * `Mirroring `__ * `Optional edit command `__: sometimes people want to make the working copy read-only, or not present at all. * `Partial commits `__ * `Patch pools `__ to efficiently store related branches. * `Revision syntax `__ -- ``hello.c@12``, etc. * `Roll-up commits `__ -- a single revision incorporates the changes from several others. * `Scalability `__ * `Security `__ * `Shared branches `__ maintained by more than one person * `Supportability `__ -- how to handle any bugs or problems in the field. * `Place tags on revisions for easy reference `__ * `Detecting unchanged files `__ * `Merging previously-unrelated branches `__ * `Usability principles `__ (very small at the moment) * ``__ * ``__ * ``__ Modelling/controlling flow of patches. * ``__ -- Discussion of using YAML_ as a storage or transmission format. .. _YAML: http://www.yaml.org/ Comparisons to other systems ---------------------------- * `Taxonomy `__: basic questions a VCS must answer. * `Bitkeeper `__, the proprietary system used by some kernel developers. * `Aegis `__, a tool focussed on enforcing process and workflow. * `Codeville `__ has an intruiging but scarcely-documented merge algorithm. * `CVSNT `__, with more Windows support and some merge enhancements. * `OpenCM `__, another hash-based tool with a good whitepaper. * `PRCS `__, a non-distributed inventory-based tool. * `GNU Arch `__, with many pros and cons. * `Darcs `__, a merge-focussed tool with good usability. * `Quilt `__ -- Andrew Morton's patch scripts, popular with kernel maintainers. * `Monotone `__, Graydon Hoare's hash-based distributed system. * `SVK `__ -- distributed operation stacked on Subversion. * `Sun Teamware `__ Project management and organization ----------------------------------- * `Development news `__ * `Notes on how to get a VCS adopted `__ * `Testing plan `__ -- very sketchy. * `Thanks `__ to various people * `Roadmap `__: High-level order for implementing features. * ``__: current tasks. * `Extra commands `__ for internal/developer/debugger use. * `Choice of Python as a development language `__ commit refs/heads/master mark :107 committer 1111976633 +1000 data 23 upload docs using rsync from :106 M 644 inline doc/Makefile data 192 %.html: %.txt rest2html $^ > $@.tmp && mv $@.tmp $@ all: $(addsuffix .html,$(basename $(wildcard *.txt))) upload: all rsync -av *html escudero.ubuntu.com:/srv/www.bazaar-ng.org/www/doc/ commit refs/heads/master mark :108 committer 1111976658 +1000 data 28 doc: more on ignore patterns from :107 M 644 inline doc/ignore.txt data 654 Ignore patterns *************** Ignore patterns need to be flexible but simple. Based on the system from rsync, which is in turn based on that of CVS. However, I won't implement all of this at first but rather add bits as they seem to be needed. First build a list by concatenating: * per-user configuration * per-tree configuration * command line options Just one ``.bzrignore`` at tree root? File can contain '+ ' and '- ' prefixes to include and exclude things, also '#' for comments. Should have a ``--ignore`` command line option that specifies ignore patterns; by putting a ! in this you can turn off any other includes. (Similar to CVS.) commit refs/heads/master mark :109 committer 1111977150 +1000 data 26 more help for diff command from :108 M 644 inline bzrlib/commands.py data 22696 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] --revision REV Show changes since REV, rather than predecessor. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Diff selected files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = cmdfn.__doc__ if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'commit': [], 'diff': [], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'relpath': ['filename'], 'remove': ['file+'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :110 committer 1111977575 +1000 data 40 Add more patterns to default ignore list from :109 M 644 inline NEWS data 1034 bzr-0.0.2 NOT RELEASED YET CHANGES: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. ENHANCEMENTS: * Ignore list is memoized after first request. * More help topics. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/__init__.py data 1485 # (C) 2005 Canonical Development Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr library""" from inventory import Inventory, InventoryEntry from branch import Branch, ScratchBranch from osutils import format_date from tree import Tree from diff import diff_trees from trace import mutter, warning import add BZRDIR = ".bzr" DEFAULT_IGNORE = ['.bzr.log', '*~', '#*#', '*$', '.#*', '*.tmp', '*.bak', '*.BAK', '*.orig', '*.o', '*.obj', '*.a', '*.py[oc]', '*.so', '*.exe', '*.elc', '{arch}', 'CVS', '.svn', '_darcs', 'SCCS', 'RCS', 'TAGS', '.make.state', '.sconsign'] IGNORE_FILENAME = ".bzrignore" __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __version__ = '0.0.1' commit refs/heads/master mark :111 committer 1111977649 +1000 data 62 Make fields wider in 'bzr info' output to accomodate big trees from :110 M 644 inline bzrlib/info.py data 3304 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import time import bzrlib from osutils import format_date def show_info(b): # TODO: Maybe show space used by working tree, versioned files, # unknown files, text store. print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl is not None: return pl else: return 's' count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %8d %s' % (count_status[fs], name) print ' %8d versioned subdirector%s' % (count_version_dirs, plural(count_version_dirs, 'y', 'ies')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %8d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %8d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %8d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) print print 'text store:' c, t = b.text_store.total_size() print ' %8d file texts' % c print ' %8d kB' % (t/1024) print print 'revision store:' c, t = b.revision_store.total_size() print ' %8d revisions' % c print ' %8d kB' % (t/1024) print print 'inventory store:' c, t = b.inventory_store.total_size() print ' %8d inventories' % c print ' %8d kB' % (t/1024) commit refs/heads/master mark :112 committer 1111977847 +1000 data 21 help for info command from :111 M 644 inline bzrlib/commands.py data 22774 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] --revision REV Show changes since REV, rather than predecessor. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Diff selected files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(): """Check consistency of the branch.""" check() def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = cmdfn.__doc__ if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'commit': [], 'diff': [], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'relpath': ['filename'], 'remove': ['file+'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :113 committer 1111978186 +1000 data 27 More help for check command from :112 M 644 inline bzrlib/check.py data 3555 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ###################################################################### # consistency checks def check(): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ assert_in_tree() mutter("checking tree") check_patches_exist() check_patch_chaining() check_patch_uniqueness() check_inventory() mutter("tree looks OK") ## TODO: Check that previous-inventory and previous-manifest ## are the same as those stored in the previous changeset. ## TODO: Check all patches present in patch directory are ## mentioned in patch history; having an orphaned patch only gives ## a warning. ## TODO: Check cached data is consistent with data reconstructed ## from scratch. ## TODO: Check no control files are versioned. ## TODO: Check that the before-hash of each file in a later ## revision matches the after-hash in the previous revision to ## touch it. def check_inventory(): mutter("checking inventory file and ids...") seen_ids = Set() seen_names = Set() for l in controlfile('inventory').readlines(): parts = l.split() if len(parts) != 2: bailout("malformed inventory line: " + `l`) file_id, name = parts if file_id in seen_ids: bailout("duplicated file id " + file_id) seen_ids.add(file_id) if name in seen_names: bailout("duplicated file name in inventory: " + quotefn(name)) seen_names.add(name) if is_control_file(name): raise BzrError("control file %s present in inventory" % quotefn(name)) def check_patches_exist(): """Check constraint of current version: all patches exist""" mutter("checking all patches are present...") for pid in revision_history(): read_patch_header(pid) def check_patch_chaining(): """Check ancestry of patches and history file is consistent""" mutter("checking patch chaining...") prev = None for pid in revision_history(): log_prev = read_patch_header(pid).precursor if log_prev != prev: bailout("inconsistent precursor links on " + pid) prev = pid def check_patch_uniqueness(): """Make sure no patch is listed twice in the history. This should be implied by having correct ancestry but I'll check it anyhow.""" mutter("checking history for duplicates...") seen = Set() for pid in revision_history(): if pid in seen: bailout("patch " + pid + " appears twice in history") seen.add(pid) M 644 inline bzrlib/commands.py data 23055 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] --revision REV Show changes since REV, rather than predecessor. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Diff selected files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = cmdfn.__doc__ if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'commit': [], 'diff': [], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'relpath': ['filename'], 'remove': ['file+'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :114 committer 1111982893 +1000 data 32 - reactivate basic check command from :113 M 644 inline bzrlib/check.py data 3759 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ###################################################################### # consistency checks import bzrlib def _ass(a, b): if a != b: bzrlib.errors.bailout("check failed: %r != %r" % (a, b)) def check(branch, verbose=True): print 'checking tree %r' % branch.base print 'checking entire revision history is present' last_ptr = None for rid in branch.revision_history(): print ' revision {%s}' % rid rev = branch.get_revision(rid) _ass(rev.revision_id, rid) _ass(rev.precursor, last_ptr) last_ptr = rid #mutter("checking tree") #check_patches_exist() #check_patch_chaining() #check_patch_uniqueness() #check_inventory() print ("tree looks OK") ## TODO: Check that previous-inventory and previous-manifest ## are the same as those stored in the previous changeset. ## TODO: Check all patches present in patch directory are ## mentioned in patch history; having an orphaned patch only gives ## a warning. ## TODO: Check cached data is consistent with data reconstructed ## from scratch. ## TODO: Check no control files are versioned. ## TODO: Check that the before-hash of each file in a later ## revision matches the after-hash in the previous revision to ## touch it. def check_inventory(): mutter("checking inventory file and ids...") seen_ids = Set() seen_names = Set() for l in controlfile('inventory').readlines(): parts = l.split() if len(parts) != 2: bailout("malformed inventory line: " + `l`) file_id, name = parts if file_id in seen_ids: bailout("duplicated file id " + file_id) seen_ids.add(file_id) if name in seen_names: bailout("duplicated file name in inventory: " + quotefn(name)) seen_names.add(name) if is_control_file(name): raise BzrError("control file %s present in inventory" % quotefn(name)) def check_patches_exist(): """Check constraint of current version: all patches exist""" mutter("checking all patches are present...") for pid in revision_history(): read_patch_header(pid) def check_patch_chaining(): """Check ancestry of patches and history file is consistent""" mutter("checking patch chaining...") prev = None for pid in revision_history(): log_prev = read_patch_header(pid).precursor if log_prev != prev: bailout("inconsistent precursor links on " + pid) prev = pid def check_patch_uniqueness(): """Make sure no patch is listed twice in the history. This should be implied by having correct ancestry but I'll check it anyhow.""" mutter("checking history for duplicates...") seen = Set() for pid in revision_history(): if pid in seen: bailout("patch " + pid + " appears twice in history") seen.add(pid) commit refs/heads/master mark :115 committer 1111983163 +1000 data 4 todo from :114 M 644 inline bzrlib/commands.py data 23043 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] --revision REV Show changes since REV, rather than predecessor. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Diff selected files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = cmdfn.__doc__ if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'commit': [], 'diff': [], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'relpath': ['filename'], 'remove': ['file+'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') M 644 inline bzrlib/trace.py data 3549 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os, time, socket, stat import bzrlib ###################################################################### # messages and logging ## TODO: If --verbose is given then write to both stderr and ## _tracefile; perhaps replace _tracefile with a tee thing. global _tracefile, _starttime # used to have % (os.environ['USER'], time.time(), os.getpid()), 'w') # If false, notes also go to stdout; should replace this with --silent # at some point. silent = False # TODO: Somehow tie this to the --verbose option? verbose = False def warning(msg): b = 'bzr: warning: ' + msg + '\n' sys.stderr.write(b) _tracefile.write(b) #_tracefile.flush() def mutter(msg): _tracefile.write(msg) _tracefile.write('\n') # _tracefile.flush() if verbose: sys.stderr.write('- ' + msg + '\n') def note(msg): b = '* ' + str(msg) + '\n' if not silent: sys.stderr.write(b) _tracefile.write(b) # _tracefile.flush() def log_error(msg): sys.stderr.write(msg) _tracefile.write(msg) # _tracefile.flush() def create_tracefile(argv): # TODO: Also show contents of /etc/lsb-release, if it can be parsed. # Perhaps that should eventually go into the platform library? # TODO: If the file doesn't exist, add a note describing it. # Messages are always written to here, so that we have some # information if something goes wrong. In a future version this # file will be removed on successful completion. global _starttime, _tracefile _starttime = os.times()[4] _tracefile = file('.bzr.log', 'at') t = _tracefile if os.fstat(t.fileno())[stat.ST_SIZE] == 0: t.write("\nthis is a debug log for diagnosing/reporting problems in bzr\n") t.write("you can delete or truncate this file, or include sections in\n") t.write("bug reports to bazaar-ng@lists.canonical.com\n\n") # TODO: If we failed to create the file, perhaps give a warning # but don't abort; send things to /dev/null instead? t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % bzrlib.osutils.format_date(time.time())) t.write(' version: %s\n' % bzrlib.__version__) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.getfqdn())) t.write(' arguments: %r\n' % argv) # This causes a vfork; I don't care enough about it. t.write(' platform: %s\n' % sys.platform) t.write(' python: %s\n' % (sys.version_info,)) import atexit atexit.register(_close_trace) def _close_trace(): times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum, %.3f elapsed" % (times[:4] + ((times[4] - _starttime),))) commit refs/heads/master mark :116 committer 1111983370 +1000 data 37 fix up debug output for check command from :115 M 644 inline bzrlib/check.py data 3793 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ###################################################################### # consistency checks import bzrlib from trace import mutter def _ass(a, b): if a != b: bzrlib.errors.bailout("check failed: %r != %r" % (a, b)) def check(branch): mutter('checking tree %r' % branch.base) mutter('checking entire revision history is present') last_ptr = None for rid in branch.revision_history(): mutter(' revision {%s}' % rid) rev = branch.get_revision(rid) _ass(rev.revision_id, rid) _ass(rev.precursor, last_ptr) last_ptr = rid #mutter("checking tree") #check_patches_exist() #check_patch_chaining() #check_patch_uniqueness() #check_inventory() mutter('branch %s is OK' % branch.base) ## TODO: Check that previous-inventory and previous-manifest ## are the same as those stored in the previous changeset. ## TODO: Check all patches present in patch directory are ## mentioned in patch history; having an orphaned patch only gives ## a warning. ## TODO: Check cached data is consistent with data reconstructed ## from scratch. ## TODO: Check no control files are versioned. ## TODO: Check that the before-hash of each file in a later ## revision matches the after-hash in the previous revision to ## touch it. def check_inventory(): mutter("checking inventory file and ids...") seen_ids = Set() seen_names = Set() for l in controlfile('inventory').readlines(): parts = l.split() if len(parts) != 2: bailout("malformed inventory line: " + `l`) file_id, name = parts if file_id in seen_ids: bailout("duplicated file id " + file_id) seen_ids.add(file_id) if name in seen_names: bailout("duplicated file name in inventory: " + quotefn(name)) seen_names.add(name) if is_control_file(name): raise BzrError("control file %s present in inventory" % quotefn(name)) def check_patches_exist(): """Check constraint of current version: all patches exist""" mutter("checking all patches are present...") for pid in revision_history(): read_patch_header(pid) def check_patch_chaining(): """Check ancestry of patches and history file is consistent""" mutter("checking patch chaining...") prev = None for pid in revision_history(): log_prev = read_patch_header(pid).precursor if log_prev != prev: bailout("inconsistent precursor links on " + pid) prev = pid def check_patch_uniqueness(): """Make sure no patch is listed twice in the history. This should be implied by having correct ancestry but I'll check it anyhow.""" mutter("checking history for duplicates...") seen = Set() for pid in revision_history(): if pid in seen: bailout("patch " + pid + " appears twice in history") seen.add(pid) commit refs/heads/master mark :117 committer 1112004279 +1000 data 34 better messages from check command from :116 M 644 inline bzrlib/check.py data 3846 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ###################################################################### # consistency checks import bzrlib from trace import mutter from errors import bailout def check(branch): mutter('checking tree %r' % branch.base) mutter('checking revision history') last_ptr = None for rid in branch.revision_history(): mutter(' revision {%s}' % rid) rev = branch.get_revision(rid) if rev.revision_id != rid: bailout('wrong internal revision id in revision {%s}' % rid) if rev.precursor != last_ptr: bailout('mismatched precursor in revision {%s}' % rid) last_ptr = rid #mutter("checking tree") #check_patches_exist() #check_patch_chaining() #check_patch_uniqueness() #check_inventory() mutter('branch %s is OK' % branch.base) ## TODO: Check that previous-inventory and previous-manifest ## are the same as those stored in the previous changeset. ## TODO: Check all patches present in patch directory are ## mentioned in patch history; having an orphaned patch only gives ## a warning. ## TODO: Check cached data is consistent with data reconstructed ## from scratch. ## TODO: Check no control files are versioned. ## TODO: Check that the before-hash of each file in a later ## revision matches the after-hash in the previous revision to ## touch it. def check_inventory(): mutter("checking inventory file and ids...") seen_ids = Set() seen_names = Set() for l in controlfile('inventory').readlines(): parts = l.split() if len(parts) != 2: bailout("malformed inventory line: " + `l`) file_id, name = parts if file_id in seen_ids: bailout("duplicated file id " + file_id) seen_ids.add(file_id) if name in seen_names: bailout("duplicated file name in inventory: " + quotefn(name)) seen_names.add(name) if is_control_file(name): raise BzrError("control file %s present in inventory" % quotefn(name)) def check_patches_exist(): """Check constraint of current version: all patches exist""" mutter("checking all patches are present...") for pid in revision_history(): read_patch_header(pid) def check_patch_chaining(): """Check ancestry of patches and history file is consistent""" mutter("checking patch chaining...") prev = None for pid in revision_history(): log_prev = read_patch_header(pid).precursor if log_prev != prev: bailout("inconsistent precursor links on " + pid) prev = pid def check_patch_uniqueness(): """Make sure no patch is listed twice in the history. This should be implied by having correct ancestry but I'll check it anyhow.""" mutter("checking history for duplicates...") seen = Set() for pid in revision_history(): if pid in seen: bailout("patch " + pid + " appears twice in history") seen.add(pid) commit refs/heads/master mark :118 committer 1112004623 +1000 data 11 Update news from :117 M 644 inline NEWS data 1115 bzr-0.0.2 NOT RELEASED YET CHANGES: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. ENHANCEMENTS: * Ignore list is memoized after first request. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. commit refs/heads/master mark :119 committer 1112009412 +1000 data 45 check revisions are not duplicated in history from :118 M 644 inline bzrlib/check.py data 2931 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ###################################################################### # consistency checks from sets import Set import bzrlib from trace import mutter from errors import bailout def check(branch): mutter('checking tree %r' % branch.base) mutter('checking revision history') last_ptr = None checked_revs = Set() for rid in branch.revision_history(): mutter(' revision {%s}' % rid) rev = branch.get_revision(rid) if rev.revision_id != rid: bailout('wrong internal revision id in revision {%s}' % rid) if rev.precursor != last_ptr: bailout('mismatched precursor in revision {%s}' % rid) last_ptr = rid if rid in checked_revs: bailout('repeated revision {%s}' % rid) checked_revs.add(rid) #check_inventory() mutter('branch %s is OK' % branch.base) ## TODO: Check that previous-inventory and previous-manifest ## are the same as those stored in the previous changeset. ## TODO: Check all patches present in patch directory are ## mentioned in patch history; having an orphaned patch only gives ## a warning. ## TODO: Check cached data is consistent with data reconstructed ## from scratch. ## TODO: Check no control files are versioned. ## TODO: Check that the before-hash of each file in a later ## revision matches the after-hash in the previous revision to ## touch it. def check_inventory(): mutter("checking inventory file and ids...") seen_ids = Set() seen_names = Set() for l in controlfile('inventory').readlines(): parts = l.split() if len(parts) != 2: bailout("malformed inventory line: " + `l`) file_id, name = parts if file_id in seen_ids: bailout("duplicated file id " + file_id) seen_ids.add(file_id) if name in seen_names: bailout("duplicated file name in inventory: " + quotefn(name)) seen_names.add(name) if is_control_file(name): raise BzrError("control file %s present in inventory" % quotefn(name)) commit refs/heads/master mark :120 committer 1112016038 +1000 data 20 more check functions from :119 M 644 inline bzrlib/check.py data 2205 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ###################################################################### # consistency checks from sets import Set import bzrlib from trace import mutter from errors import bailout def check(branch): mutter('checking tree %r' % branch.base) mutter('checking revision history') last_ptr = None checked_revs = Set() for rid in branch.revision_history(): mutter(' revision {%s}' % rid) rev = branch.get_revision(rid) if rev.revision_id != rid: bailout('wrong internal revision id in revision {%s}' % rid) if rev.precursor != last_ptr: bailout('mismatched precursor in revision {%s}' % rid) last_ptr = rid if rid in checked_revs: bailout('repeated revision {%s}' % rid) checked_revs.add(rid) ## TODO: Check all the required fields are present on the revision. inv = branch.get_inventory(rev.inventory_id) check_inventory(branch, inv) mutter('branch %s is OK' % branch.base) def check_inventory(branch, inv): seen_ids = Set() seen_names = Set() for path, ie in inv.iter_entries(): if path in seen_names: bailout('duplicated path %r in inventory' % path) seen_names.add(path) if ie.kind == 'file': if not ie.text_id in branch.text_store: bailout('text {%s} not in text_store' % ie.text_id) M 644 inline bzrlib/inventory.py data 15608 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Inventories map files to their name in a revision.""" # TODO: Maybe store inventory_id in the file? Not really needed. __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os.path, types from sets import Set try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from xml import XMLMixin from errors import bailout import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') >>> i.add(InventoryEntry('123', 'src', kind='directory')) >>> i.add(InventoryEntry('2323', 'hello.c', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id=None)) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', parent_id='123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', parent_id='123')) >>> i.add(InventoryEntry('2325', 'wibble', parent_id='123', kind='directory')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', parent_id='2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' :todo: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ def __init__(self, file_id, name, kind='file', text_id=None, parent_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c') >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c') Traceback (most recent call last): BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", []) """ if len(splitpath(name)) != 1: bailout('InventoryEntry name is not a simple filename: %r' % name) self.file_id = file_id self.name = name assert kind in ['file', 'directory'] self.kind = kind self.text_id = text_id self.parent_id = parent_id self.text_sha1 = None self.text_size = None def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.text_id, self.parent_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size is not None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1', 'parent_id']: v = getattr(self, f) if v is not None: e.set(f, v) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind')) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') self.parent_id = elt.get('parent_id') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __cmp__(self, other): if self is other: return 0 if not isinstance(other, InventoryEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.name, other.name) \ or cmp(self.text_sha1, other.text_sha1) \ or cmp(self.text_size, other.text_size) \ or cmp(self.text_id, other.text_id) \ or cmp(self.parent_id, other.parent_id) \ or cmp(self.kind, other.kind) class Inventory(XMLMixin): """Inventory of versioned files in a tree. An Inventory acts like a set of InventoryEntry items. You can also look files up by their file_id or name. May be read from and written to a metadata file in a tree. To manipulate the inventory (for example to add a file), it is read in, modified, and then written back out. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c')) >>> inv['123-123'].name 'hello.c' >>> for file_id in inv: print file_id ... 123-123 May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ ## TODO: Clear up handling of files in subdirectories; we probably ## do want to be able to just look them up by name but this ## probably means gradually walking down the path, looking up as we go. ## TODO: Make sure only canonical filenames are stored. ## TODO: Do something sensible about the possible collisions on ## case-losing filesystems. Perhaps we should just always forbid ## such collisions. ## _tree should probably just be stored as ## InventoryEntry._children on each directory. def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. """ self._byid = dict() # _tree is indexed by parent_id; at each level a map from name # to ie. The None entry is the root. self._tree = {None: {}} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, parent_id=None): """Return (path, entry) pairs, in order by name.""" kids = self._tree[parent_id].items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(parent_id=ie.file_id): yield joinpath([name, cn]), cie def directories(self, include_root=True): """Return (path, entry) pairs for all directories. """ if include_root: yield '', None for path, entry in self.iter_entries(): if entry.kind == 'directory': yield path, entry def children(self, parent_id): """Return entries that are direct children of parent_id.""" return self._tree[parent_id] # TODO: return all paths and entries def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c')) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c')) >>> inv['123123'].name 'hello.c' """ return self._byid[file_id] def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self: bailout("inventory already contains entry with id {%s}" % entry.file_id) if entry.parent_id != None: if entry.parent_id not in self: bailout("parent_id %s of new entry not found in inventory" % entry.parent_id) if self._tree[entry.parent_id].has_key(entry.name): bailout("%s is already versioned" % appendpath(self.id2path(entry.parent_id), entry.name)) self._byid[entry.file_id] = entry self._tree[entry.parent_id][entry.name] = entry if entry.kind == 'directory': self._tree[entry.file_id] = {} def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: bailout("cannot re-add root of inventory") if file_id is None: file_id = bzrlib.branch.gen_file_id(relpath) parent_id = self.path2id(parts[:-1]) ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c')) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self._tree[ie.parent_id][ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in self._tree[file_id].values(): del self[cie.file_id] del self._tree[file_id] del self._byid[file_id] del self._tree[ie.parent_id][ie.name] def id_set(self): return Set(self._byid) def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c')) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __cmp__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo')) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo')) >>> i1 == i2 True """ if self is other: return 0 if not isinstance(other, Inventory): return NotImplemented if self.id_set() ^ other.id_set(): return 1 for file_id in self._byid: c = cmp(self[file_id], other[file_id]) if c: return c return 0 def id2path(self, file_id): """Return as a list the path to file_id.""" p = [] while file_id != None: ie = self[file_id] p = [ie.name] + p file_id = ie.parent_id return joinpath(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. """ if isinstance(name, types.StringTypes): name = splitpath(name) parent_id = None for f in name: try: cie = self._tree[parent_id][f] assert cie.name == f parent_id = cie.file_id except KeyError: # or raise an error? return None return parent_id def get_child(self, parent_id, child_name): return self._tree[parent_id].get(child_name) def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): assert isinstance(file_id, str) return self._byid.has_key(file_id) if __name__ == '__main__': import doctest, inventory doctest.testmod(inventory) commit refs/heads/master mark :121 committer 1112054608 +1000 data 88 - progress indicator while checking - check file ids are not duplicated in the inventory from :120 M 644 inline bzrlib/check.py data 2805 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ###################################################################### # consistency checks import sys from sets import Set import bzrlib from trace import mutter from errors import bailout def check(branch, progress=True): out = sys.stdout if progress: def p(m): mutter('checking ' + m) out.write('\rchecking: %-50.50s' % m) out.flush() else: def p(m): mutter('checking ' + m) p('history of %r' % branch.base) last_ptr = None checked_revs = Set() history = branch.revision_history() revno = 0 revcount = len(history) for rid in history: revno += 1 p('revision %d/%d' % (revno, revcount)) mutter(' revision {%s}' % rid) rev = branch.get_revision(rid) if rev.revision_id != rid: bailout('wrong internal revision id in revision {%s}' % rid) if rev.precursor != last_ptr: bailout('mismatched precursor in revision {%s}' % rid) last_ptr = rid if rid in checked_revs: bailout('repeated revision {%s}' % rid) checked_revs.add(rid) ## TODO: Check all the required fields are present on the revision. inv = branch.get_inventory(rev.inventory_id) check_inventory(branch, inv, rid) p('done') if progress: print def check_inventory(branch, inv, revid): seen_ids = Set() seen_names = Set() for path, ie in inv.iter_entries(): if path in seen_names: bailout('duplicated path %r in inventory for revision {%s}' % (path, revid)) seen_names.add(path) if ie.file_id in seen_ids: bailout('duplicated file_id {%s} in inventory for revision {%s}' % (ie.file_id, revid)) seen_ids.add(ie.file_id) if ie.kind == 'file': if not ie.text_id in branch.text_store: bailout('text {%s} not in text_store' % ie.text_id) commit refs/heads/master mark :122 committer 1112054843 +1000 data 51 check: make sure parent of file entries are present from :121 M 644 inline bzrlib/check.py data 3102 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ###################################################################### # consistency checks import sys from sets import Set import bzrlib from trace import mutter from errors import bailout def check(branch, progress=True): out = sys.stdout if progress: def p(m): mutter('checking ' + m) out.write('\rchecking: %-50.50s' % m) out.flush() else: def p(m): mutter('checking ' + m) p('history of %r' % branch.base) last_ptr = None checked_revs = Set() history = branch.revision_history() revno = 0 revcount = len(history) for rid in history: revno += 1 p('revision %d/%d' % (revno, revcount)) mutter(' revision {%s}' % rid) rev = branch.get_revision(rid) if rev.revision_id != rid: bailout('wrong internal revision id in revision {%s}' % rid) if rev.precursor != last_ptr: bailout('mismatched precursor in revision {%s}' % rid) last_ptr = rid if rid in checked_revs: bailout('repeated revision {%s}' % rid) checked_revs.add(rid) ## TODO: Check all the required fields are present on the revision. inv = branch.get_inventory(rev.inventory_id) check_inventory(branch, inv, rid) p('done') if progress: print def check_inventory(branch, inv, revid): seen_ids = Set() seen_names = Set() for file_id in inv: if file_id in seen_ids: bailout('duplicated file_id {%s} in inventory for revision {%s}' % (file_id, revid)) seen_ids.add(file_id) for file_id in inv: ie = inv[file_id] if ie.parent_id != None: if ie.parent_id not in seen_ids: bailout('missing parent {%s} in inventory for revision {%s}' % (ie.parent_id, revid)) if ie.kind == 'file': if not ie.text_id in branch.text_store: bailout('text {%s} not in text_store' % ie.text_id) for path, ie in inv.iter_entries(): if path in seen_names: bailout('duplicated path %r in inventory for revision {%s}' % (path, revid)) seen_names.add(path) commit refs/heads/master mark :123 committer 1112055032 +1000 data 41 check: directories must not have any text from :122 M 644 inline bzrlib/check.py data 3333 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ###################################################################### # consistency checks import sys from sets import Set import bzrlib from trace import mutter from errors import bailout def check(branch, progress=True): out = sys.stdout if progress: def p(m): mutter('checking ' + m) out.write('\rchecking: %-50.50s' % m) out.flush() else: def p(m): mutter('checking ' + m) p('history of %r' % branch.base) last_ptr = None checked_revs = Set() history = branch.revision_history() revno = 0 revcount = len(history) for rid in history: revno += 1 p('revision %d/%d' % (revno, revcount)) mutter(' revision {%s}' % rid) rev = branch.get_revision(rid) if rev.revision_id != rid: bailout('wrong internal revision id in revision {%s}' % rid) if rev.precursor != last_ptr: bailout('mismatched precursor in revision {%s}' % rid) last_ptr = rid if rid in checked_revs: bailout('repeated revision {%s}' % rid) checked_revs.add(rid) ## TODO: Check all the required fields are present on the revision. inv = branch.get_inventory(rev.inventory_id) check_inventory(branch, inv, rid) p('done') if progress: print def check_inventory(branch, inv, revid): seen_ids = Set() seen_names = Set() for file_id in inv: if file_id in seen_ids: bailout('duplicated file_id {%s} in inventory for revision {%s}' % (file_id, revid)) seen_ids.add(file_id) for file_id in inv: ie = inv[file_id] if ie.parent_id != None: if ie.parent_id not in seen_ids: bailout('missing parent {%s} in inventory for revision {%s}' % (ie.parent_id, revid)) if ie.kind == 'file': if not ie.text_id in branch.text_store: bailout('text {%s} not in text_store' % ie.text_id) elif ie.kind == 'directory': if ie.text_sha1 != None or ie.text_size != None or ie.text_id != None: bailout('directory {%s} has text in revision {%s}' % (file_id, revid)) for path, ie in inv.iter_entries(): if path in seen_names: bailout('duplicated path %r in inventory for revision {%s}' % (path, revid)) seen_names.add(path) commit refs/heads/master mark :124 committer 1112055800 +1000 data 79 - check file text for past revisions is correct - new fingerprint_file function from :123 M 644 inline bzrlib/check.py data 3649 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ###################################################################### # consistency checks import sys from sets import Set import bzrlib from trace import mutter from errors import bailout import osutils def check(branch, progress=True): out = sys.stdout if progress: def p(m): mutter('checking ' + m) out.write('\rchecking: %-50.50s' % m) out.flush() else: def p(m): mutter('checking ' + m) p('history of %r' % branch.base) last_ptr = None checked_revs = Set() history = branch.revision_history() revno = 0 revcount = len(history) for rid in history: revno += 1 p('revision %d/%d' % (revno, revcount)) mutter(' revision {%s}' % rid) rev = branch.get_revision(rid) if rev.revision_id != rid: bailout('wrong internal revision id in revision {%s}' % rid) if rev.precursor != last_ptr: bailout('mismatched precursor in revision {%s}' % rid) last_ptr = rid if rid in checked_revs: bailout('repeated revision {%s}' % rid) checked_revs.add(rid) ## TODO: Check all the required fields are present on the revision. inv = branch.get_inventory(rev.inventory_id) check_inventory(branch, inv, rid) p('done') if progress: print def check_inventory(branch, inv, revid): seen_ids = Set() seen_names = Set() for file_id in inv: if file_id in seen_ids: bailout('duplicated file_id {%s} in inventory for revision {%s}' % (file_id, revid)) seen_ids.add(file_id) for file_id in inv: ie = inv[file_id] if ie.parent_id != None: if ie.parent_id not in seen_ids: bailout('missing parent {%s} in inventory for revision {%s}' % (ie.parent_id, revid)) if ie.kind == 'file': if not ie.text_id in branch.text_store: bailout('text {%s} not in text_store' % ie.text_id) tf = branch.text_store[ie.text_id] fp = osutils.fingerprint_file(tf) if ie.text_size != fp['size']: bailout('text {%s} wrong size' % ie.text_id) if ie.text_sha1 != fp['sha1']: bailout('text {%s} wrong sha1' % ie.text_id) elif ie.kind == 'directory': if ie.text_sha1 != None or ie.text_size != None or ie.text_id != None: bailout('directory {%s} has text in revision {%s}' % (file_id, revid)) for path, ie in inv.iter_entries(): if path in seen_names: bailout('duplicated path %r in inventory for revision {%s}' % (path, revid)) seen_names.add(path) M 644 inline bzrlib/osutils.py data 7513 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, types, re, time, types from stat import S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE from errors import bailout def make_readonly(filename): """Make a filename read-only.""" # TODO: probably needs to be fixed for windows mod = os.stat(filename).st_mode mod = mod & 0777555 os.chmod(filename, mod) def make_writable(filename): mod = os.stat(filename).st_mode mod = mod | 0200 os.chmod(filename, mod) _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])') def quotefn(f): """Return shell-quoted filename""" ## We could be a bit more terse by using double-quotes etc f = _QUOTE_RE.sub(r'\\\1', f) if f[0] == '~': f[0:1] = r'\~' return f def file_kind(f): mode = os.lstat(f)[ST_MODE] if S_ISREG(mode): return 'file' elif S_ISDIR(mode): return 'directory' elif S_ISLNK(mode): return 'symlink' else: bailout("can't handle file kind with mode %o of %r" % (mode, f)) def isdir(f): """True if f is an accessible directory.""" try: return S_ISDIR(os.lstat(f)[ST_MODE]) except OSError: return False def isfile(f): """True if f is a regular file.""" try: return S_ISREG(os.lstat(f)[ST_MODE]) except OSError: return False def pumpfile(fromfile, tofile): """Copy contents of one file to another.""" tofile.write(fromfile.read()) def uuid(): """Return a new UUID""" ## XXX: Could alternatively read /proc/sys/kernel/random/uuid on ## Linux, but we need something portable for other systems; ## preferably an implementation in Python. try: return chomp(file('/proc/sys/kernel/random/uuid').readline()) except IOError: return chomp(os.popen('uuidgen').readline()) def chomp(s): if s and (s[-1] == '\n'): return s[:-1] else: return s def sha_file(f): import sha ## TODO: Maybe read in chunks to handle big files if hasattr(f, 'tell'): assert f.tell() == 0 s = sha.new() s.update(f.read()) return s.hexdigest() def sha_string(f): import sha s = sha.new() s.update(f) return s.hexdigest() def fingerprint_file(f): import sha s = sha.new() size = 0 BUFSIZE = 64<<10 while True: b = f.read(BUFSIZE) if b == '': break s.update(b) size += len(b) return {'size': size, 'sha1': s.hexdigest()} def username(): """Return email-style username. Something similar to 'Martin Pool ' :todo: Check it's reasonably well-formed. :todo: Allow taking it from a dotfile to help people on windows who can't easily set variables. :todo: Cope without pwd module, which is only on unix. """ e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: return e import socket try: import pwd uid = os.getuid() w = pwd.getpwuid(uid) gecos = w.pw_gecos comma = gecos.find(',') if comma == -1: realname = gecos else: realname = gecos[:comma] return '%s <%s@%s>' % (realname, w.pw_name, socket.getfqdn()) except ImportError: pass import getpass, socket return '<%s@%s>' % (getpass.getuser(), socket.getfqdn()) def user_email(): """Return just the email component of a username.""" e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: import re m = re.search(r'[\w+.-]+@[\w+.-]+', e) if not m: bailout('%r is not a reasonable email address' % e) return m.group(0) import getpass, socket return '%s@%s' % (getpass.getuser(), socket.getfqdn()) def compare_files(a, b): """Returns true if equal in contents""" # TODO: don't read the whole thing in one go. BUFSIZE = 4096 while True: ai = a.read(BUFSIZE) bi = b.read(BUFSIZE) if ai != bi: return False if ai == '': return True def local_time_offset(t=None): """Return offset of local zone from GMT, either at present or at time t.""" # python2.3 localtime() can't take None if t is None: t = time.time() if time.localtime(t).tm_isdst and time.daylight: return -time.altzone else: return -time.timezone def format_date(t, offset=0, timezone='original'): ## TODO: Perhaps a global option to use either universal or local time? ## Or perhaps just let people set $TZ? import time assert isinstance(t, float) if timezone == 'utc': tt = time.gmtime(t) offset = 0 elif timezone == 'original': if offset == None: offset = 0 tt = time.gmtime(t + offset) elif timezone == 'local': tt = time.localtime(t) offset = local_time_offset(t) else: bailout("unsupported timezone format %r", ['options are "utc", "original", "local"']) return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt) + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)) def compact_date(when): return time.strftime('%Y%m%d%H%M%S', time.gmtime(when)) def filesize(f): """Return size of given open file.""" return os.fstat(f.fileno())[ST_SIZE] if hasattr(os, 'urandom'): # python 2.4 and later rand_bytes = os.urandom else: # FIXME: No good on non-Linux _rand_file = file('/dev/urandom', 'rb') rand_bytes = _rand_file.read ## TODO: We could later have path objects that remember their list ## decomposition (might be too tricksy though.) def splitpath(p): """Turn string into list of parts. >>> splitpath('a') ['a'] >>> splitpath('a/b') ['a', 'b'] >>> splitpath('a/./b') ['a', 'b'] >>> splitpath('a/.b') ['a', '.b'] >>> splitpath('a/../b') Traceback (most recent call last): ... BzrError: ("sorry, '..' not allowed in path", []) """ assert isinstance(p, types.StringTypes) ps = [f for f in p.split('/') if f != '.'] for f in ps: if f == '..': bailout("sorry, %r not allowed in path" % f) return ps def joinpath(p): assert isinstance(p, list) for f in p: if (f == '..') or (f is None) or (f == ''): bailout("sorry, %r not allowed in path" % f) return '/'.join(p) def appendpath(p1, p2): if p1 == '': return p2 else: return p1 + '/' + p2 def extern_command(cmd, ignore_errors = False): mutter('external command: %s' % `cmd`) if os.system(cmd): if not ignore_errors: bailout('command failed') commit refs/heads/master mark :125 committer 1112056372 +1000 data 102 - check progress indicator for file texts - avoid re-reading file texts that have already been checked from :124 M 644 inline bzrlib/check.py data 4136 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ###################################################################### # consistency checks import sys from sets import Set import bzrlib from trace import mutter from errors import bailout import osutils def check(branch, progress=True): out = sys.stdout if progress: def p(m): mutter('checking ' + m) out.write('\rchecking: %-50.50s' % m) out.flush() else: def p(m): mutter('checking ' + m) p('history of %r' % branch.base) last_ptr = None checked_revs = Set() history = branch.revision_history() revno = 0 revcount = len(history) checked_texts = {} for rid in history: revno += 1 p('revision %d/%d' % (revno, revcount)) mutter(' revision {%s}' % rid) rev = branch.get_revision(rid) if rev.revision_id != rid: bailout('wrong internal revision id in revision {%s}' % rid) if rev.precursor != last_ptr: bailout('mismatched precursor in revision {%s}' % rid) last_ptr = rid if rid in checked_revs: bailout('repeated revision {%s}' % rid) checked_revs.add(rid) ## TODO: Check all the required fields are present on the revision. inv = branch.get_inventory(rev.inventory_id) seen_ids = Set() seen_names = Set() p('revision %d/%d file ids' % (revno, revcount)) for file_id in inv: if file_id in seen_ids: bailout('duplicated file_id {%s} in inventory for revision {%s}' % (file_id, revid)) seen_ids.add(file_id) i = 0 len_inv = len(inv) for file_id in inv: i += 1 if (i % 100) == 0: p('revision %d/%d file text %d/%d' % (revno, revcount, i, len_inv)) ie = inv[file_id] if ie.parent_id != None: if ie.parent_id not in seen_ids: bailout('missing parent {%s} in inventory for revision {%s}' % (ie.parent_id, revid)) if ie.kind == 'file': if ie.text_id in checked_texts: fp = checked_texts[ie.text_id] else: if not ie.text_id in branch.text_store: bailout('text {%s} not in text_store' % ie.text_id) tf = branch.text_store[ie.text_id] fp = osutils.fingerprint_file(tf) checked_texts[ie.text_id] = fp if ie.text_size != fp['size']: bailout('text {%s} wrong size' % ie.text_id) if ie.text_sha1 != fp['sha1']: bailout('text {%s} wrong sha1' % ie.text_id) elif ie.kind == 'directory': if ie.text_sha1 != None or ie.text_size != None or ie.text_id != None: bailout('directory {%s} has text in revision {%s}' % (file_id, revid)) p('revision %d/%d file paths' % (revno, revcount)) for path, ie in inv.iter_entries(): if path in seen_names: bailout('duplicated path %r in inventory for revision {%s}' % (path, revid)) seen_names.add(path) p('done') if progress: print commit refs/heads/master mark :126 committer 1112056500 +1000 data 42 Use just one big read to fingerprint files from :125 M 644 inline bzrlib/osutils.py data 7419 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, types, re, time, types from stat import S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE from errors import bailout def make_readonly(filename): """Make a filename read-only.""" # TODO: probably needs to be fixed for windows mod = os.stat(filename).st_mode mod = mod & 0777555 os.chmod(filename, mod) def make_writable(filename): mod = os.stat(filename).st_mode mod = mod | 0200 os.chmod(filename, mod) _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])') def quotefn(f): """Return shell-quoted filename""" ## We could be a bit more terse by using double-quotes etc f = _QUOTE_RE.sub(r'\\\1', f) if f[0] == '~': f[0:1] = r'\~' return f def file_kind(f): mode = os.lstat(f)[ST_MODE] if S_ISREG(mode): return 'file' elif S_ISDIR(mode): return 'directory' elif S_ISLNK(mode): return 'symlink' else: bailout("can't handle file kind with mode %o of %r" % (mode, f)) def isdir(f): """True if f is an accessible directory.""" try: return S_ISDIR(os.lstat(f)[ST_MODE]) except OSError: return False def isfile(f): """True if f is a regular file.""" try: return S_ISREG(os.lstat(f)[ST_MODE]) except OSError: return False def pumpfile(fromfile, tofile): """Copy contents of one file to another.""" tofile.write(fromfile.read()) def uuid(): """Return a new UUID""" ## XXX: Could alternatively read /proc/sys/kernel/random/uuid on ## Linux, but we need something portable for other systems; ## preferably an implementation in Python. try: return chomp(file('/proc/sys/kernel/random/uuid').readline()) except IOError: return chomp(os.popen('uuidgen').readline()) def chomp(s): if s and (s[-1] == '\n'): return s[:-1] else: return s def sha_file(f): import sha ## TODO: Maybe read in chunks to handle big files if hasattr(f, 'tell'): assert f.tell() == 0 s = sha.new() s.update(f.read()) return s.hexdigest() def sha_string(f): import sha s = sha.new() s.update(f) return s.hexdigest() def fingerprint_file(f): import sha s = sha.new() b = f.read() s.update(b) size = len(b) f.close() return {'size': size, 'sha1': s.hexdigest()} def username(): """Return email-style username. Something similar to 'Martin Pool ' :todo: Check it's reasonably well-formed. :todo: Allow taking it from a dotfile to help people on windows who can't easily set variables. :todo: Cope without pwd module, which is only on unix. """ e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: return e import socket try: import pwd uid = os.getuid() w = pwd.getpwuid(uid) gecos = w.pw_gecos comma = gecos.find(',') if comma == -1: realname = gecos else: realname = gecos[:comma] return '%s <%s@%s>' % (realname, w.pw_name, socket.getfqdn()) except ImportError: pass import getpass, socket return '<%s@%s>' % (getpass.getuser(), socket.getfqdn()) def user_email(): """Return just the email component of a username.""" e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: import re m = re.search(r'[\w+.-]+@[\w+.-]+', e) if not m: bailout('%r is not a reasonable email address' % e) return m.group(0) import getpass, socket return '%s@%s' % (getpass.getuser(), socket.getfqdn()) def compare_files(a, b): """Returns true if equal in contents""" # TODO: don't read the whole thing in one go. BUFSIZE = 4096 while True: ai = a.read(BUFSIZE) bi = b.read(BUFSIZE) if ai != bi: return False if ai == '': return True def local_time_offset(t=None): """Return offset of local zone from GMT, either at present or at time t.""" # python2.3 localtime() can't take None if t is None: t = time.time() if time.localtime(t).tm_isdst and time.daylight: return -time.altzone else: return -time.timezone def format_date(t, offset=0, timezone='original'): ## TODO: Perhaps a global option to use either universal or local time? ## Or perhaps just let people set $TZ? import time assert isinstance(t, float) if timezone == 'utc': tt = time.gmtime(t) offset = 0 elif timezone == 'original': if offset == None: offset = 0 tt = time.gmtime(t + offset) elif timezone == 'local': tt = time.localtime(t) offset = local_time_offset(t) else: bailout("unsupported timezone format %r", ['options are "utc", "original", "local"']) return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt) + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)) def compact_date(when): return time.strftime('%Y%m%d%H%M%S', time.gmtime(when)) def filesize(f): """Return size of given open file.""" return os.fstat(f.fileno())[ST_SIZE] if hasattr(os, 'urandom'): # python 2.4 and later rand_bytes = os.urandom else: # FIXME: No good on non-Linux _rand_file = file('/dev/urandom', 'rb') rand_bytes = _rand_file.read ## TODO: We could later have path objects that remember their list ## decomposition (might be too tricksy though.) def splitpath(p): """Turn string into list of parts. >>> splitpath('a') ['a'] >>> splitpath('a/b') ['a', 'b'] >>> splitpath('a/./b') ['a', 'b'] >>> splitpath('a/.b') ['a', '.b'] >>> splitpath('a/../b') Traceback (most recent call last): ... BzrError: ("sorry, '..' not allowed in path", []) """ assert isinstance(p, types.StringTypes) ps = [f for f in p.split('/') if f != '.'] for f in ps: if f == '..': bailout("sorry, %r not allowed in path" % f) return ps def joinpath(p): assert isinstance(p, list) for f in p: if (f == '..') or (f is None) or (f == ''): bailout("sorry, %r not allowed in path" % f) return '/'.join(p) def appendpath(p1, p2): if p1 == '': return p2 else: return p1 + '/' + p2 def extern_command(cmd, ignore_errors = False): mutter('external command: %s' % `cmd`) if os.system(cmd): if not ignore_errors: bailout('command failed') commit refs/heads/master mark :127 committer 1112057028 +1000 data 47 - store support for retrieving compressed files from :126 M 644 inline bzrlib/store.py data 5024 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Stores are the main data-storage mechanism for Bazaar-NG. A store is a simple write-once container indexed by a universally unique ID, which is typically the SHA-1 of the content.""" __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import os, tempfile, types, osutils, gzip, errno from stat import ST_SIZE from StringIO import StringIO from trace import mutter ###################################################################### # stores class StoreError(Exception): pass class ImmutableStore: """Store that holds files indexed by unique names. Files can be added, but not modified once they are in. Typically the hash is used as the name, or something else known to be unique, such as a UUID. >>> st = ImmutableScratchStore() >>> st.add(StringIO('hello'), 'aa') >>> 'aa' in st True >>> 'foo' in st False You are not allowed to add an id that is already present. Entries can be retrieved as files, which may then be read. >>> st.add(StringIO('goodbye'), '123123') >>> st['123123'].read() 'goodbye' :todo: Atomic add by writing to a temporary file and renaming. :todo: Perhaps automatically transform to/from XML in a method? Would just need to tell the constructor what class to use... :todo: Even within a simple disk store like this, we could gzip the files. But since many are less than one disk block, that might not help a lot. """ def __init__(self, basedir): """ImmutableStore constructor.""" self._basedir = basedir def _path(self, id): return os.path.join(self._basedir, id) def __repr__(self): return "%s(%r)" % (self.__class__.__name__, self._basedir) def add(self, f, fileid): """Add contents of a file into the store. :param f: An open file, or file-like object.""" # FIXME: Only works on smallish files # TODO: Can be optimized by copying at the same time as # computing the sum. mutter("add store entry %r" % (fileid)) if isinstance(f, types.StringTypes): content = f else: content = f.read() if fileid not in self: filename = self._path(fileid) f = file(filename, 'wb') f.write(content) ## f.flush() ## os.fsync(f.fileno()) f.close() osutils.make_readonly(filename) def __contains__(self, fileid): """""" return os.access(self._path(fileid), os.R_OK) def __iter__(self): return iter(os.listdir(self._basedir)) def __len__(self): return len(os.listdir(self._basedir)) def __getitem__(self, fileid): """Returns a file reading from a particular entry.""" p = self._path(fileid) try: return gzip.GzipFile(p + '.gz', 'rb') except IOError, e: if e.errno == errno.ENOENT: return file(p, 'rb') else: raise e def total_size(self): """Return (count, bytes) This is the (compressed) size stored on disk, not the size of the content.""" total = 0 count = 0 for fid in self: count += 1 total += os.stat(self._path(fid))[ST_SIZE] return count, total def delete_all(self): for fileid in self: self.delete(fileid) def delete(self, fileid): """Remove nominated store entry. Most stores will be add-only.""" filename = self._path(fileid) ## osutils.make_writable(filename) os.remove(filename) def destroy(self): """Remove store; only allowed if it is empty.""" os.rmdir(self._basedir) mutter("%r destroyed" % self) class ImmutableScratchStore(ImmutableStore): """Self-destructing test subclass of ImmutableStore. The Store only exists for the lifetime of the Python object. Obviously you should not put anything precious in it. """ def __init__(self): ImmutableStore.__init__(self, tempfile.mkdtemp()) def __del__(self): self.delete_all() self.destroy() M 644 inline notes/performance.txt data 7764 For a tree holding 2.4.18 (two copies), 2.4.19, 2.4.20 With gzip -9: mbp@hope% du .bzr 195110 .bzr/text-store 20 .bzr/revision-store 12355 .bzr/inventory-store 216325 .bzr mbp@hope% du -s . 523128 . Without gzip: This is actually a pretty bad example because of deleting and re-importing 2.4.18, but still not totally unreasonable. ---- linux-2.4.0: 116399 kB after addding everything: 119505kB bzr status 2.68s user 0.13s system 84% cpu 3.330 total bzr commit 'import 2.4.0' 4.41s user 2.15s system 11% cpu 59.490 total 242446 . 122068 .bzr ---- Performance (2005-03-01) To add all files from linux-2.4.18: about 70s, mostly inventory serialization/deserialization. To commit: - finished, 6.520u/3.870s cpu, 33.940u/10.730s cum - 134.040 elapsed Interesting that it spends so long on external processing! I wonder if this is for running uuidgen? Let's try generating things internally. Great, this cuts it to 17.15s user 0.61s system 83% cpu 21.365 total to add, with no external command time. The commit now seems to spend most of its time copying to disk. - finished, 6.550u/3.320s cpu, 35.050u/9.870s cum - 89.650 elapsed I wonder where the external time is now? We were also using uuids() for revisions. Let's remove everything and re-add. Detecting everything was removed takes - finished, 2.460u/0.110s cpu, 0.000u/0.000s cum - 3.430 elapsed which may be mostly XML deserialization? Just getting the previous revision takes about this long: bzr invoked at Tue 2005-03-01 15:53:05.183741 EST +1100 by mbp@sourcefrog.net on hope arguments: ['/home/mbp/bin/bzr', 'get-revision-inventory', 'mbp@sourcefrog.net-20050301044608-8513202ab179aff4-44e8cd52a41aa705'] platform: Linux-2.6.10-4-686-i686-with-debian-3.1 - finished, 3.910u/0.390s cpu, 0.000u/0.000s cum - 6.690 elapsed Now committing the revision which removes all files should be fast. - finished, 1.280u/0.030s cpu, 0.000u/0.000s cum - 1.320 elapsed Now re-add with new code that doesn't call uuidgen: - finished, 1.990u/0.030s cpu, 0.000u/0.000s cum - 2.040 elapsed 16.61s user 0.55s system 74% cpu 22.965 total Status:: - finished, 2.500u/0.110s cpu, 0.010u/0.000s cum - 3.350 elapsed And commit:: Now patch up to 2.4.19. There were some bugs in handling missing directories, but with that fixed we do much better:: bzr status 5.86s user 1.06s system 10% cpu 1:05.55 total This is slow because it's diffing every file; we should use mtimes etc to make this faster. The cpu time is reasonable. I see difflib is pure Python; it might be faster to shell out to GNU diff when we need it. Export is very fast:: - finished, 4.220u/1.480s cpu, 0.010u/0.000s cum - 10.810 elapsed bzr export 1 ../linux-2.4.18.export1 3.92s user 1.72s system 21% cpu 26.030 total Now to find and add the new changes:: - finished, 2.190u/0.030s cpu, 0.000u/0.000s cum - 2.300 elapsed :: bzr commit 'import 2.4.19' 9.36s user 1.91s system 23% cpu 47.127 total And the result is exactly right. Try exporting:: mbp@hope% bzr export 4 ../linux-2.4.19.export4 bzr export 4 ../linux-2.4.19.export4 4.21s user 1.70s system 18% cpu 32.304 total and the export is exactly the same as the tarball. Now we can optimize the diff a bit more by not comparing files that have the right SHA-1 from within the commit For comparison:: patch -p1 < ../kernel.pkg/patch-2.4.20 1.61s user 1.03s system 13% cpu 19.106 total Now status after applying the .20 patch. With full-text verification:: bzr status 7.07s user 1.32s system 13% cpu 1:04.29 total with that turned off:: bzr status 5.86s user 0.56s system 25% cpu 25.577 total After adding: bzr status 6.14s user 0.61s system 25% cpu 26.583 total Should add some kind of profile counter for quick compares vs slow compares. bzr commit 'import 2.4.20' 7.57s user 1.36s system 20% cpu 43.568 total export: finished, 3.940u/1.820s cpu, 0.000u/0.000s cum, 50.990 elapsed also exports correctly now .21 bzr commit 'import 2.4.1' 5.59s user 0.51s system 60% cpu 10.122 total 265520 . 137704 .bzr import 2.4.2 317758 . 183463 .bzr with everything through to 2.4.29 imported, the .bzr directory is 1132MB, compared to 185MB for one tree. The .bzr.log is 100MB!. So the storage is 6.1 times larger, although we're holding 30 versions. It's pretty large but I think not ridiculous. By contrast the tarball for 2.4.0 is 104MB, and the tarball plus uncompressed patches are 315MB. Uncompressed, the text store is 1041MB. So it is only three times worse than patches, and could be compressed at presumably roughly equal efficiency. It is large, but also a very simple design and perhaps adequate for the moment. The text store with each file individually gziped is 264MB, which is also a very simple format and makes it less than twice the size of the source tree. This is actually rather pessimistic because I think there are some orphaned texts in there. Measured by du, the compressed full-text store is 363MB; also probably tolerable. The real fix is perhaps to use some kind of weave, not so much for storage efficiency as for fast annotation and therefore possible annotation-based merge. ----- 2005-03-25 Now we have recursive add, add is much faster. Adding all of the linux 2.4.19 kernel tree takes only finished, 5.460u/0.610s cpu, 0.010u/0.000s cum, 6.710 elapsed However, the store code currently flushes to disk after every write, which is probably excessive. So a commit takes finished, 8.740u/3.950s cpu, 0.010u/0.000s cum, 156.420 elapsed Status is now also quite fast, depsite that it still has to read all the working copies: mbp@hope% bzr status ~/work/linux-2.4.19 bzr status 5.51s user 0.79s system 99% cpu 6.337 total strace shows much of this is in write(2), probably because of logging. With more buffering on that file, removing all the explicit flushes, that is reduced to mbp@hope% time bzr status bzr status 5.23s user 0.42s system 97% cpu 5.780 total which is mostly opening, stating and reading files, as it should be. Still a few too many stat calls. Now fixed up handling of root directory. Without flushing everything to disk as it goes into the store: mbp@hope% bzr commit -m 'import linux 2.4.19' bzr commit -m 'import linux 2.4.19' 8.15s user 2.09s system 53% cpu 19.295 total mbp@hope% time bzr diff bzr diff 5.80s user 0.52s system 69% cpu 9.128 total mbp@hope% time bzr status bzr status 5.64s user 0.43s system 68% cpu 8.848 total patch -p1 < ../linux.pkg/patch-2.4.20 1.67s user 0.96s system 90% cpu 2.905 total The diff changes 3462 files according to diffstat. branch format: Bazaar-NG branch, format 0.0.4 in the working tree: 8674 unchanged 2463 modified 818 added 229 removed 0 renamed 0 unknown 4 ignored 614 versioned subdirectories That is, 3510 entries have changed, but there are 48 changed directories so the count is exactly right! bzr commit -v -m 'import 2.4.20' 8.23s user 1.09s system 48% cpu 19.411 total Kind of strange that this takes as much time as committing the whole thing; I suppose it has to read every file. This shows many files as being renamed; I don't know why that would be. Patch to 2.4.21: 2969 files changed, 366643 insertions(+), 147759 deletions(-) After auto-add: 2969 files changed, 372168 insertions(+), 153284 deletions(-) I wonder why it is not exactly the same? Maybe because the python diff algorithm is a bit differnt to GNU diff. ---- 2005-03-29 full check, retrieving all file texts once for the 2.4 kernel branch takes 10m elapsed, 1m cpu time. lots of random IO and seeking. commit refs/heads/master mark :128 committer 1112058529 +1000 data 43 More support for compressed files in stores from :127 M 644 inline bzrlib/store.py data 5485 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Stores are the main data-storage mechanism for Bazaar-NG. A store is a simple write-once container indexed by a universally unique ID, which is typically the SHA-1 of the content.""" __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import os, tempfile, types, osutils, gzip, errno from stat import ST_SIZE from StringIO import StringIO from trace import mutter ###################################################################### # stores class StoreError(Exception): pass class ImmutableStore: """Store that holds files indexed by unique names. Files can be added, but not modified once they are in. Typically the hash is used as the name, or something else known to be unique, such as a UUID. >>> st = ImmutableScratchStore() >>> st.add(StringIO('hello'), 'aa') >>> 'aa' in st True >>> 'foo' in st False You are not allowed to add an id that is already present. Entries can be retrieved as files, which may then be read. >>> st.add(StringIO('goodbye'), '123123') >>> st['123123'].read() 'goodbye' :todo: Atomic add by writing to a temporary file and renaming. :todo: Perhaps automatically transform to/from XML in a method? Would just need to tell the constructor what class to use... :todo: Even within a simple disk store like this, we could gzip the files. But since many are less than one disk block, that might not help a lot. """ def __init__(self, basedir): """ImmutableStore constructor.""" self._basedir = basedir def _path(self, id): return os.path.join(self._basedir, id) def __repr__(self): return "%s(%r)" % (self.__class__.__name__, self._basedir) def add(self, f, fileid): """Add contents of a file into the store. :param f: An open file, or file-like object.""" # FIXME: Only works on smallish files # TODO: Can be optimized by copying at the same time as # computing the sum. mutter("add store entry %r" % (fileid)) if isinstance(f, types.StringTypes): content = f else: content = f.read() if fileid not in self: filename = self._path(fileid) f = file(filename, 'wb') f.write(content) ## f.flush() ## os.fsync(f.fileno()) f.close() osutils.make_readonly(filename) def __contains__(self, fileid): """""" p = self._path(fileid) return (os.access(p, os.R_OK) or os.access(p + '.gz', os.R_OK)) # TODO: Guard against the same thing being stored twice, compressed and uncompresse def __iter__(self): for f in os.listdir(self._basedir): if f[-3:] == '.gz': # TODO: case-insensitive? yield f[:-3] else: yield f def __len__(self): return len(os.listdir(self._basedir)) def __getitem__(self, fileid): """Returns a file reading from a particular entry.""" p = self._path(fileid) try: return gzip.GzipFile(p + '.gz', 'rb') except IOError, e: if e.errno == errno.ENOENT: return file(p, 'rb') else: raise e def total_size(self): """Return (count, bytes) This is the (compressed) size stored on disk, not the size of the content.""" total = 0 count = 0 for fid in self: count += 1 p = self._path(fid) try: total += os.stat(p)[ST_SIZE] except OSError: total += os.stat(p + '.gz')[ST_SIZE] return count, total def delete_all(self): for fileid in self: self.delete(fileid) def delete(self, fileid): """Remove nominated store entry. Most stores will be add-only.""" filename = self._path(fileid) ## osutils.make_writable(filename) ## TODO: handle gzip os.remove(filename) def destroy(self): """Remove store; only allowed if it is empty.""" os.rmdir(self._basedir) mutter("%r destroyed" % self) class ImmutableScratchStore(ImmutableStore): """Self-destructing test subclass of ImmutableStore. The Store only exists for the lifetime of the Python object. Obviously you should not put anything precious in it. """ def __init__(self): ImmutableStore.__init__(self, tempfile.mkdtemp()) def __del__(self): self.delete_all() self.destroy() commit refs/heads/master mark :129 committer 1112058976 +1000 data 42 Store.add defaults to adding gzipped files from :128 M 644 inline bzrlib/store.py data 5655 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Stores are the main data-storage mechanism for Bazaar-NG. A store is a simple write-once container indexed by a universally unique ID, which is typically the SHA-1 of the content.""" __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import os, tempfile, types, osutils, gzip, errno from stat import ST_SIZE from StringIO import StringIO from trace import mutter ###################################################################### # stores class StoreError(Exception): pass class ImmutableStore: """Store that holds files indexed by unique names. Files can be added, but not modified once they are in. Typically the hash is used as the name, or something else known to be unique, such as a UUID. >>> st = ImmutableScratchStore() >>> st.add(StringIO('hello'), 'aa') >>> 'aa' in st True >>> 'foo' in st False You are not allowed to add an id that is already present. Entries can be retrieved as files, which may then be read. >>> st.add(StringIO('goodbye'), '123123') >>> st['123123'].read() 'goodbye' :todo: Atomic add by writing to a temporary file and renaming. :todo: Perhaps automatically transform to/from XML in a method? Would just need to tell the constructor what class to use... :todo: Even within a simple disk store like this, we could gzip the files. But since many are less than one disk block, that might not help a lot. """ def __init__(self, basedir): """ImmutableStore constructor.""" self._basedir = basedir def _path(self, id): return os.path.join(self._basedir, id) def __repr__(self): return "%s(%r)" % (self.__class__.__name__, self._basedir) def add(self, f, fileid, compressed=True): """Add contents of a file into the store. :param f: An open file, or file-like object.""" # FIXME: Only works on smallish files # TODO: Can be optimized by copying at the same time as # computing the sum. mutter("add store entry %r" % (fileid)) if isinstance(f, types.StringTypes): content = f else: content = f.read() p = self._path(fileid) if os.access(p, os.F_OK) or os.access(p + '.gz', os.F_OK): bailout("store %r already contains id %r" % (self._basedir, fileid)) if compressed: f = gzip.GzipFile(p + '.gz', 'wb') os.chmod(p + '.gz', 0444) else: f = file(p, 'wb') os.chmod(p, 0444) f.write(content) f.close() def __contains__(self, fileid): """""" p = self._path(fileid) return (os.access(p, os.R_OK) or os.access(p + '.gz', os.R_OK)) # TODO: Guard against the same thing being stored twice, compressed and uncompresse def __iter__(self): for f in os.listdir(self._basedir): if f[-3:] == '.gz': # TODO: case-insensitive? yield f[:-3] else: yield f def __len__(self): return len(os.listdir(self._basedir)) def __getitem__(self, fileid): """Returns a file reading from a particular entry.""" p = self._path(fileid) try: return gzip.GzipFile(p + '.gz', 'rb') except IOError, e: if e.errno == errno.ENOENT: return file(p, 'rb') else: raise e def total_size(self): """Return (count, bytes) This is the (compressed) size stored on disk, not the size of the content.""" total = 0 count = 0 for fid in self: count += 1 p = self._path(fid) try: total += os.stat(p)[ST_SIZE] except OSError: total += os.stat(p + '.gz')[ST_SIZE] return count, total def delete_all(self): for fileid in self: self.delete(fileid) def delete(self, fileid): """Remove nominated store entry. Most stores will be add-only.""" filename = self._path(fileid) ## osutils.make_writable(filename) ## TODO: handle gzip os.remove(filename) def destroy(self): """Remove store; only allowed if it is empty.""" os.rmdir(self._basedir) mutter("%r destroyed" % self) class ImmutableScratchStore(ImmutableStore): """Self-destructing test subclass of ImmutableStore. The Store only exists for the lifetime of the Python object. Obviously you should not put anything precious in it. """ def __init__(self): ImmutableStore.__init__(self, tempfile.mkdtemp()) def __del__(self): self.delete_all() self.destroy() commit refs/heads/master mark :130 committer 1112059579 +1000 data 178 - fixup checks on retrieved files to cope with compression, and have less checks now there's a check command again - move code to destroy stores into the ScratchStore subclass from :129 M 644 inline bzrlib/store.py data 5285 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Stores are the main data-storage mechanism for Bazaar-NG. A store is a simple write-once container indexed by a universally unique ID, which is typically the SHA-1 of the content.""" __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import os, tempfile, types, osutils, gzip, errno from stat import ST_SIZE from StringIO import StringIO from trace import mutter ###################################################################### # stores class StoreError(Exception): pass class ImmutableStore: """Store that holds files indexed by unique names. Files can be added, but not modified once they are in. Typically the hash is used as the name, or something else known to be unique, such as a UUID. >>> st = ImmutableScratchStore() >>> st.add(StringIO('hello'), 'aa') >>> 'aa' in st True >>> 'foo' in st False You are not allowed to add an id that is already present. Entries can be retrieved as files, which may then be read. >>> st.add(StringIO('goodbye'), '123123') >>> st['123123'].read() 'goodbye' :todo: Atomic add by writing to a temporary file and renaming. :todo: Perhaps automatically transform to/from XML in a method? Would just need to tell the constructor what class to use... :todo: Even within a simple disk store like this, we could gzip the files. But since many are less than one disk block, that might not help a lot. """ def __init__(self, basedir): """ImmutableStore constructor.""" self._basedir = basedir def _path(self, id): return os.path.join(self._basedir, id) def __repr__(self): return "%s(%r)" % (self.__class__.__name__, self._basedir) def add(self, f, fileid, compressed=True): """Add contents of a file into the store. :param f: An open file, or file-like object.""" # FIXME: Only works on smallish files # TODO: Can be optimized by copying at the same time as # computing the sum. mutter("add store entry %r" % (fileid)) if isinstance(f, types.StringTypes): content = f else: content = f.read() p = self._path(fileid) if os.access(p, os.F_OK) or os.access(p + '.gz', os.F_OK): bailout("store %r already contains id %r" % (self._basedir, fileid)) if compressed: f = gzip.GzipFile(p + '.gz', 'wb') os.chmod(p + '.gz', 0444) else: f = file(p, 'wb') os.chmod(p, 0444) f.write(content) f.close() def __contains__(self, fileid): """""" p = self._path(fileid) return (os.access(p, os.R_OK) or os.access(p + '.gz', os.R_OK)) # TODO: Guard against the same thing being stored twice, compressed and uncompresse def __iter__(self): for f in os.listdir(self._basedir): if f[-3:] == '.gz': # TODO: case-insensitive? yield f[:-3] else: yield f def __len__(self): return len(os.listdir(self._basedir)) def __getitem__(self, fileid): """Returns a file reading from a particular entry.""" p = self._path(fileid) try: return gzip.GzipFile(p + '.gz', 'rb') except IOError, e: if e.errno == errno.ENOENT: return file(p, 'rb') else: raise e def total_size(self): """Return (count, bytes) This is the (compressed) size stored on disk, not the size of the content.""" total = 0 count = 0 for fid in self: count += 1 p = self._path(fid) try: total += os.stat(p)[ST_SIZE] except OSError: total += os.stat(p + '.gz')[ST_SIZE] return count, total class ImmutableScratchStore(ImmutableStore): """Self-destructing test subclass of ImmutableStore. The Store only exists for the lifetime of the Python object. Obviously you should not put anything precious in it. """ def __init__(self): ImmutableStore.__init__(self, tempfile.mkdtemp()) def __del__(self): for f in os.listdir(self._basedir): os.remove(os.path.join(self._basedir, f)) os.rmdir(self._basedir) mutter("%r destroyed" % self) M 644 inline bzrlib/tree.py data 12579 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tree classes, representing directory at point in time. """ from sets import Set import os.path, os, fnmatch from inventory import Inventory from trace import mutter, note from osutils import pumpfile, compare_files, filesize, quotefn, sha_file, \ joinpath, splitpath, appendpath, isdir, isfile, file_kind, fingerprint_file from errors import bailout import branch from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE import bzrlib class Tree: """Abstract file tree. There are several subclasses: * `WorkingTree` exists as files on disk editable by the user. * `RevisionTree` is a tree as recorded at some point in the past. * `EmptyTree` Trees contain an `Inventory` object, and also know how to retrieve file texts mentioned in the inventory, either from a working directory or from a store. It is possible for trees to contain files that are not described in their inventory or vice versa; for this use `filenames()`. Trees can be compared, etc, regardless of whether they are working trees or versioned trees. """ def has_filename(self, filename): """True if the tree has given filename.""" raise NotImplementedError() def has_id(self, file_id): return self.inventory.has_id(file_id) def id_set(self): """Return set of all ids in this tree.""" return self.inventory.id_set() def id2path(self, file_id): return self.inventory.id2path(file_id) def _get_inventory(self): return self._inventory inventory = property(_get_inventory, doc="Inventory of this Tree") def _check_retrieved(self, ie, f): fp = fingerprint_file(f) f.seek(0) if ie.text_size is not None: if fs != fp['size']: bailout("mismatched size for file %r in %r" % (ie.file_id, self._store), ["inventory expects %d bytes" % ie.text_size, "file is actually %d bytes" % fp['size'], "store is probably damaged/corrupt"]) if ie.text_sha1 != fp['sha1']: bailout("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store), ["inventory expects %s" % ie.text_sha1, "file is actually %s" % fp['sha1'], "store is probably damaged/corrupt"]) def export(self, dest): """Export this tree to a new directory. `dest` should not exist, and will be created holding the contents of this tree. :todo: To handle subdirectories we need to create the directories first. :note: If the export fails, the destination directory will be left in a half-assed state. """ os.mkdir(dest) mutter('export version %r' % self) inv = self.inventory for dp, ie in inv.iter_entries(): kind = ie.kind fullpath = appendpath(dest, dp) if kind == 'directory': os.mkdir(fullpath) elif kind == 'file': pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb')) else: bailout("don't know how to export {%s} of kind %r", fid, kind) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) class WorkingTree(Tree): """Working copy tree. The inventory is held in the `Branch` working-inventory, and the files are in a directory on disk. It is possible for a `WorkingTree` to have a filename which is not listed in the Inventory and vice versa. """ def __init__(self, basedir, inv): self._inventory = inv self.basedir = basedir self.path2id = inv.path2id def __repr__(self): return "<%s of %s>" % (self.__class__.__name__, self.basedir) def abspath(self, filename): return os.path.join(self.basedir, filename) def has_filename(self, filename): return os.path.exists(self.abspath(filename)) def get_file(self, file_id): return self.get_file_byname(self.id2path(file_id)) def get_file_byname(self, filename): return file(self.abspath(filename), 'rb') def _get_store_filename(self, file_id): return self.abspath(self.id2path(file_id)) def has_id(self, file_id): # files that have been deleted are excluded if not self.inventory.has_id(file_id): return False return os.access(self.abspath(self.inventory.id2path(file_id)), os.F_OK) def get_file_size(self, file_id): return os.stat(self._get_store_filename(file_id))[ST_SIZE] def get_file_sha1(self, file_id): f = self.get_file(file_id) return sha_file(f) def file_class(self, filename): if self.path2id(filename): return 'V' elif self.is_ignored(filename): return 'I' else: return '?' def file_kind(self, filename): if isfile(self.abspath(filename)): return 'file' elif isdir(self.abspath(filename)): return 'directory' else: return 'unknown' def list_files(self): """Recursively list all files as (path, class, kind, id). Lists, but does not descend into unversioned directories. This does not include files that have been deleted in this tree. Skips the control directory. """ inv = self.inventory def descend(from_dir, from_dir_id, dp): ls = os.listdir(dp) ls.sort() for f in ls: if bzrlib.BZRDIR == f: continue # path within tree fp = appendpath(from_dir, f) # absolute path fap = appendpath(dp, f) f_ie = inv.get_child(from_dir_id, f) if f_ie: c = 'V' elif self.is_ignored(fp): c = 'I' else: c = '?' fk = file_kind(fap) if f_ie: if f_ie.kind != fk: bailout("file %r entered as kind %r id %r, now of kind %r" % (fap, f_ie.kind, f_ie.file_id, fk)) yield fp, c, fk, (f_ie and f_ie.file_id) if fk != 'directory': continue if c != 'V': # don't descend unversioned directories continue for ff in descend(fp, f_ie.file_id, fap): yield ff for f in descend('', None, self.basedir): yield f def unknowns(self, path='', dir_id=None): """Yield names of unknown files in this WorkingTree. If there are any unknown directories then only the directory is returned, not all its children. But if there are unknown files under a versioned subdirectory, they are returned. Currently returned depth-first, sorted by name within directories. """ for fpath, fclass, fkind, fid in self.list_files(): if fclass == '?': yield fpath def ignored_files(self): for fpath, fclass, fkind, fid in self.list_files(): if fclass == 'I': yield fpath def get_ignore_list(self): """Return list of ignore patterns. Cached in the Tree object after the first call. """ if hasattr(self, '_ignorelist'): return self._ignorelist l = bzrlib.DEFAULT_IGNORE[:] if self.has_filename(bzrlib.IGNORE_FILENAME): f = self.get_file_byname(bzrlib.IGNORE_FILENAME) l.extend([line.rstrip("\n\r") for line in f.readlines()]) self._ignorelist = l return l def is_ignored(self, filename): """Check whether the filename matches an ignore pattern. Patterns containing '/' need to match the whole path; others match against only the last component.""" ## TODO: Use extended zsh-style globs maybe? ## TODO: Use '**' to match directories? for pat in self.get_ignore_list(): if '/' in pat: if fnmatch.fnmatchcase(filename, pat): return True else: if fnmatch.fnmatchcase(splitpath(filename)[-1], pat): return True return False class RevisionTree(Tree): """Tree viewing a previous revision. File text can be retrieved from the text store. :todo: Some kind of `__repr__` method, but a good one probably means knowing the branch and revision number, or at least passing a description to the constructor. """ def __init__(self, store, inv): self._store = store self._inventory = inv def get_file(self, file_id): ie = self._inventory[file_id] f = self._store[ie.text_id] mutter(" get fileid{%s} from %r" % (file_id, self)) ## self._check_retrieved(ie, f) return f def get_file_size(self, file_id): return self._inventory[file_id].text_size def get_file_sha1(self, file_id): ie = self._inventory[file_id] return ie.text_sha1 def has_filename(self, filename): return bool(self.inventory.path2id(filename)) def list_files(self): # The only files returned by this are those from the version for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id class EmptyTree(Tree): def __init__(self): self._inventory = Inventory() def has_filename(self, filename): return False def list_files(self): if False: # just to make it a generator yield None ###################################################################### # diff # TODO: Merge these two functions into a single one that can operate # on either a whole tree or a set of files. # TODO: Return the diff in order by filename, not by category or in # random order. Can probably be done by lock-stepping through the # filenames from both trees. def file_status(filename, old_tree, new_tree): """Return single-letter status, old and new names for a file. The complexity here is in deciding how to represent renames; many complex cases are possible. """ old_inv = old_tree.inventory new_inv = new_tree.inventory new_id = new_inv.path2id(filename) old_id = old_inv.path2id(filename) if not new_id and not old_id: # easy: doesn't exist in either; not versioned at all if new_tree.is_ignored(filename): return 'I', None, None else: return '?', None, None elif new_id: # There is now a file of this name, great. pass else: # There is no longer a file of this name, but we can describe # what happened to the file that used to have # this name. There are two possibilities: either it was # deleted entirely, or renamed. assert old_id if new_inv.has_id(old_id): return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id) else: return 'D', old_inv.id2path(old_id), None # if the file_id is new in this revision, it is added if new_id and not old_inv.has_id(new_id): return 'A' # if there used to be a file of this name, but that ID has now # disappeared, it is deleted if old_id and not new_inv.has_id(old_id): return 'D' return 'wtf?' commit refs/heads/master mark :131 committer 1112063494 +1000 data 52 check size and sha1 of files retrieved from the tree from :130 M 644 inline bzrlib/osutils.py data 7405 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, types, re, time, types from stat import S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE from errors import bailout def make_readonly(filename): """Make a filename read-only.""" # TODO: probably needs to be fixed for windows mod = os.stat(filename).st_mode mod = mod & 0777555 os.chmod(filename, mod) def make_writable(filename): mod = os.stat(filename).st_mode mod = mod | 0200 os.chmod(filename, mod) _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])') def quotefn(f): """Return shell-quoted filename""" ## We could be a bit more terse by using double-quotes etc f = _QUOTE_RE.sub(r'\\\1', f) if f[0] == '~': f[0:1] = r'\~' return f def file_kind(f): mode = os.lstat(f)[ST_MODE] if S_ISREG(mode): return 'file' elif S_ISDIR(mode): return 'directory' elif S_ISLNK(mode): return 'symlink' else: bailout("can't handle file kind with mode %o of %r" % (mode, f)) def isdir(f): """True if f is an accessible directory.""" try: return S_ISDIR(os.lstat(f)[ST_MODE]) except OSError: return False def isfile(f): """True if f is a regular file.""" try: return S_ISREG(os.lstat(f)[ST_MODE]) except OSError: return False def pumpfile(fromfile, tofile): """Copy contents of one file to another.""" tofile.write(fromfile.read()) def uuid(): """Return a new UUID""" ## XXX: Could alternatively read /proc/sys/kernel/random/uuid on ## Linux, but we need something portable for other systems; ## preferably an implementation in Python. try: return chomp(file('/proc/sys/kernel/random/uuid').readline()) except IOError: return chomp(os.popen('uuidgen').readline()) def chomp(s): if s and (s[-1] == '\n'): return s[:-1] else: return s def sha_file(f): import sha ## TODO: Maybe read in chunks to handle big files if hasattr(f, 'tell'): assert f.tell() == 0 s = sha.new() s.update(f.read()) return s.hexdigest() def sha_string(f): import sha s = sha.new() s.update(f) return s.hexdigest() def fingerprint_file(f): import sha s = sha.new() b = f.read() s.update(b) size = len(b) return {'size': size, 'sha1': s.hexdigest()} def username(): """Return email-style username. Something similar to 'Martin Pool ' :todo: Check it's reasonably well-formed. :todo: Allow taking it from a dotfile to help people on windows who can't easily set variables. :todo: Cope without pwd module, which is only on unix. """ e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: return e import socket try: import pwd uid = os.getuid() w = pwd.getpwuid(uid) gecos = w.pw_gecos comma = gecos.find(',') if comma == -1: realname = gecos else: realname = gecos[:comma] return '%s <%s@%s>' % (realname, w.pw_name, socket.getfqdn()) except ImportError: pass import getpass, socket return '<%s@%s>' % (getpass.getuser(), socket.getfqdn()) def user_email(): """Return just the email component of a username.""" e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: import re m = re.search(r'[\w+.-]+@[\w+.-]+', e) if not m: bailout('%r is not a reasonable email address' % e) return m.group(0) import getpass, socket return '%s@%s' % (getpass.getuser(), socket.getfqdn()) def compare_files(a, b): """Returns true if equal in contents""" # TODO: don't read the whole thing in one go. BUFSIZE = 4096 while True: ai = a.read(BUFSIZE) bi = b.read(BUFSIZE) if ai != bi: return False if ai == '': return True def local_time_offset(t=None): """Return offset of local zone from GMT, either at present or at time t.""" # python2.3 localtime() can't take None if t is None: t = time.time() if time.localtime(t).tm_isdst and time.daylight: return -time.altzone else: return -time.timezone def format_date(t, offset=0, timezone='original'): ## TODO: Perhaps a global option to use either universal or local time? ## Or perhaps just let people set $TZ? import time assert isinstance(t, float) if timezone == 'utc': tt = time.gmtime(t) offset = 0 elif timezone == 'original': if offset == None: offset = 0 tt = time.gmtime(t + offset) elif timezone == 'local': tt = time.localtime(t) offset = local_time_offset(t) else: bailout("unsupported timezone format %r", ['options are "utc", "original", "local"']) return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt) + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)) def compact_date(when): return time.strftime('%Y%m%d%H%M%S', time.gmtime(when)) def filesize(f): """Return size of given open file.""" return os.fstat(f.fileno())[ST_SIZE] if hasattr(os, 'urandom'): # python 2.4 and later rand_bytes = os.urandom else: # FIXME: No good on non-Linux _rand_file = file('/dev/urandom', 'rb') rand_bytes = _rand_file.read ## TODO: We could later have path objects that remember their list ## decomposition (might be too tricksy though.) def splitpath(p): """Turn string into list of parts. >>> splitpath('a') ['a'] >>> splitpath('a/b') ['a', 'b'] >>> splitpath('a/./b') ['a', 'b'] >>> splitpath('a/.b') ['a', '.b'] >>> splitpath('a/../b') Traceback (most recent call last): ... BzrError: ("sorry, '..' not allowed in path", []) """ assert isinstance(p, types.StringTypes) ps = [f for f in p.split('/') if f != '.'] for f in ps: if f == '..': bailout("sorry, %r not allowed in path" % f) return ps def joinpath(p): assert isinstance(p, list) for f in p: if (f == '..') or (f is None) or (f == ''): bailout("sorry, %r not allowed in path" % f) return '/'.join(p) def appendpath(p1, p2): if p1 == '': return p2 else: return p1 + '/' + p2 def extern_command(cmd, ignore_errors = False): mutter('external command: %s' % `cmd`) if os.system(cmd): if not ignore_errors: bailout('command failed') M 644 inline bzrlib/tree.py data 12586 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tree classes, representing directory at point in time. """ from sets import Set import os.path, os, fnmatch from inventory import Inventory from trace import mutter, note from osutils import pumpfile, compare_files, filesize, quotefn, sha_file, \ joinpath, splitpath, appendpath, isdir, isfile, file_kind, fingerprint_file from errors import bailout import branch from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE import bzrlib class Tree: """Abstract file tree. There are several subclasses: * `WorkingTree` exists as files on disk editable by the user. * `RevisionTree` is a tree as recorded at some point in the past. * `EmptyTree` Trees contain an `Inventory` object, and also know how to retrieve file texts mentioned in the inventory, either from a working directory or from a store. It is possible for trees to contain files that are not described in their inventory or vice versa; for this use `filenames()`. Trees can be compared, etc, regardless of whether they are working trees or versioned trees. """ def has_filename(self, filename): """True if the tree has given filename.""" raise NotImplementedError() def has_id(self, file_id): return self.inventory.has_id(file_id) def id_set(self): """Return set of all ids in this tree.""" return self.inventory.id_set() def id2path(self, file_id): return self.inventory.id2path(file_id) def _get_inventory(self): return self._inventory inventory = property(_get_inventory, doc="Inventory of this Tree") def _check_retrieved(self, ie, f): fp = fingerprint_file(f) f.seek(0) if ie.text_size is not None: if ie.text_size != fp['size']: bailout("mismatched size for file %r in %r" % (ie.file_id, self._store), ["inventory expects %d bytes" % ie.text_size, "file is actually %d bytes" % fp['size'], "store is probably damaged/corrupt"]) if ie.text_sha1 != fp['sha1']: bailout("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store), ["inventory expects %s" % ie.text_sha1, "file is actually %s" % fp['sha1'], "store is probably damaged/corrupt"]) def export(self, dest): """Export this tree to a new directory. `dest` should not exist, and will be created holding the contents of this tree. :todo: To handle subdirectories we need to create the directories first. :note: If the export fails, the destination directory will be left in a half-assed state. """ os.mkdir(dest) mutter('export version %r' % self) inv = self.inventory for dp, ie in inv.iter_entries(): kind = ie.kind fullpath = appendpath(dest, dp) if kind == 'directory': os.mkdir(fullpath) elif kind == 'file': pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb')) else: bailout("don't know how to export {%s} of kind %r", fid, kind) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) class WorkingTree(Tree): """Working copy tree. The inventory is held in the `Branch` working-inventory, and the files are in a directory on disk. It is possible for a `WorkingTree` to have a filename which is not listed in the Inventory and vice versa. """ def __init__(self, basedir, inv): self._inventory = inv self.basedir = basedir self.path2id = inv.path2id def __repr__(self): return "<%s of %s>" % (self.__class__.__name__, self.basedir) def abspath(self, filename): return os.path.join(self.basedir, filename) def has_filename(self, filename): return os.path.exists(self.abspath(filename)) def get_file(self, file_id): return self.get_file_byname(self.id2path(file_id)) def get_file_byname(self, filename): return file(self.abspath(filename), 'rb') def _get_store_filename(self, file_id): return self.abspath(self.id2path(file_id)) def has_id(self, file_id): # files that have been deleted are excluded if not self.inventory.has_id(file_id): return False return os.access(self.abspath(self.inventory.id2path(file_id)), os.F_OK) def get_file_size(self, file_id): return os.stat(self._get_store_filename(file_id))[ST_SIZE] def get_file_sha1(self, file_id): f = self.get_file(file_id) return sha_file(f) def file_class(self, filename): if self.path2id(filename): return 'V' elif self.is_ignored(filename): return 'I' else: return '?' def file_kind(self, filename): if isfile(self.abspath(filename)): return 'file' elif isdir(self.abspath(filename)): return 'directory' else: return 'unknown' def list_files(self): """Recursively list all files as (path, class, kind, id). Lists, but does not descend into unversioned directories. This does not include files that have been deleted in this tree. Skips the control directory. """ inv = self.inventory def descend(from_dir, from_dir_id, dp): ls = os.listdir(dp) ls.sort() for f in ls: if bzrlib.BZRDIR == f: continue # path within tree fp = appendpath(from_dir, f) # absolute path fap = appendpath(dp, f) f_ie = inv.get_child(from_dir_id, f) if f_ie: c = 'V' elif self.is_ignored(fp): c = 'I' else: c = '?' fk = file_kind(fap) if f_ie: if f_ie.kind != fk: bailout("file %r entered as kind %r id %r, now of kind %r" % (fap, f_ie.kind, f_ie.file_id, fk)) yield fp, c, fk, (f_ie and f_ie.file_id) if fk != 'directory': continue if c != 'V': # don't descend unversioned directories continue for ff in descend(fp, f_ie.file_id, fap): yield ff for f in descend('', None, self.basedir): yield f def unknowns(self, path='', dir_id=None): """Yield names of unknown files in this WorkingTree. If there are any unknown directories then only the directory is returned, not all its children. But if there are unknown files under a versioned subdirectory, they are returned. Currently returned depth-first, sorted by name within directories. """ for fpath, fclass, fkind, fid in self.list_files(): if fclass == '?': yield fpath def ignored_files(self): for fpath, fclass, fkind, fid in self.list_files(): if fclass == 'I': yield fpath def get_ignore_list(self): """Return list of ignore patterns. Cached in the Tree object after the first call. """ if hasattr(self, '_ignorelist'): return self._ignorelist l = bzrlib.DEFAULT_IGNORE[:] if self.has_filename(bzrlib.IGNORE_FILENAME): f = self.get_file_byname(bzrlib.IGNORE_FILENAME) l.extend([line.rstrip("\n\r") for line in f.readlines()]) self._ignorelist = l return l def is_ignored(self, filename): """Check whether the filename matches an ignore pattern. Patterns containing '/' need to match the whole path; others match against only the last component.""" ## TODO: Use extended zsh-style globs maybe? ## TODO: Use '**' to match directories? for pat in self.get_ignore_list(): if '/' in pat: if fnmatch.fnmatchcase(filename, pat): return True else: if fnmatch.fnmatchcase(splitpath(filename)[-1], pat): return True return False class RevisionTree(Tree): """Tree viewing a previous revision. File text can be retrieved from the text store. :todo: Some kind of `__repr__` method, but a good one probably means knowing the branch and revision number, or at least passing a description to the constructor. """ def __init__(self, store, inv): self._store = store self._inventory = inv def get_file(self, file_id): ie = self._inventory[file_id] f = self._store[ie.text_id] mutter(" get fileid{%s} from %r" % (file_id, self)) self._check_retrieved(ie, f) return f def get_file_size(self, file_id): return self._inventory[file_id].text_size def get_file_sha1(self, file_id): ie = self._inventory[file_id] return ie.text_sha1 def has_filename(self, filename): return bool(self.inventory.path2id(filename)) def list_files(self): # The only files returned by this are those from the version for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id class EmptyTree(Tree): def __init__(self): self._inventory = Inventory() def has_filename(self, filename): return False def list_files(self): if False: # just to make it a generator yield None ###################################################################### # diff # TODO: Merge these two functions into a single one that can operate # on either a whole tree or a set of files. # TODO: Return the diff in order by filename, not by category or in # random order. Can probably be done by lock-stepping through the # filenames from both trees. def file_status(filename, old_tree, new_tree): """Return single-letter status, old and new names for a file. The complexity here is in deciding how to represent renames; many complex cases are possible. """ old_inv = old_tree.inventory new_inv = new_tree.inventory new_id = new_inv.path2id(filename) old_id = old_inv.path2id(filename) if not new_id and not old_id: # easy: doesn't exist in either; not versioned at all if new_tree.is_ignored(filename): return 'I', None, None else: return '?', None, None elif new_id: # There is now a file of this name, great. pass else: # There is no longer a file of this name, but we can describe # what happened to the file that used to have # this name. There are two possibilities: either it was # deleted entirely, or renamed. assert old_id if new_inv.has_id(old_id): return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id) else: return 'D', old_inv.id2path(old_id), None # if the file_id is new in this revision, it is added if new_id and not old_inv.has_id(new_id): return 'A' # if there used to be a file of this name, but that ID has now # disappeared, it is deleted if old_id and not new_inv.has_id(old_id): return 'D' return 'wtf?' commit refs/heads/master mark :132 committer 1112064067 +1000 data 56 Tree.is_ignored returns the pattern that matched, if any from :131 M 644 inline bzrlib/tree.py data 12759 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tree classes, representing directory at point in time. """ from sets import Set import os.path, os, fnmatch from inventory import Inventory from trace import mutter, note from osutils import pumpfile, compare_files, filesize, quotefn, sha_file, \ joinpath, splitpath, appendpath, isdir, isfile, file_kind, fingerprint_file from errors import bailout import branch from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE import bzrlib class Tree: """Abstract file tree. There are several subclasses: * `WorkingTree` exists as files on disk editable by the user. * `RevisionTree` is a tree as recorded at some point in the past. * `EmptyTree` Trees contain an `Inventory` object, and also know how to retrieve file texts mentioned in the inventory, either from a working directory or from a store. It is possible for trees to contain files that are not described in their inventory or vice versa; for this use `filenames()`. Trees can be compared, etc, regardless of whether they are working trees or versioned trees. """ def has_filename(self, filename): """True if the tree has given filename.""" raise NotImplementedError() def has_id(self, file_id): return self.inventory.has_id(file_id) def id_set(self): """Return set of all ids in this tree.""" return self.inventory.id_set() def id2path(self, file_id): return self.inventory.id2path(file_id) def _get_inventory(self): return self._inventory inventory = property(_get_inventory, doc="Inventory of this Tree") def _check_retrieved(self, ie, f): fp = fingerprint_file(f) f.seek(0) if ie.text_size is not None: if ie.text_size != fp['size']: bailout("mismatched size for file %r in %r" % (ie.file_id, self._store), ["inventory expects %d bytes" % ie.text_size, "file is actually %d bytes" % fp['size'], "store is probably damaged/corrupt"]) if ie.text_sha1 != fp['sha1']: bailout("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store), ["inventory expects %s" % ie.text_sha1, "file is actually %s" % fp['sha1'], "store is probably damaged/corrupt"]) def export(self, dest): """Export this tree to a new directory. `dest` should not exist, and will be created holding the contents of this tree. :todo: To handle subdirectories we need to create the directories first. :note: If the export fails, the destination directory will be left in a half-assed state. """ os.mkdir(dest) mutter('export version %r' % self) inv = self.inventory for dp, ie in inv.iter_entries(): kind = ie.kind fullpath = appendpath(dest, dp) if kind == 'directory': os.mkdir(fullpath) elif kind == 'file': pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb')) else: bailout("don't know how to export {%s} of kind %r", fid, kind) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) class WorkingTree(Tree): """Working copy tree. The inventory is held in the `Branch` working-inventory, and the files are in a directory on disk. It is possible for a `WorkingTree` to have a filename which is not listed in the Inventory and vice versa. """ def __init__(self, basedir, inv): self._inventory = inv self.basedir = basedir self.path2id = inv.path2id def __repr__(self): return "<%s of %s>" % (self.__class__.__name__, self.basedir) def abspath(self, filename): return os.path.join(self.basedir, filename) def has_filename(self, filename): return os.path.exists(self.abspath(filename)) def get_file(self, file_id): return self.get_file_byname(self.id2path(file_id)) def get_file_byname(self, filename): return file(self.abspath(filename), 'rb') def _get_store_filename(self, file_id): return self.abspath(self.id2path(file_id)) def has_id(self, file_id): # files that have been deleted are excluded if not self.inventory.has_id(file_id): return False return os.access(self.abspath(self.inventory.id2path(file_id)), os.F_OK) def get_file_size(self, file_id): return os.stat(self._get_store_filename(file_id))[ST_SIZE] def get_file_sha1(self, file_id): f = self.get_file(file_id) return sha_file(f) def file_class(self, filename): if self.path2id(filename): return 'V' elif self.is_ignored(filename): return 'I' else: return '?' def file_kind(self, filename): if isfile(self.abspath(filename)): return 'file' elif isdir(self.abspath(filename)): return 'directory' else: return 'unknown' def list_files(self): """Recursively list all files as (path, class, kind, id). Lists, but does not descend into unversioned directories. This does not include files that have been deleted in this tree. Skips the control directory. """ inv = self.inventory def descend(from_dir, from_dir_id, dp): ls = os.listdir(dp) ls.sort() for f in ls: if bzrlib.BZRDIR == f: continue # path within tree fp = appendpath(from_dir, f) # absolute path fap = appendpath(dp, f) f_ie = inv.get_child(from_dir_id, f) if f_ie: c = 'V' elif self.is_ignored(fp): c = 'I' else: c = '?' fk = file_kind(fap) if f_ie: if f_ie.kind != fk: bailout("file %r entered as kind %r id %r, now of kind %r" % (fap, f_ie.kind, f_ie.file_id, fk)) yield fp, c, fk, (f_ie and f_ie.file_id) if fk != 'directory': continue if c != 'V': # don't descend unversioned directories continue for ff in descend(fp, f_ie.file_id, fap): yield ff for f in descend('', None, self.basedir): yield f def unknowns(self, path='', dir_id=None): """Yield names of unknown files in this WorkingTree. If there are any unknown directories then only the directory is returned, not all its children. But if there are unknown files under a versioned subdirectory, they are returned. Currently returned depth-first, sorted by name within directories. """ for fpath, fclass, fkind, fid in self.list_files(): if fclass == '?': yield fpath def ignored_files(self): for fpath, fclass, fkind, fid in self.list_files(): if fclass == 'I': yield fpath def get_ignore_list(self): """Return list of ignore patterns. Cached in the Tree object after the first call. """ if hasattr(self, '_ignorelist'): return self._ignorelist l = bzrlib.DEFAULT_IGNORE[:] if self.has_filename(bzrlib.IGNORE_FILENAME): f = self.get_file_byname(bzrlib.IGNORE_FILENAME) l.extend([line.rstrip("\n\r") for line in f.readlines()]) self._ignorelist = l return l def is_ignored(self, filename): """Check whether the filename matches an ignore pattern. Patterns containing '/' need to match the whole path; others match against only the last component. If the file is ignored, returns the pattern which caused it to be ignored, otherwise None. So this can simply be used as a boolean if desired.""" ## TODO: Use '**' to match directories, and other extended globbing stuff from cvs/rsync. for pat in self.get_ignore_list(): if '/' in pat: if fnmatch.fnmatchcase(filename, pat): return pat else: if fnmatch.fnmatchcase(splitpath(filename)[-1], pat): return pat return None class RevisionTree(Tree): """Tree viewing a previous revision. File text can be retrieved from the text store. :todo: Some kind of `__repr__` method, but a good one probably means knowing the branch and revision number, or at least passing a description to the constructor. """ def __init__(self, store, inv): self._store = store self._inventory = inv def get_file(self, file_id): ie = self._inventory[file_id] f = self._store[ie.text_id] mutter(" get fileid{%s} from %r" % (file_id, self)) self._check_retrieved(ie, f) return f def get_file_size(self, file_id): return self._inventory[file_id].text_size def get_file_sha1(self, file_id): ie = self._inventory[file_id] return ie.text_sha1 def has_filename(self, filename): return bool(self.inventory.path2id(filename)) def list_files(self): # The only files returned by this are those from the version for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id class EmptyTree(Tree): def __init__(self): self._inventory = Inventory() def has_filename(self, filename): return False def list_files(self): if False: # just to make it a generator yield None ###################################################################### # diff # TODO: Merge these two functions into a single one that can operate # on either a whole tree or a set of files. # TODO: Return the diff in order by filename, not by category or in # random order. Can probably be done by lock-stepping through the # filenames from both trees. def file_status(filename, old_tree, new_tree): """Return single-letter status, old and new names for a file. The complexity here is in deciding how to represent renames; many complex cases are possible. """ old_inv = old_tree.inventory new_inv = new_tree.inventory new_id = new_inv.path2id(filename) old_id = old_inv.path2id(filename) if not new_id and not old_id: # easy: doesn't exist in either; not versioned at all if new_tree.is_ignored(filename): return 'I', None, None else: return '?', None, None elif new_id: # There is now a file of this name, great. pass else: # There is no longer a file of this name, but we can describe # what happened to the file that used to have # this name. There are two possibilities: either it was # deleted entirely, or renamed. assert old_id if new_inv.has_id(old_id): return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id) else: return 'D', old_inv.id2path(old_id), None # if the file_id is new in this revision, it is added if new_id and not old_inv.has_id(new_id): return 'A' # if there used to be a file of this name, but that ID has now # disappeared, it is deleted if old_id and not new_inv.has_id(old_id): return 'D' return 'wtf?' commit refs/heads/master mark :133 committer 1112065354 +1000 data 23 - new 'ignored' command from :132 M 644 inline NEWS data 1185 bzr-0.0.2 "black cube" NOT RELEASED YET ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list is memoized after first request. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command shows which files are ignored and why. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 23446 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] --revision REV Show changes since REV, rather than predecessor. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Diff selected files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(verbose=True): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = cmdfn.__doc__ if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'commit': [], 'diff': [], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'relpath': ['filename'], 'remove': ['file+'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :134 committer 1112073541 +1000 data 3 Doc from :133 M 644 inline bzrlib/tree.py data 12932 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tree classes, representing directory at point in time. """ from sets import Set import os.path, os, fnmatch from inventory import Inventory from trace import mutter, note from osutils import pumpfile, compare_files, filesize, quotefn, sha_file, \ joinpath, splitpath, appendpath, isdir, isfile, file_kind, fingerprint_file from errors import bailout import branch from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE import bzrlib class Tree: """Abstract file tree. There are several subclasses: * `WorkingTree` exists as files on disk editable by the user. * `RevisionTree` is a tree as recorded at some point in the past. * `EmptyTree` Trees contain an `Inventory` object, and also know how to retrieve file texts mentioned in the inventory, either from a working directory or from a store. It is possible for trees to contain files that are not described in their inventory or vice versa; for this use `filenames()`. Trees can be compared, etc, regardless of whether they are working trees or versioned trees. """ def has_filename(self, filename): """True if the tree has given filename.""" raise NotImplementedError() def has_id(self, file_id): return self.inventory.has_id(file_id) def id_set(self): """Return set of all ids in this tree.""" return self.inventory.id_set() def id2path(self, file_id): return self.inventory.id2path(file_id) def _get_inventory(self): return self._inventory inventory = property(_get_inventory, doc="Inventory of this Tree") def _check_retrieved(self, ie, f): fp = fingerprint_file(f) f.seek(0) if ie.text_size is not None: if ie.text_size != fp['size']: bailout("mismatched size for file %r in %r" % (ie.file_id, self._store), ["inventory expects %d bytes" % ie.text_size, "file is actually %d bytes" % fp['size'], "store is probably damaged/corrupt"]) if ie.text_sha1 != fp['sha1']: bailout("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store), ["inventory expects %s" % ie.text_sha1, "file is actually %s" % fp['sha1'], "store is probably damaged/corrupt"]) def export(self, dest): """Export this tree to a new directory. `dest` should not exist, and will be created holding the contents of this tree. :todo: To handle subdirectories we need to create the directories first. :note: If the export fails, the destination directory will be left in a half-assed state. """ os.mkdir(dest) mutter('export version %r' % self) inv = self.inventory for dp, ie in inv.iter_entries(): kind = ie.kind fullpath = appendpath(dest, dp) if kind == 'directory': os.mkdir(fullpath) elif kind == 'file': pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb')) else: bailout("don't know how to export {%s} of kind %r", fid, kind) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) class WorkingTree(Tree): """Working copy tree. The inventory is held in the `Branch` working-inventory, and the files are in a directory on disk. It is possible for a `WorkingTree` to have a filename which is not listed in the Inventory and vice versa. """ def __init__(self, basedir, inv): self._inventory = inv self.basedir = basedir self.path2id = inv.path2id def __repr__(self): return "<%s of %s>" % (self.__class__.__name__, self.basedir) def abspath(self, filename): return os.path.join(self.basedir, filename) def has_filename(self, filename): return os.path.exists(self.abspath(filename)) def get_file(self, file_id): return self.get_file_byname(self.id2path(file_id)) def get_file_byname(self, filename): return file(self.abspath(filename), 'rb') def _get_store_filename(self, file_id): return self.abspath(self.id2path(file_id)) def has_id(self, file_id): # files that have been deleted are excluded if not self.inventory.has_id(file_id): return False return os.access(self.abspath(self.inventory.id2path(file_id)), os.F_OK) def get_file_size(self, file_id): return os.stat(self._get_store_filename(file_id))[ST_SIZE] def get_file_sha1(self, file_id): f = self.get_file(file_id) return sha_file(f) def file_class(self, filename): if self.path2id(filename): return 'V' elif self.is_ignored(filename): return 'I' else: return '?' def file_kind(self, filename): if isfile(self.abspath(filename)): return 'file' elif isdir(self.abspath(filename)): return 'directory' else: return 'unknown' def list_files(self): """Recursively list all files as (path, class, kind, id). Lists, but does not descend into unversioned directories. This does not include files that have been deleted in this tree. Skips the control directory. """ inv = self.inventory def descend(from_dir, from_dir_id, dp): ls = os.listdir(dp) ls.sort() for f in ls: ## TODO: If we find a subdirectory with its own .bzr ## directory, then that is a separate tree and we ## should exclude it. if bzrlib.BZRDIR == f: continue # path within tree fp = appendpath(from_dir, f) # absolute path fap = appendpath(dp, f) f_ie = inv.get_child(from_dir_id, f) if f_ie: c = 'V' elif self.is_ignored(fp): c = 'I' else: c = '?' fk = file_kind(fap) if f_ie: if f_ie.kind != fk: bailout("file %r entered as kind %r id %r, now of kind %r" % (fap, f_ie.kind, f_ie.file_id, fk)) yield fp, c, fk, (f_ie and f_ie.file_id) if fk != 'directory': continue if c != 'V': # don't descend unversioned directories continue for ff in descend(fp, f_ie.file_id, fap): yield ff for f in descend('', None, self.basedir): yield f def unknowns(self, path='', dir_id=None): """Yield names of unknown files in this WorkingTree. If there are any unknown directories then only the directory is returned, not all its children. But if there are unknown files under a versioned subdirectory, they are returned. Currently returned depth-first, sorted by name within directories. """ for fpath, fclass, fkind, fid in self.list_files(): if fclass == '?': yield fpath def ignored_files(self): for fpath, fclass, fkind, fid in self.list_files(): if fclass == 'I': yield fpath def get_ignore_list(self): """Return list of ignore patterns. Cached in the Tree object after the first call. """ if hasattr(self, '_ignorelist'): return self._ignorelist l = bzrlib.DEFAULT_IGNORE[:] if self.has_filename(bzrlib.IGNORE_FILENAME): f = self.get_file_byname(bzrlib.IGNORE_FILENAME) l.extend([line.rstrip("\n\r") for line in f.readlines()]) self._ignorelist = l return l def is_ignored(self, filename): """Check whether the filename matches an ignore pattern. Patterns containing '/' need to match the whole path; others match against only the last component. If the file is ignored, returns the pattern which caused it to be ignored, otherwise None. So this can simply be used as a boolean if desired.""" ## TODO: Use '**' to match directories, and other extended globbing stuff from cvs/rsync. for pat in self.get_ignore_list(): if '/' in pat: if fnmatch.fnmatchcase(filename, pat): return pat else: if fnmatch.fnmatchcase(splitpath(filename)[-1], pat): return pat return None class RevisionTree(Tree): """Tree viewing a previous revision. File text can be retrieved from the text store. :todo: Some kind of `__repr__` method, but a good one probably means knowing the branch and revision number, or at least passing a description to the constructor. """ def __init__(self, store, inv): self._store = store self._inventory = inv def get_file(self, file_id): ie = self._inventory[file_id] f = self._store[ie.text_id] mutter(" get fileid{%s} from %r" % (file_id, self)) self._check_retrieved(ie, f) return f def get_file_size(self, file_id): return self._inventory[file_id].text_size def get_file_sha1(self, file_id): ie = self._inventory[file_id] return ie.text_sha1 def has_filename(self, filename): return bool(self.inventory.path2id(filename)) def list_files(self): # The only files returned by this are those from the version for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id class EmptyTree(Tree): def __init__(self): self._inventory = Inventory() def has_filename(self, filename): return False def list_files(self): if False: # just to make it a generator yield None ###################################################################### # diff # TODO: Merge these two functions into a single one that can operate # on either a whole tree or a set of files. # TODO: Return the diff in order by filename, not by category or in # random order. Can probably be done by lock-stepping through the # filenames from both trees. def file_status(filename, old_tree, new_tree): """Return single-letter status, old and new names for a file. The complexity here is in deciding how to represent renames; many complex cases are possible. """ old_inv = old_tree.inventory new_inv = new_tree.inventory new_id = new_inv.path2id(filename) old_id = old_inv.path2id(filename) if not new_id and not old_id: # easy: doesn't exist in either; not versioned at all if new_tree.is_ignored(filename): return 'I', None, None else: return '?', None, None elif new_id: # There is now a file of this name, great. pass else: # There is no longer a file of this name, but we can describe # what happened to the file that used to have # this name. There are two possibilities: either it was # deleted entirely, or renamed. assert old_id if new_inv.has_id(old_id): return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id) else: return 'D', old_inv.id2path(old_id), None # if the file_id is new in this revision, it is added if new_id and not old_inv.has_id(new_id): return 'A' # if there used to be a file of this name, but that ID has now # disappeared, it is deleted if old_id and not new_inv.has_id(old_id): return 'D' return 'wtf?' commit refs/heads/master mark :135 committer 1112076134 +1000 data 28 Simple new 'deleted' command from :134 M 644 inline NEWS data 1283 bzr-0.0.2 "black cube" NOT RELEASED YET ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list is memoized after first request. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 23791 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] --revision REV Show changes since REV, rather than predecessor. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Diff selected files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): print path def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(verbose=True): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = cmdfn.__doc__ if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['show-ids', 'timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'commit': [], 'diff': [], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'relpath': ['filename'], 'remove': ['file+'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) mutter(" option argument %r" % opts[optname]) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) cmdargs.update(opts) ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :136 committer 1112076625 +1000 data 43 new --show-ids option for 'deleted' command from :135 M 644 inline bzrlib/commands.py data 23936 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] --revision REV Show changes since REV, rather than predecessor. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Diff selected files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(verbose=True): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = cmdfn.__doc__ if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'commit': [], 'diff': [], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'relpath': ['filename'], 'remove': ['file+'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # TODO: special --profile option to turn on the Python profiler # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v ret = cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :137 committer 1112077515 +1000 data 20 new --profile option from :136 M 644 inline NEWS data 1323 bzr-0.0.2 "black cube" NOT RELEASED YET ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list is memoized after first request. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 24452 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] --revision REV Show changes since REV, rather than predecessor. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Diff selected files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(verbose=True): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = cmdfn.__doc__ if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'commit': [], 'diff': [], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'relpath': ['filename'], 'remove': ['file+'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot prof = hotshot.Profile('.bzr.profile') ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load('.bzr.profile') #stats.strip_dirs() stats.sort_stats('cumulative', 'calls') stats.print_stats(20) else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :138 committer 1112079491 +1000 data 79 remove parallel tree from inventory; store children directly in InventoryEntry from :137 M 644 inline bzrlib/inventory.py data 15214 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Inventories map files to their name in a revision.""" # TODO: Maybe store inventory_id in the file? Not really needed. __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os.path, types from sets import Set try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from xml import XMLMixin from errors import bailout import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') >>> i.add(InventoryEntry('123', 'src', kind='directory')) >>> i.add(InventoryEntry('2323', 'hello.c', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id=None)) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', parent_id='123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', parent_id='123')) >>> i.add(InventoryEntry('2325', 'wibble', parent_id='123', kind='directory')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', parent_id='2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' :todo: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ def __init__(self, file_id, name, kind='file', text_id=None, parent_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c') >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c') Traceback (most recent call last): BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", []) """ if len(splitpath(name)) != 1: bailout('InventoryEntry name is not a simple filename: %r' % name) self.file_id = file_id self.name = name assert kind in ['file', 'directory'] self.kind = kind self.text_id = text_id self.parent_id = parent_id self.text_sha1 = None self.text_size = None if kind == 'directory': self.children = {} def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.text_id, self.parent_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size is not None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1', 'parent_id']: v = getattr(self, f) if v is not None: e.set(f, v) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind')) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') self.parent_id = elt.get('parent_id') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __cmp__(self, other): if self is other: return 0 if not isinstance(other, InventoryEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.name, other.name) \ or cmp(self.text_sha1, other.text_sha1) \ or cmp(self.text_size, other.text_size) \ or cmp(self.text_id, other.text_id) \ or cmp(self.parent_id, other.parent_id) \ or cmp(self.kind, other.kind) class Inventory(XMLMixin): """Inventory of versioned files in a tree. An Inventory acts like a set of InventoryEntry items. You can also look files up by their file_id or name. May be read from and written to a metadata file in a tree. To manipulate the inventory (for example to add a file), it is read in, modified, and then written back out. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c')) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ ## TODO: Make sure only canonical filenames are stored. ## TODO: Do something sensible about the possible collisions on ## case-losing filesystems. Perhaps we should just always forbid ## such collisions. ## TODO: No special cases for root, rather just give it a file id ## like everything else. ## TODO: Probably change XML serialization to use nesting def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. """ self._root = InventoryEntry(None, '', kind='directory') self._byid = {None: self._root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, parent_id=None): """Return (path, entry) pairs, in order by name.""" kids = self[parent_id].children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(parent_id=ie.file_id): yield joinpath([name, cn]), cie def directories(self, include_root=True): """Return (path, entry) pairs for all directories. """ if include_root: yield '', None for path, entry in self.iter_entries(): if entry.kind == 'directory': yield path, entry def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c')) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c')) >>> inv['123123'].name 'hello.c' """ return self._byid[file_id] def get_child(self, parent_id, filename): if parent_id == None: return self._root.children.get(filename) else: return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self: bailout("inventory already contains entry with id {%s}" % entry.file_id) if entry.parent_id != None: if entry.parent_id not in self: bailout("parent_id %s of new entry not found in inventory" % entry.parent_id) # TODO: parent must be a directory if self[entry.parent_id].children.has_key(entry.name): bailout("%s is already versioned" % appendpath(self.id2path(entry.parent_id), entry.name)) self._byid[entry.file_id] = entry self[entry.parent_id].children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: bailout("cannot re-add root of inventory") if file_id is None: file_id = bzrlib.branch.gen_file_id(relpath) parent_id = self.path2id(parts[:-1]) ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c')) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def id_set(self): return Set(self._byid) def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c')) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __cmp__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo')) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo')) >>> i1 == i2 True """ if self is other: return 0 if not isinstance(other, Inventory): return NotImplemented if self.id_set() ^ other.id_set(): return 1 for file_id in self._byid: c = cmp(self[file_id], other[file_id]) if c: return c return 0 def id2path(self, file_id): """Return as a list the path to file_id.""" p = [] while file_id != None: ie = self[file_id] p = [ie.name] + p file_id = ie.parent_id return joinpath(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. """ if isinstance(name, types.StringTypes): name = splitpath(name) parent = self[None] for f in name: try: cie = parent.children[f] assert cie.name == f parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): assert isinstance(file_id, str) return self._byid.has_key(file_id) if __name__ == '__main__': import doctest, inventory doctest.testmod(inventory) M 644 inline notes/performance.txt data 8698 For a tree holding 2.4.18 (two copies), 2.4.19, 2.4.20 With gzip -9: mbp@hope% du .bzr 195110 .bzr/text-store 20 .bzr/revision-store 12355 .bzr/inventory-store 216325 .bzr mbp@hope% du -s . 523128 . Without gzip: This is actually a pretty bad example because of deleting and re-importing 2.4.18, but still not totally unreasonable. ---- linux-2.4.0: 116399 kB after addding everything: 119505kB bzr status 2.68s user 0.13s system 84% cpu 3.330 total bzr commit 'import 2.4.0' 4.41s user 2.15s system 11% cpu 59.490 total 242446 . 122068 .bzr ---- Performance (2005-03-01) To add all files from linux-2.4.18: about 70s, mostly inventory serialization/deserialization. To commit: - finished, 6.520u/3.870s cpu, 33.940u/10.730s cum - 134.040 elapsed Interesting that it spends so long on external processing! I wonder if this is for running uuidgen? Let's try generating things internally. Great, this cuts it to 17.15s user 0.61s system 83% cpu 21.365 total to add, with no external command time. The commit now seems to spend most of its time copying to disk. - finished, 6.550u/3.320s cpu, 35.050u/9.870s cum - 89.650 elapsed I wonder where the external time is now? We were also using uuids() for revisions. Let's remove everything and re-add. Detecting everything was removed takes - finished, 2.460u/0.110s cpu, 0.000u/0.000s cum - 3.430 elapsed which may be mostly XML deserialization? Just getting the previous revision takes about this long: bzr invoked at Tue 2005-03-01 15:53:05.183741 EST +1100 by mbp@sourcefrog.net on hope arguments: ['/home/mbp/bin/bzr', 'get-revision-inventory', 'mbp@sourcefrog.net-20050301044608-8513202ab179aff4-44e8cd52a41aa705'] platform: Linux-2.6.10-4-686-i686-with-debian-3.1 - finished, 3.910u/0.390s cpu, 0.000u/0.000s cum - 6.690 elapsed Now committing the revision which removes all files should be fast. - finished, 1.280u/0.030s cpu, 0.000u/0.000s cum - 1.320 elapsed Now re-add with new code that doesn't call uuidgen: - finished, 1.990u/0.030s cpu, 0.000u/0.000s cum - 2.040 elapsed 16.61s user 0.55s system 74% cpu 22.965 total Status:: - finished, 2.500u/0.110s cpu, 0.010u/0.000s cum - 3.350 elapsed And commit:: Now patch up to 2.4.19. There were some bugs in handling missing directories, but with that fixed we do much better:: bzr status 5.86s user 1.06s system 10% cpu 1:05.55 total This is slow because it's diffing every file; we should use mtimes etc to make this faster. The cpu time is reasonable. I see difflib is pure Python; it might be faster to shell out to GNU diff when we need it. Export is very fast:: - finished, 4.220u/1.480s cpu, 0.010u/0.000s cum - 10.810 elapsed bzr export 1 ../linux-2.4.18.export1 3.92s user 1.72s system 21% cpu 26.030 total Now to find and add the new changes:: - finished, 2.190u/0.030s cpu, 0.000u/0.000s cum - 2.300 elapsed :: bzr commit 'import 2.4.19' 9.36s user 1.91s system 23% cpu 47.127 total And the result is exactly right. Try exporting:: mbp@hope% bzr export 4 ../linux-2.4.19.export4 bzr export 4 ../linux-2.4.19.export4 4.21s user 1.70s system 18% cpu 32.304 total and the export is exactly the same as the tarball. Now we can optimize the diff a bit more by not comparing files that have the right SHA-1 from within the commit For comparison:: patch -p1 < ../kernel.pkg/patch-2.4.20 1.61s user 1.03s system 13% cpu 19.106 total Now status after applying the .20 patch. With full-text verification:: bzr status 7.07s user 1.32s system 13% cpu 1:04.29 total with that turned off:: bzr status 5.86s user 0.56s system 25% cpu 25.577 total After adding: bzr status 6.14s user 0.61s system 25% cpu 26.583 total Should add some kind of profile counter for quick compares vs slow compares. bzr commit 'import 2.4.20' 7.57s user 1.36s system 20% cpu 43.568 total export: finished, 3.940u/1.820s cpu, 0.000u/0.000s cum, 50.990 elapsed also exports correctly now .21 bzr commit 'import 2.4.1' 5.59s user 0.51s system 60% cpu 10.122 total 265520 . 137704 .bzr import 2.4.2 317758 . 183463 .bzr with everything through to 2.4.29 imported, the .bzr directory is 1132MB, compared to 185MB for one tree. The .bzr.log is 100MB!. So the storage is 6.1 times larger, although we're holding 30 versions. It's pretty large but I think not ridiculous. By contrast the tarball for 2.4.0 is 104MB, and the tarball plus uncompressed patches are 315MB. Uncompressed, the text store is 1041MB. So it is only three times worse than patches, and could be compressed at presumably roughly equal efficiency. It is large, but also a very simple design and perhaps adequate for the moment. The text store with each file individually gziped is 264MB, which is also a very simple format and makes it less than twice the size of the source tree. This is actually rather pessimistic because I think there are some orphaned texts in there. Measured by du, the compressed full-text store is 363MB; also probably tolerable. The real fix is perhaps to use some kind of weave, not so much for storage efficiency as for fast annotation and therefore possible annotation-based merge. ----- 2005-03-25 Now we have recursive add, add is much faster. Adding all of the linux 2.4.19 kernel tree takes only finished, 5.460u/0.610s cpu, 0.010u/0.000s cum, 6.710 elapsed However, the store code currently flushes to disk after every write, which is probably excessive. So a commit takes finished, 8.740u/3.950s cpu, 0.010u/0.000s cum, 156.420 elapsed Status is now also quite fast, depsite that it still has to read all the working copies: mbp@hope% bzr status ~/work/linux-2.4.19 bzr status 5.51s user 0.79s system 99% cpu 6.337 total strace shows much of this is in write(2), probably because of logging. With more buffering on that file, removing all the explicit flushes, that is reduced to mbp@hope% time bzr status bzr status 5.23s user 0.42s system 97% cpu 5.780 total which is mostly opening, stating and reading files, as it should be. Still a few too many stat calls. Now fixed up handling of root directory. Without flushing everything to disk as it goes into the store: mbp@hope% bzr commit -m 'import linux 2.4.19' bzr commit -m 'import linux 2.4.19' 8.15s user 2.09s system 53% cpu 19.295 total mbp@hope% time bzr diff bzr diff 5.80s user 0.52s system 69% cpu 9.128 total mbp@hope% time bzr status bzr status 5.64s user 0.43s system 68% cpu 8.848 total patch -p1 < ../linux.pkg/patch-2.4.20 1.67s user 0.96s system 90% cpu 2.905 total The diff changes 3462 files according to diffstat. branch format: Bazaar-NG branch, format 0.0.4 in the working tree: 8674 unchanged 2463 modified 818 added 229 removed 0 renamed 0 unknown 4 ignored 614 versioned subdirectories That is, 3510 entries have changed, but there are 48 changed directories so the count is exactly right! bzr commit -v -m 'import 2.4.20' 8.23s user 1.09s system 48% cpu 19.411 total Kind of strange that this takes as much time as committing the whole thing; I suppose it has to read every file. This shows many files as being renamed; I don't know why that would be. Patch to 2.4.21: 2969 files changed, 366643 insertions(+), 147759 deletions(-) After auto-add: 2969 files changed, 372168 insertions(+), 153284 deletions(-) I wonder why it is not exactly the same? Maybe because the python diff algorithm is a bit differnt to GNU diff. ---- 2005-03-29 full check, retrieving all file texts once for the 2.4 kernel branch takes 10m elapsed, 1m cpu time. lots of random IO and seeking. ---- mbp@hope% time python =bzr deleted --show-ids README README-fa1d8447b4fd0140-adbf4342752f0fc3 python =bzr deleted --show-ids 1.55s user 0.09s system 96% cpu 1.701 total mbp@hope% time python -O =bzr deleted --show-ids README README-fa1d8447b4fd0140-adbf4342752f0fc3 python -O =bzr deleted --show-ids 1.47s user 0.10s system 101% cpu 1.547 total mbp@hope% time python -O =bzr deleted --show-ids README README-fa1d8447b4fd0140-adbf4342752f0fc3 python -O =bzr deleted --show-ids 1.49s user 0.07s system 99% cpu 1.565 total mbp@hope% time python =bzr deleted --show-ids README README-fa1d8447b4fd0140-adbf4342752f0fc3 python =bzr deleted --show-ids 1.55s user 0.08s system 99% cpu 1.637 total small but significant improvement from Python -O commit refs/heads/master mark :139 committer 1112080429 +1000 data 31 simplified/faster Inventory.add from :138 M 644 inline bzrlib/inventory.py data 15054 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Inventories map files to their name in a revision.""" # TODO: Maybe store inventory_id in the file? Not really needed. __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os.path, types from sets import Set try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from xml import XMLMixin from errors import bailout import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') >>> i.add(InventoryEntry('123', 'src', kind='directory')) >>> i.add(InventoryEntry('2323', 'hello.c', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id=None)) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', parent_id='123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', parent_id='123')) >>> i.add(InventoryEntry('2325', 'wibble', parent_id='123', kind='directory')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', parent_id='2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' :todo: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ def __init__(self, file_id, name, kind='file', text_id=None, parent_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c') >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c') Traceback (most recent call last): BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", []) """ if len(splitpath(name)) != 1: bailout('InventoryEntry name is not a simple filename: %r' % name) self.file_id = file_id self.name = name assert kind in ['file', 'directory'] self.kind = kind self.text_id = text_id self.parent_id = parent_id self.text_sha1 = None self.text_size = None if kind == 'directory': self.children = {} def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.text_id, self.parent_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size is not None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1', 'parent_id']: v = getattr(self, f) if v is not None: e.set(f, v) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind')) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') self.parent_id = elt.get('parent_id') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __cmp__(self, other): if self is other: return 0 if not isinstance(other, InventoryEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.name, other.name) \ or cmp(self.text_sha1, other.text_sha1) \ or cmp(self.text_size, other.text_size) \ or cmp(self.text_id, other.text_id) \ or cmp(self.parent_id, other.parent_id) \ or cmp(self.kind, other.kind) class Inventory(XMLMixin): """Inventory of versioned files in a tree. An Inventory acts like a set of InventoryEntry items. You can also look files up by their file_id or name. May be read from and written to a metadata file in a tree. To manipulate the inventory (for example to add a file), it is read in, modified, and then written back out. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c')) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ ## TODO: Make sure only canonical filenames are stored. ## TODO: Do something sensible about the possible collisions on ## case-losing filesystems. Perhaps we should just always forbid ## such collisions. ## TODO: No special cases for root, rather just give it a file id ## like everything else. ## TODO: Probably change XML serialization to use nesting def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. """ self._root = InventoryEntry(None, '', kind='directory') self._byid = {None: self._root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, parent_id=None): """Return (path, entry) pairs, in order by name.""" kids = self[parent_id].children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(parent_id=ie.file_id): yield joinpath([name, cn]), cie def directories(self, include_root=True): """Return (path, entry) pairs for all directories. """ if include_root: yield '', None for path, entry in self.iter_entries(): if entry.kind == 'directory': yield path, entry def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c')) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c')) >>> inv['123123'].name 'hello.c' """ return self._byid[file_id] def get_child(self, parent_id, filename): if parent_id == None: return self._root.children.get(filename) else: return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self._byid: bailout("inventory already contains entry with id {%s}" % entry.file_id) parent = self._byid[entry.parent_id] if parent.kind != 'directory': bailout("attempt to add under non-directory {%s}" % parent.file_id) if parent.children.has_key(entry.name): bailout("{%s} already has child %r" % (parent.file_id, entry.name)) self._byid[entry.file_id] = entry parent.children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: bailout("cannot re-add root of inventory") if file_id is None: file_id = bzrlib.branch.gen_file_id(relpath) parent_id = self.path2id(parts[:-1]) ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c')) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def id_set(self): return Set(self._byid) def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c')) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __cmp__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo')) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo')) >>> i1 == i2 True """ if self is other: return 0 if not isinstance(other, Inventory): return NotImplemented if self.id_set() ^ other.id_set(): return 1 for file_id in self._byid: c = cmp(self[file_id], other[file_id]) if c: return c return 0 def id2path(self, file_id): """Return as a list the path to file_id.""" p = [] while file_id != None: ie = self[file_id] p = [ie.name] + p file_id = ie.parent_id return joinpath(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. """ if isinstance(name, types.StringTypes): name = splitpath(name) parent = self[None] for f in name: try: cie = parent.children[f] assert cie.name == f parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): assert isinstance(file_id, str) return self._byid.has_key(file_id) if __name__ == '__main__': import doctest, inventory doctest.testmod(inventory) commit refs/heads/master mark :140 committer 1112080534 +1000 data 34 fix error message for repeated add from :139 M 644 inline bzrlib/inventory.py data 15096 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Inventories map files to their name in a revision.""" # TODO: Maybe store inventory_id in the file? Not really needed. __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os.path, types from sets import Set try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from xml import XMLMixin from errors import bailout import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') >>> i.add(InventoryEntry('123', 'src', kind='directory')) >>> i.add(InventoryEntry('2323', 'hello.c', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id=None)) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', parent_id='123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', parent_id='123')) >>> i.add(InventoryEntry('2325', 'wibble', parent_id='123', kind='directory')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', parent_id='2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' :todo: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ def __init__(self, file_id, name, kind='file', text_id=None, parent_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c') >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c') Traceback (most recent call last): BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", []) """ if len(splitpath(name)) != 1: bailout('InventoryEntry name is not a simple filename: %r' % name) self.file_id = file_id self.name = name assert kind in ['file', 'directory'] self.kind = kind self.text_id = text_id self.parent_id = parent_id self.text_sha1 = None self.text_size = None if kind == 'directory': self.children = {} def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.text_id, self.parent_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size is not None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1', 'parent_id']: v = getattr(self, f) if v is not None: e.set(f, v) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind')) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') self.parent_id = elt.get('parent_id') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __cmp__(self, other): if self is other: return 0 if not isinstance(other, InventoryEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.name, other.name) \ or cmp(self.text_sha1, other.text_sha1) \ or cmp(self.text_size, other.text_size) \ or cmp(self.text_id, other.text_id) \ or cmp(self.parent_id, other.parent_id) \ or cmp(self.kind, other.kind) class Inventory(XMLMixin): """Inventory of versioned files in a tree. An Inventory acts like a set of InventoryEntry items. You can also look files up by their file_id or name. May be read from and written to a metadata file in a tree. To manipulate the inventory (for example to add a file), it is read in, modified, and then written back out. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c')) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ ## TODO: Make sure only canonical filenames are stored. ## TODO: Do something sensible about the possible collisions on ## case-losing filesystems. Perhaps we should just always forbid ## such collisions. ## TODO: No special cases for root, rather just give it a file id ## like everything else. ## TODO: Probably change XML serialization to use nesting def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. """ self._root = InventoryEntry(None, '', kind='directory') self._byid = {None: self._root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, parent_id=None): """Return (path, entry) pairs, in order by name.""" kids = self[parent_id].children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(parent_id=ie.file_id): yield joinpath([name, cn]), cie def directories(self, include_root=True): """Return (path, entry) pairs for all directories. """ if include_root: yield '', None for path, entry in self.iter_entries(): if entry.kind == 'directory': yield path, entry def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c')) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c')) >>> inv['123123'].name 'hello.c' """ return self._byid[file_id] def get_child(self, parent_id, filename): if parent_id == None: return self._root.children.get(filename) else: return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self._byid: bailout("inventory already contains entry with id {%s}" % entry.file_id) parent = self._byid[entry.parent_id] if parent.kind != 'directory': bailout("attempt to add under non-directory {%s}" % parent.file_id) if parent.children.has_key(entry.name): bailout("%s is already versioned" % appendpath(self.id2path(parent.file_id), entry.name)) self._byid[entry.file_id] = entry parent.children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: bailout("cannot re-add root of inventory") if file_id is None: file_id = bzrlib.branch.gen_file_id(relpath) parent_id = self.path2id(parts[:-1]) ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c')) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def id_set(self): return Set(self._byid) def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c')) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __cmp__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo')) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo')) >>> i1 == i2 True """ if self is other: return 0 if not isinstance(other, Inventory): return NotImplemented if self.id_set() ^ other.id_set(): return 1 for file_id in self._byid: c = cmp(self[file_id], other[file_id]) if c: return c return 0 def id2path(self, file_id): """Return as a list the path to file_id.""" p = [] while file_id != None: ie = self[file_id] p = [ie.name] + p file_id = ie.parent_id return joinpath(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. """ if isinstance(name, types.StringTypes): name = splitpath(name) parent = self[None] for f in name: try: cie = parent.children[f] assert cie.name == f parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): assert isinstance(file_id, str) return self._byid.has_key(file_id) if __name__ == '__main__': import doctest, inventory doctest.testmod(inventory) commit refs/heads/master mark :141 committer 1112080579 +1000 data 32 delete obsolete Tree.file_kind() from :140 M 644 inline bzrlib/tree.py data 12708 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tree classes, representing directory at point in time. """ from sets import Set import os.path, os, fnmatch from inventory import Inventory from trace import mutter, note from osutils import pumpfile, compare_files, filesize, quotefn, sha_file, \ joinpath, splitpath, appendpath, isdir, isfile, file_kind, fingerprint_file from errors import bailout import branch from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE import bzrlib class Tree: """Abstract file tree. There are several subclasses: * `WorkingTree` exists as files on disk editable by the user. * `RevisionTree` is a tree as recorded at some point in the past. * `EmptyTree` Trees contain an `Inventory` object, and also know how to retrieve file texts mentioned in the inventory, either from a working directory or from a store. It is possible for trees to contain files that are not described in their inventory or vice versa; for this use `filenames()`. Trees can be compared, etc, regardless of whether they are working trees or versioned trees. """ def has_filename(self, filename): """True if the tree has given filename.""" raise NotImplementedError() def has_id(self, file_id): return self.inventory.has_id(file_id) def id_set(self): """Return set of all ids in this tree.""" return self.inventory.id_set() def id2path(self, file_id): return self.inventory.id2path(file_id) def _get_inventory(self): return self._inventory inventory = property(_get_inventory, doc="Inventory of this Tree") def _check_retrieved(self, ie, f): fp = fingerprint_file(f) f.seek(0) if ie.text_size is not None: if ie.text_size != fp['size']: bailout("mismatched size for file %r in %r" % (ie.file_id, self._store), ["inventory expects %d bytes" % ie.text_size, "file is actually %d bytes" % fp['size'], "store is probably damaged/corrupt"]) if ie.text_sha1 != fp['sha1']: bailout("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store), ["inventory expects %s" % ie.text_sha1, "file is actually %s" % fp['sha1'], "store is probably damaged/corrupt"]) def export(self, dest): """Export this tree to a new directory. `dest` should not exist, and will be created holding the contents of this tree. :todo: To handle subdirectories we need to create the directories first. :note: If the export fails, the destination directory will be left in a half-assed state. """ os.mkdir(dest) mutter('export version %r' % self) inv = self.inventory for dp, ie in inv.iter_entries(): kind = ie.kind fullpath = appendpath(dest, dp) if kind == 'directory': os.mkdir(fullpath) elif kind == 'file': pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb')) else: bailout("don't know how to export {%s} of kind %r", fid, kind) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) class WorkingTree(Tree): """Working copy tree. The inventory is held in the `Branch` working-inventory, and the files are in a directory on disk. It is possible for a `WorkingTree` to have a filename which is not listed in the Inventory and vice versa. """ def __init__(self, basedir, inv): self._inventory = inv self.basedir = basedir self.path2id = inv.path2id def __repr__(self): return "<%s of %s>" % (self.__class__.__name__, self.basedir) def abspath(self, filename): return os.path.join(self.basedir, filename) def has_filename(self, filename): return os.path.exists(self.abspath(filename)) def get_file(self, file_id): return self.get_file_byname(self.id2path(file_id)) def get_file_byname(self, filename): return file(self.abspath(filename), 'rb') def _get_store_filename(self, file_id): return self.abspath(self.id2path(file_id)) def has_id(self, file_id): # files that have been deleted are excluded if not self.inventory.has_id(file_id): return False return os.access(self.abspath(self.inventory.id2path(file_id)), os.F_OK) def get_file_size(self, file_id): return os.stat(self._get_store_filename(file_id))[ST_SIZE] def get_file_sha1(self, file_id): f = self.get_file(file_id) return sha_file(f) def file_class(self, filename): if self.path2id(filename): return 'V' elif self.is_ignored(filename): return 'I' else: return '?' def list_files(self): """Recursively list all files as (path, class, kind, id). Lists, but does not descend into unversioned directories. This does not include files that have been deleted in this tree. Skips the control directory. """ inv = self.inventory def descend(from_dir, from_dir_id, dp): ls = os.listdir(dp) ls.sort() for f in ls: ## TODO: If we find a subdirectory with its own .bzr ## directory, then that is a separate tree and we ## should exclude it. if bzrlib.BZRDIR == f: continue # path within tree fp = appendpath(from_dir, f) # absolute path fap = appendpath(dp, f) f_ie = inv.get_child(from_dir_id, f) if f_ie: c = 'V' elif self.is_ignored(fp): c = 'I' else: c = '?' fk = file_kind(fap) if f_ie: if f_ie.kind != fk: bailout("file %r entered as kind %r id %r, now of kind %r" % (fap, f_ie.kind, f_ie.file_id, fk)) yield fp, c, fk, (f_ie and f_ie.file_id) if fk != 'directory': continue if c != 'V': # don't descend unversioned directories continue for ff in descend(fp, f_ie.file_id, fap): yield ff for f in descend('', None, self.basedir): yield f def unknowns(self, path='', dir_id=None): """Yield names of unknown files in this WorkingTree. If there are any unknown directories then only the directory is returned, not all its children. But if there are unknown files under a versioned subdirectory, they are returned. Currently returned depth-first, sorted by name within directories. """ for fpath, fclass, fkind, fid in self.list_files(): if fclass == '?': yield fpath def ignored_files(self): for fpath, fclass, fkind, fid in self.list_files(): if fclass == 'I': yield fpath def get_ignore_list(self): """Return list of ignore patterns. Cached in the Tree object after the first call. """ if hasattr(self, '_ignorelist'): return self._ignorelist l = bzrlib.DEFAULT_IGNORE[:] if self.has_filename(bzrlib.IGNORE_FILENAME): f = self.get_file_byname(bzrlib.IGNORE_FILENAME) l.extend([line.rstrip("\n\r") for line in f.readlines()]) self._ignorelist = l return l def is_ignored(self, filename): """Check whether the filename matches an ignore pattern. Patterns containing '/' need to match the whole path; others match against only the last component. If the file is ignored, returns the pattern which caused it to be ignored, otherwise None. So this can simply be used as a boolean if desired.""" ## TODO: Use '**' to match directories, and other extended globbing stuff from cvs/rsync. for pat in self.get_ignore_list(): if '/' in pat: if fnmatch.fnmatchcase(filename, pat): return pat else: if fnmatch.fnmatchcase(splitpath(filename)[-1], pat): return pat return None class RevisionTree(Tree): """Tree viewing a previous revision. File text can be retrieved from the text store. :todo: Some kind of `__repr__` method, but a good one probably means knowing the branch and revision number, or at least passing a description to the constructor. """ def __init__(self, store, inv): self._store = store self._inventory = inv def get_file(self, file_id): ie = self._inventory[file_id] f = self._store[ie.text_id] mutter(" get fileid{%s} from %r" % (file_id, self)) self._check_retrieved(ie, f) return f def get_file_size(self, file_id): return self._inventory[file_id].text_size def get_file_sha1(self, file_id): ie = self._inventory[file_id] return ie.text_sha1 def has_filename(self, filename): return bool(self.inventory.path2id(filename)) def list_files(self): # The only files returned by this are those from the version for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id class EmptyTree(Tree): def __init__(self): self._inventory = Inventory() def has_filename(self, filename): return False def list_files(self): if False: # just to make it a generator yield None ###################################################################### # diff # TODO: Merge these two functions into a single one that can operate # on either a whole tree or a set of files. # TODO: Return the diff in order by filename, not by category or in # random order. Can probably be done by lock-stepping through the # filenames from both trees. def file_status(filename, old_tree, new_tree): """Return single-letter status, old and new names for a file. The complexity here is in deciding how to represent renames; many complex cases are possible. """ old_inv = old_tree.inventory new_inv = new_tree.inventory new_id = new_inv.path2id(filename) old_id = old_inv.path2id(filename) if not new_id and not old_id: # easy: doesn't exist in either; not versioned at all if new_tree.is_ignored(filename): return 'I', None, None else: return '?', None, None elif new_id: # There is now a file of this name, great. pass else: # There is no longer a file of this name, but we can describe # what happened to the file that used to have # this name. There are two possibilities: either it was # deleted entirely, or renamed. assert old_id if new_inv.has_id(old_id): return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id) else: return 'D', old_inv.id2path(old_id), None # if the file_id is new in this revision, it is added if new_id and not old_inv.has_id(new_id): return 'A' # if there used to be a file of this name, but that ID has now # disappeared, it is deleted if old_id and not new_inv.has_id(old_id): return 'D' return 'wtf?' commit refs/heads/master mark :142 committer 1112081369 +1000 data 58 better "unknowns" based on just listing the relevant files from :141 M 644 inline bzrlib/commands.py data 24447 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] --revision REV Show changes since REV, rather than predecessor. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Diff selected files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(verbose=True): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = cmdfn.__doc__ if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'commit': [], 'diff': [], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'relpath': ['filename'], 'remove': ['file+'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot prof = hotshot.Profile('.bzr.profile') ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load('.bzr.profile') #stats.strip_dirs() stats.sort_stats('cumulative', 'calls') stats.print_stats(20) else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') M 644 inline bzrlib/inventory.py data 15054 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Inventories map files to their name in a revision.""" # TODO: Maybe store inventory_id in the file? Not really needed. __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os.path, types from sets import Set try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from xml import XMLMixin from errors import bailout import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') >>> i.add(InventoryEntry('123', 'src', kind='directory')) >>> i.add(InventoryEntry('2323', 'hello.c', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id=None)) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', parent_id='123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', parent_id='123')) >>> i.add(InventoryEntry('2325', 'wibble', parent_id='123', kind='directory')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', parent_id='2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' :todo: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ def __init__(self, file_id, name, kind='file', text_id=None, parent_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c') >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c') Traceback (most recent call last): BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", []) """ if len(splitpath(name)) != 1: bailout('InventoryEntry name is not a simple filename: %r' % name) self.file_id = file_id self.name = name assert kind in ['file', 'directory'] self.kind = kind self.text_id = text_id self.parent_id = parent_id self.text_sha1 = None self.text_size = None if kind == 'directory': self.children = {} def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.text_id, self.parent_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size is not None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1', 'parent_id']: v = getattr(self, f) if v is not None: e.set(f, v) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind')) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') self.parent_id = elt.get('parent_id') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __cmp__(self, other): if self is other: return 0 if not isinstance(other, InventoryEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.name, other.name) \ or cmp(self.text_sha1, other.text_sha1) \ or cmp(self.text_size, other.text_size) \ or cmp(self.text_id, other.text_id) \ or cmp(self.parent_id, other.parent_id) \ or cmp(self.kind, other.kind) class Inventory(XMLMixin): """Inventory of versioned files in a tree. An Inventory acts like a set of InventoryEntry items. You can also look files up by their file_id or name. May be read from and written to a metadata file in a tree. To manipulate the inventory (for example to add a file), it is read in, modified, and then written back out. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c')) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ ## TODO: Make sure only canonical filenames are stored. ## TODO: Do something sensible about the possible collisions on ## case-losing filesystems. Perhaps we should just always forbid ## such collisions. ## TODO: No special cases for root, rather just give it a file id ## like everything else. ## TODO: Probably change XML serialization to use nesting def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. """ self._root = InventoryEntry(None, '', kind='directory') self._byid = {None: self._root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, parent_id=None): """Return (path, entry) pairs, in order by name.""" kids = self[parent_id].children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(parent_id=ie.file_id): yield joinpath([name, cn]), cie def directories(self): """Return (path, entry) pairs for all directories. """ yield '', self._root for path, entry in self.iter_entries(): if entry.kind == 'directory': yield path, entry def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c')) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c')) >>> inv['123123'].name 'hello.c' """ return self._byid[file_id] def get_child(self, parent_id, filename): if parent_id == None: return self._root.children.get(filename) else: return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self._byid: bailout("inventory already contains entry with id {%s}" % entry.file_id) parent = self._byid[entry.parent_id] if parent.kind != 'directory': bailout("attempt to add under non-directory {%s}" % parent.file_id) if parent.children.has_key(entry.name): bailout("%s is already versioned" % appendpath(self.id2path(parent.file_id), entry.name)) self._byid[entry.file_id] = entry parent.children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: bailout("cannot re-add root of inventory") if file_id is None: file_id = bzrlib.branch.gen_file_id(relpath) parent_id = self.path2id(parts[:-1]) ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c')) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def id_set(self): return Set(self._byid) def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c')) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __cmp__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo')) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo')) >>> i1 == i2 True """ if self is other: return 0 if not isinstance(other, Inventory): return NotImplemented if self.id_set() ^ other.id_set(): return 1 for file_id in self._byid: c = cmp(self[file_id], other[file_id]) if c: return c return 0 def id2path(self, file_id): """Return as a list the path to file_id.""" p = [] while file_id != None: ie = self[file_id] p = [ie.name] + p file_id = ie.parent_id return joinpath(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. """ if isinstance(name, types.StringTypes): name = splitpath(name) parent = self[None] for f in name: try: cie = parent.children[f] assert cie.name == f parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): assert isinstance(file_id, str) return self._byid.has_key(file_id) if __name__ == '__main__': import doctest, inventory doctest.testmod(inventory) M 644 inline bzrlib/tree.py data 13283 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tree classes, representing directory at point in time. """ from sets import Set import os.path, os, fnmatch from inventory import Inventory from trace import mutter, note from osutils import pumpfile, compare_files, filesize, quotefn, sha_file, \ joinpath, splitpath, appendpath, isdir, isfile, file_kind, fingerprint_file from errors import bailout import branch from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE import bzrlib class Tree: """Abstract file tree. There are several subclasses: * `WorkingTree` exists as files on disk editable by the user. * `RevisionTree` is a tree as recorded at some point in the past. * `EmptyTree` Trees contain an `Inventory` object, and also know how to retrieve file texts mentioned in the inventory, either from a working directory or from a store. It is possible for trees to contain files that are not described in their inventory or vice versa; for this use `filenames()`. Trees can be compared, etc, regardless of whether they are working trees or versioned trees. """ def has_filename(self, filename): """True if the tree has given filename.""" raise NotImplementedError() def has_id(self, file_id): return self.inventory.has_id(file_id) def id_set(self): """Return set of all ids in this tree.""" return self.inventory.id_set() def id2path(self, file_id): return self.inventory.id2path(file_id) def _get_inventory(self): return self._inventory inventory = property(_get_inventory, doc="Inventory of this Tree") def _check_retrieved(self, ie, f): fp = fingerprint_file(f) f.seek(0) if ie.text_size is not None: if ie.text_size != fp['size']: bailout("mismatched size for file %r in %r" % (ie.file_id, self._store), ["inventory expects %d bytes" % ie.text_size, "file is actually %d bytes" % fp['size'], "store is probably damaged/corrupt"]) if ie.text_sha1 != fp['sha1']: bailout("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store), ["inventory expects %s" % ie.text_sha1, "file is actually %s" % fp['sha1'], "store is probably damaged/corrupt"]) def export(self, dest): """Export this tree to a new directory. `dest` should not exist, and will be created holding the contents of this tree. :todo: To handle subdirectories we need to create the directories first. :note: If the export fails, the destination directory will be left in a half-assed state. """ os.mkdir(dest) mutter('export version %r' % self) inv = self.inventory for dp, ie in inv.iter_entries(): kind = ie.kind fullpath = appendpath(dest, dp) if kind == 'directory': os.mkdir(fullpath) elif kind == 'file': pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb')) else: bailout("don't know how to export {%s} of kind %r", fid, kind) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) class WorkingTree(Tree): """Working copy tree. The inventory is held in the `Branch` working-inventory, and the files are in a directory on disk. It is possible for a `WorkingTree` to have a filename which is not listed in the Inventory and vice versa. """ def __init__(self, basedir, inv): self._inventory = inv self.basedir = basedir self.path2id = inv.path2id def __repr__(self): return "<%s of %s>" % (self.__class__.__name__, self.basedir) def abspath(self, filename): return os.path.join(self.basedir, filename) def has_filename(self, filename): return os.path.exists(self.abspath(filename)) def get_file(self, file_id): return self.get_file_byname(self.id2path(file_id)) def get_file_byname(self, filename): return file(self.abspath(filename), 'rb') def _get_store_filename(self, file_id): return self.abspath(self.id2path(file_id)) def has_id(self, file_id): # files that have been deleted are excluded if not self.inventory.has_id(file_id): return False return os.access(self.abspath(self.inventory.id2path(file_id)), os.F_OK) def get_file_size(self, file_id): return os.stat(self._get_store_filename(file_id))[ST_SIZE] def get_file_sha1(self, file_id): f = self.get_file(file_id) return sha_file(f) def file_class(self, filename): if self.path2id(filename): return 'V' elif self.is_ignored(filename): return 'I' else: return '?' def list_files(self): """Recursively list all files as (path, class, kind, id). Lists, but does not descend into unversioned directories. This does not include files that have been deleted in this tree. Skips the control directory. """ inv = self.inventory def descend(from_dir, from_dir_id, dp): ls = os.listdir(dp) ls.sort() for f in ls: ## TODO: If we find a subdirectory with its own .bzr ## directory, then that is a separate tree and we ## should exclude it. if bzrlib.BZRDIR == f: continue # path within tree fp = appendpath(from_dir, f) # absolute path fap = appendpath(dp, f) f_ie = inv.get_child(from_dir_id, f) if f_ie: c = 'V' elif self.is_ignored(fp): c = 'I' else: c = '?' fk = file_kind(fap) if f_ie: if f_ie.kind != fk: bailout("file %r entered as kind %r id %r, now of kind %r" % (fap, f_ie.kind, f_ie.file_id, fk)) yield fp, c, fk, (f_ie and f_ie.file_id) if fk != 'directory': continue if c != 'V': # don't descend unversioned directories continue for ff in descend(fp, f_ie.file_id, fap): yield ff for f in descend('', None, self.basedir): yield f def unknowns(self): """Yield all unknown files in this WorkingTree. If there are any unknown directories then only the directory is returned, not all its children. But if there are unknown files under a versioned subdirectory, they are returned. Currently returned depth-first, sorted by name within directories. """ ## TODO: Work from given directory downwards for path, dir_entry in self.inventory.directories(): mutter("search for unknowns in %r" % path) dirabs = self.abspath(path) if not isdir(dirabs): # e.g. directory deleted continue fl = [] for subf in os.listdir(dirabs): if (subf != '.bzr' and (subf not in dir_entry.children)): fl.append(subf) fl.sort() for subf in fl: subp = appendpath(path, subf) if self.is_ignored(subp): continue yield subp def ignored_files(self): for fpath, fclass, fkind, fid in self.list_files(): if fclass == 'I': yield fpath def get_ignore_list(self): """Return list of ignore patterns. Cached in the Tree object after the first call. """ if hasattr(self, '_ignorelist'): return self._ignorelist l = bzrlib.DEFAULT_IGNORE[:] if self.has_filename(bzrlib.IGNORE_FILENAME): f = self.get_file_byname(bzrlib.IGNORE_FILENAME) l.extend([line.rstrip("\n\r") for line in f.readlines()]) self._ignorelist = l return l def is_ignored(self, filename): """Check whether the filename matches an ignore pattern. Patterns containing '/' need to match the whole path; others match against only the last component. If the file is ignored, returns the pattern which caused it to be ignored, otherwise None. So this can simply be used as a boolean if desired.""" ## TODO: Use '**' to match directories, and other extended globbing stuff from cvs/rsync. for pat in self.get_ignore_list(): if '/' in pat: if fnmatch.fnmatchcase(filename, pat): return pat else: if fnmatch.fnmatchcase(splitpath(filename)[-1], pat): return pat return None class RevisionTree(Tree): """Tree viewing a previous revision. File text can be retrieved from the text store. :todo: Some kind of `__repr__` method, but a good one probably means knowing the branch and revision number, or at least passing a description to the constructor. """ def __init__(self, store, inv): self._store = store self._inventory = inv def get_file(self, file_id): ie = self._inventory[file_id] f = self._store[ie.text_id] mutter(" get fileid{%s} from %r" % (file_id, self)) self._check_retrieved(ie, f) return f def get_file_size(self, file_id): return self._inventory[file_id].text_size def get_file_sha1(self, file_id): ie = self._inventory[file_id] return ie.text_sha1 def has_filename(self, filename): return bool(self.inventory.path2id(filename)) def list_files(self): # The only files returned by this are those from the version for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id class EmptyTree(Tree): def __init__(self): self._inventory = Inventory() def has_filename(self, filename): return False def list_files(self): if False: # just to make it a generator yield None ###################################################################### # diff # TODO: Merge these two functions into a single one that can operate # on either a whole tree or a set of files. # TODO: Return the diff in order by filename, not by category or in # random order. Can probably be done by lock-stepping through the # filenames from both trees. def file_status(filename, old_tree, new_tree): """Return single-letter status, old and new names for a file. The complexity here is in deciding how to represent renames; many complex cases are possible. """ old_inv = old_tree.inventory new_inv = new_tree.inventory new_id = new_inv.path2id(filename) old_id = old_inv.path2id(filename) if not new_id and not old_id: # easy: doesn't exist in either; not versioned at all if new_tree.is_ignored(filename): return 'I', None, None else: return '?', None, None elif new_id: # There is now a file of this name, great. pass else: # There is no longer a file of this name, but we can describe # what happened to the file that used to have # this name. There are two possibilities: either it was # deleted entirely, or renamed. assert old_id if new_inv.has_id(old_id): return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id) else: return 'D', old_inv.id2path(old_id), None # if the file_id is new in this revision, it is added if new_id and not old_inv.has_id(new_id): return 'A' # if there used to be a file of this name, but that ID has now # disappeared, it is deleted if old_id and not new_inv.has_id(old_id): return 'D' return 'wtf?' commit refs/heads/master mark :143 committer 1112081454 +1000 data 61 new common Tree.extras() to support both unknowns and ignored from :142 M 644 inline bzrlib/tree.py data 13340 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tree classes, representing directory at point in time. """ from sets import Set import os.path, os, fnmatch from inventory import Inventory from trace import mutter, note from osutils import pumpfile, compare_files, filesize, quotefn, sha_file, \ joinpath, splitpath, appendpath, isdir, isfile, file_kind, fingerprint_file from errors import bailout import branch from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE import bzrlib class Tree: """Abstract file tree. There are several subclasses: * `WorkingTree` exists as files on disk editable by the user. * `RevisionTree` is a tree as recorded at some point in the past. * `EmptyTree` Trees contain an `Inventory` object, and also know how to retrieve file texts mentioned in the inventory, either from a working directory or from a store. It is possible for trees to contain files that are not described in their inventory or vice versa; for this use `filenames()`. Trees can be compared, etc, regardless of whether they are working trees or versioned trees. """ def has_filename(self, filename): """True if the tree has given filename.""" raise NotImplementedError() def has_id(self, file_id): return self.inventory.has_id(file_id) def id_set(self): """Return set of all ids in this tree.""" return self.inventory.id_set() def id2path(self, file_id): return self.inventory.id2path(file_id) def _get_inventory(self): return self._inventory inventory = property(_get_inventory, doc="Inventory of this Tree") def _check_retrieved(self, ie, f): fp = fingerprint_file(f) f.seek(0) if ie.text_size is not None: if ie.text_size != fp['size']: bailout("mismatched size for file %r in %r" % (ie.file_id, self._store), ["inventory expects %d bytes" % ie.text_size, "file is actually %d bytes" % fp['size'], "store is probably damaged/corrupt"]) if ie.text_sha1 != fp['sha1']: bailout("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store), ["inventory expects %s" % ie.text_sha1, "file is actually %s" % fp['sha1'], "store is probably damaged/corrupt"]) def export(self, dest): """Export this tree to a new directory. `dest` should not exist, and will be created holding the contents of this tree. :todo: To handle subdirectories we need to create the directories first. :note: If the export fails, the destination directory will be left in a half-assed state. """ os.mkdir(dest) mutter('export version %r' % self) inv = self.inventory for dp, ie in inv.iter_entries(): kind = ie.kind fullpath = appendpath(dest, dp) if kind == 'directory': os.mkdir(fullpath) elif kind == 'file': pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb')) else: bailout("don't know how to export {%s} of kind %r", fid, kind) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) class WorkingTree(Tree): """Working copy tree. The inventory is held in the `Branch` working-inventory, and the files are in a directory on disk. It is possible for a `WorkingTree` to have a filename which is not listed in the Inventory and vice versa. """ def __init__(self, basedir, inv): self._inventory = inv self.basedir = basedir self.path2id = inv.path2id def __repr__(self): return "<%s of %s>" % (self.__class__.__name__, self.basedir) def abspath(self, filename): return os.path.join(self.basedir, filename) def has_filename(self, filename): return os.path.exists(self.abspath(filename)) def get_file(self, file_id): return self.get_file_byname(self.id2path(file_id)) def get_file_byname(self, filename): return file(self.abspath(filename), 'rb') def _get_store_filename(self, file_id): return self.abspath(self.id2path(file_id)) def has_id(self, file_id): # files that have been deleted are excluded if not self.inventory.has_id(file_id): return False return os.access(self.abspath(self.inventory.id2path(file_id)), os.F_OK) def get_file_size(self, file_id): return os.stat(self._get_store_filename(file_id))[ST_SIZE] def get_file_sha1(self, file_id): f = self.get_file(file_id) return sha_file(f) def file_class(self, filename): if self.path2id(filename): return 'V' elif self.is_ignored(filename): return 'I' else: return '?' def list_files(self): """Recursively list all files as (path, class, kind, id). Lists, but does not descend into unversioned directories. This does not include files that have been deleted in this tree. Skips the control directory. """ inv = self.inventory def descend(from_dir, from_dir_id, dp): ls = os.listdir(dp) ls.sort() for f in ls: ## TODO: If we find a subdirectory with its own .bzr ## directory, then that is a separate tree and we ## should exclude it. if bzrlib.BZRDIR == f: continue # path within tree fp = appendpath(from_dir, f) # absolute path fap = appendpath(dp, f) f_ie = inv.get_child(from_dir_id, f) if f_ie: c = 'V' elif self.is_ignored(fp): c = 'I' else: c = '?' fk = file_kind(fap) if f_ie: if f_ie.kind != fk: bailout("file %r entered as kind %r id %r, now of kind %r" % (fap, f_ie.kind, f_ie.file_id, fk)) yield fp, c, fk, (f_ie and f_ie.file_id) if fk != 'directory': continue if c != 'V': # don't descend unversioned directories continue for ff in descend(fp, f_ie.file_id, fap): yield ff for f in descend('', None, self.basedir): yield f def unknowns(self): for subp in self.extras(): if not self.is_ignored(subp): yield subp def extras(self): """Yield all unknown files in this WorkingTree. If there are any unknown directories then only the directory is returned, not all its children. But if there are unknown files under a versioned subdirectory, they are returned. Currently returned depth-first, sorted by name within directories. """ ## TODO: Work from given directory downwards for path, dir_entry in self.inventory.directories(): mutter("search for unknowns in %r" % path) dirabs = self.abspath(path) if not isdir(dirabs): # e.g. directory deleted continue fl = [] for subf in os.listdir(dirabs): if (subf != '.bzr' and (subf not in dir_entry.children)): fl.append(subf) fl.sort() for subf in fl: subp = appendpath(path, subf) yield subp def ignored_files(self): for fpath, fclass, fkind, fid in self.list_files(): if fclass == 'I': yield fpath def get_ignore_list(self): """Return list of ignore patterns. Cached in the Tree object after the first call. """ if hasattr(self, '_ignorelist'): return self._ignorelist l = bzrlib.DEFAULT_IGNORE[:] if self.has_filename(bzrlib.IGNORE_FILENAME): f = self.get_file_byname(bzrlib.IGNORE_FILENAME) l.extend([line.rstrip("\n\r") for line in f.readlines()]) self._ignorelist = l return l def is_ignored(self, filename): """Check whether the filename matches an ignore pattern. Patterns containing '/' need to match the whole path; others match against only the last component. If the file is ignored, returns the pattern which caused it to be ignored, otherwise None. So this can simply be used as a boolean if desired.""" ## TODO: Use '**' to match directories, and other extended globbing stuff from cvs/rsync. for pat in self.get_ignore_list(): if '/' in pat: if fnmatch.fnmatchcase(filename, pat): return pat else: if fnmatch.fnmatchcase(splitpath(filename)[-1], pat): return pat return None class RevisionTree(Tree): """Tree viewing a previous revision. File text can be retrieved from the text store. :todo: Some kind of `__repr__` method, but a good one probably means knowing the branch and revision number, or at least passing a description to the constructor. """ def __init__(self, store, inv): self._store = store self._inventory = inv def get_file(self, file_id): ie = self._inventory[file_id] f = self._store[ie.text_id] mutter(" get fileid{%s} from %r" % (file_id, self)) self._check_retrieved(ie, f) return f def get_file_size(self, file_id): return self._inventory[file_id].text_size def get_file_sha1(self, file_id): ie = self._inventory[file_id] return ie.text_sha1 def has_filename(self, filename): return bool(self.inventory.path2id(filename)) def list_files(self): # The only files returned by this are those from the version for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id class EmptyTree(Tree): def __init__(self): self._inventory = Inventory() def has_filename(self, filename): return False def list_files(self): if False: # just to make it a generator yield None ###################################################################### # diff # TODO: Merge these two functions into a single one that can operate # on either a whole tree or a set of files. # TODO: Return the diff in order by filename, not by category or in # random order. Can probably be done by lock-stepping through the # filenames from both trees. def file_status(filename, old_tree, new_tree): """Return single-letter status, old and new names for a file. The complexity here is in deciding how to represent renames; many complex cases are possible. """ old_inv = old_tree.inventory new_inv = new_tree.inventory new_id = new_inv.path2id(filename) old_id = old_inv.path2id(filename) if not new_id and not old_id: # easy: doesn't exist in either; not versioned at all if new_tree.is_ignored(filename): return 'I', None, None else: return '?', None, None elif new_id: # There is now a file of this name, great. pass else: # There is no longer a file of this name, but we can describe # what happened to the file that used to have # this name. There are two possibilities: either it was # deleted entirely, or renamed. assert old_id if new_inv.has_id(old_id): return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id) else: return 'D', old_inv.id2path(old_id), None # if the file_id is new in this revision, it is added if new_id and not old_inv.has_id(new_id): return 'A' # if there used to be a file of this name, but that ID has now # disappeared, it is deleted if old_id and not new_inv.has_id(old_id): return 'D' return 'wtf?' commit refs/heads/master mark :144 committer 1112081549 +1000 data 45 new Tree.ignored_files based on Tree.extras() from :143 M 644 inline bzrlib/tree.py data 13406 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tree classes, representing directory at point in time. """ from sets import Set import os.path, os, fnmatch from inventory import Inventory from trace import mutter, note from osutils import pumpfile, compare_files, filesize, quotefn, sha_file, \ joinpath, splitpath, appendpath, isdir, isfile, file_kind, fingerprint_file from errors import bailout import branch from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE import bzrlib class Tree: """Abstract file tree. There are several subclasses: * `WorkingTree` exists as files on disk editable by the user. * `RevisionTree` is a tree as recorded at some point in the past. * `EmptyTree` Trees contain an `Inventory` object, and also know how to retrieve file texts mentioned in the inventory, either from a working directory or from a store. It is possible for trees to contain files that are not described in their inventory or vice versa; for this use `filenames()`. Trees can be compared, etc, regardless of whether they are working trees or versioned trees. """ def has_filename(self, filename): """True if the tree has given filename.""" raise NotImplementedError() def has_id(self, file_id): return self.inventory.has_id(file_id) def id_set(self): """Return set of all ids in this tree.""" return self.inventory.id_set() def id2path(self, file_id): return self.inventory.id2path(file_id) def _get_inventory(self): return self._inventory inventory = property(_get_inventory, doc="Inventory of this Tree") def _check_retrieved(self, ie, f): fp = fingerprint_file(f) f.seek(0) if ie.text_size is not None: if ie.text_size != fp['size']: bailout("mismatched size for file %r in %r" % (ie.file_id, self._store), ["inventory expects %d bytes" % ie.text_size, "file is actually %d bytes" % fp['size'], "store is probably damaged/corrupt"]) if ie.text_sha1 != fp['sha1']: bailout("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store), ["inventory expects %s" % ie.text_sha1, "file is actually %s" % fp['sha1'], "store is probably damaged/corrupt"]) def export(self, dest): """Export this tree to a new directory. `dest` should not exist, and will be created holding the contents of this tree. :todo: To handle subdirectories we need to create the directories first. :note: If the export fails, the destination directory will be left in a half-assed state. """ os.mkdir(dest) mutter('export version %r' % self) inv = self.inventory for dp, ie in inv.iter_entries(): kind = ie.kind fullpath = appendpath(dest, dp) if kind == 'directory': os.mkdir(fullpath) elif kind == 'file': pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb')) else: bailout("don't know how to export {%s} of kind %r", fid, kind) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) class WorkingTree(Tree): """Working copy tree. The inventory is held in the `Branch` working-inventory, and the files are in a directory on disk. It is possible for a `WorkingTree` to have a filename which is not listed in the Inventory and vice versa. """ def __init__(self, basedir, inv): self._inventory = inv self.basedir = basedir self.path2id = inv.path2id def __repr__(self): return "<%s of %s>" % (self.__class__.__name__, self.basedir) def abspath(self, filename): return os.path.join(self.basedir, filename) def has_filename(self, filename): return os.path.exists(self.abspath(filename)) def get_file(self, file_id): return self.get_file_byname(self.id2path(file_id)) def get_file_byname(self, filename): return file(self.abspath(filename), 'rb') def _get_store_filename(self, file_id): return self.abspath(self.id2path(file_id)) def has_id(self, file_id): # files that have been deleted are excluded if not self.inventory.has_id(file_id): return False return os.access(self.abspath(self.inventory.id2path(file_id)), os.F_OK) def get_file_size(self, file_id): return os.stat(self._get_store_filename(file_id))[ST_SIZE] def get_file_sha1(self, file_id): f = self.get_file(file_id) return sha_file(f) def file_class(self, filename): if self.path2id(filename): return 'V' elif self.is_ignored(filename): return 'I' else: return '?' def list_files(self): """Recursively list all files as (path, class, kind, id). Lists, but does not descend into unversioned directories. This does not include files that have been deleted in this tree. Skips the control directory. """ inv = self.inventory def descend(from_dir, from_dir_id, dp): ls = os.listdir(dp) ls.sort() for f in ls: ## TODO: If we find a subdirectory with its own .bzr ## directory, then that is a separate tree and we ## should exclude it. if bzrlib.BZRDIR == f: continue # path within tree fp = appendpath(from_dir, f) # absolute path fap = appendpath(dp, f) f_ie = inv.get_child(from_dir_id, f) if f_ie: c = 'V' elif self.is_ignored(fp): c = 'I' else: c = '?' fk = file_kind(fap) if f_ie: if f_ie.kind != fk: bailout("file %r entered as kind %r id %r, now of kind %r" % (fap, f_ie.kind, f_ie.file_id, fk)) yield fp, c, fk, (f_ie and f_ie.file_id) if fk != 'directory': continue if c != 'V': # don't descend unversioned directories continue for ff in descend(fp, f_ie.file_id, fap): yield ff for f in descend('', None, self.basedir): yield f def unknowns(self): for subp in self.extras(): if not self.is_ignored(subp): yield subp def extras(self): """Yield all unknown files in this WorkingTree. If there are any unknown directories then only the directory is returned, not all its children. But if there are unknown files under a versioned subdirectory, they are returned. Currently returned depth-first, sorted by name within directories. """ ## TODO: Work from given directory downwards for path, dir_entry in self.inventory.directories(): mutter("search for unknowns in %r" % path) dirabs = self.abspath(path) if not isdir(dirabs): # e.g. directory deleted continue fl = [] for subf in os.listdir(dirabs): if (subf != '.bzr' and (subf not in dir_entry.children)): fl.append(subf) fl.sort() for subf in fl: subp = appendpath(path, subf) yield subp def ignored_files(self): """Yield list of PATH, IGNORE_PATTERN""" for subp in self.extras(): pat = self.is_ignored(subp) if pat != None: yield subp, pat def get_ignore_list(self): """Return list of ignore patterns. Cached in the Tree object after the first call. """ if hasattr(self, '_ignorelist'): return self._ignorelist l = bzrlib.DEFAULT_IGNORE[:] if self.has_filename(bzrlib.IGNORE_FILENAME): f = self.get_file_byname(bzrlib.IGNORE_FILENAME) l.extend([line.rstrip("\n\r") for line in f.readlines()]) self._ignorelist = l return l def is_ignored(self, filename): """Check whether the filename matches an ignore pattern. Patterns containing '/' need to match the whole path; others match against only the last component. If the file is ignored, returns the pattern which caused it to be ignored, otherwise None. So this can simply be used as a boolean if desired.""" ## TODO: Use '**' to match directories, and other extended globbing stuff from cvs/rsync. for pat in self.get_ignore_list(): if '/' in pat: if fnmatch.fnmatchcase(filename, pat): return pat else: if fnmatch.fnmatchcase(splitpath(filename)[-1], pat): return pat return None class RevisionTree(Tree): """Tree viewing a previous revision. File text can be retrieved from the text store. :todo: Some kind of `__repr__` method, but a good one probably means knowing the branch and revision number, or at least passing a description to the constructor. """ def __init__(self, store, inv): self._store = store self._inventory = inv def get_file(self, file_id): ie = self._inventory[file_id] f = self._store[ie.text_id] mutter(" get fileid{%s} from %r" % (file_id, self)) self._check_retrieved(ie, f) return f def get_file_size(self, file_id): return self._inventory[file_id].text_size def get_file_sha1(self, file_id): ie = self._inventory[file_id] return ie.text_sha1 def has_filename(self, filename): return bool(self.inventory.path2id(filename)) def list_files(self): # The only files returned by this are those from the version for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id class EmptyTree(Tree): def __init__(self): self._inventory = Inventory() def has_filename(self, filename): return False def list_files(self): if False: # just to make it a generator yield None ###################################################################### # diff # TODO: Merge these two functions into a single one that can operate # on either a whole tree or a set of files. # TODO: Return the diff in order by filename, not by category or in # random order. Can probably be done by lock-stepping through the # filenames from both trees. def file_status(filename, old_tree, new_tree): """Return single-letter status, old and new names for a file. The complexity here is in deciding how to represent renames; many complex cases are possible. """ old_inv = old_tree.inventory new_inv = new_tree.inventory new_id = new_inv.path2id(filename) old_id = old_inv.path2id(filename) if not new_id and not old_id: # easy: doesn't exist in either; not versioned at all if new_tree.is_ignored(filename): return 'I', None, None else: return '?', None, None elif new_id: # There is now a file of this name, great. pass else: # There is no longer a file of this name, but we can describe # what happened to the file that used to have # this name. There are two possibilities: either it was # deleted entirely, or renamed. assert old_id if new_inv.has_id(old_id): return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id) else: return 'D', old_inv.id2path(old_id), None # if the file_id is new in this revision, it is added if new_id and not old_inv.has_id(new_id): return 'A' # if there used to be a file of this name, but that ID has now # disappeared, it is deleted if old_id and not new_inv.has_id(old_id): return 'D' return 'wtf?' commit refs/heads/master mark :145 committer 1112081609 +1000 data 22 update ignore patterns from :144 M 644 inline .bzrignore data 79 doc/*.html *.py[oc] *~ .arch-ids .bzr.profile .arch-inventory {arch} CHANGELOG commit refs/heads/master mark :146 committer 1112083290 +1000 data 37 match ignore patterns like ./config.h from :145 M 644 inline .bzrignore data 81 ./doc/*.html *.py[oc] *~ .arch-ids .bzr.profile .arch-inventory {arch} CHANGELOG M 644 inline NEWS data 1419 bzr-0.0.2 "black cube" NOT RELEASED YET ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/tree.py data 13679 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tree classes, representing directory at point in time. """ from sets import Set import os.path, os, fnmatch from inventory import Inventory from trace import mutter, note from osutils import pumpfile, compare_files, filesize, quotefn, sha_file, \ joinpath, splitpath, appendpath, isdir, isfile, file_kind, fingerprint_file from errors import bailout import branch from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE import bzrlib class Tree: """Abstract file tree. There are several subclasses: * `WorkingTree` exists as files on disk editable by the user. * `RevisionTree` is a tree as recorded at some point in the past. * `EmptyTree` Trees contain an `Inventory` object, and also know how to retrieve file texts mentioned in the inventory, either from a working directory or from a store. It is possible for trees to contain files that are not described in their inventory or vice versa; for this use `filenames()`. Trees can be compared, etc, regardless of whether they are working trees or versioned trees. """ def has_filename(self, filename): """True if the tree has given filename.""" raise NotImplementedError() def has_id(self, file_id): return self.inventory.has_id(file_id) def id_set(self): """Return set of all ids in this tree.""" return self.inventory.id_set() def id2path(self, file_id): return self.inventory.id2path(file_id) def _get_inventory(self): return self._inventory inventory = property(_get_inventory, doc="Inventory of this Tree") def _check_retrieved(self, ie, f): fp = fingerprint_file(f) f.seek(0) if ie.text_size is not None: if ie.text_size != fp['size']: bailout("mismatched size for file %r in %r" % (ie.file_id, self._store), ["inventory expects %d bytes" % ie.text_size, "file is actually %d bytes" % fp['size'], "store is probably damaged/corrupt"]) if ie.text_sha1 != fp['sha1']: bailout("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store), ["inventory expects %s" % ie.text_sha1, "file is actually %s" % fp['sha1'], "store is probably damaged/corrupt"]) def export(self, dest): """Export this tree to a new directory. `dest` should not exist, and will be created holding the contents of this tree. :todo: To handle subdirectories we need to create the directories first. :note: If the export fails, the destination directory will be left in a half-assed state. """ os.mkdir(dest) mutter('export version %r' % self) inv = self.inventory for dp, ie in inv.iter_entries(): kind = ie.kind fullpath = appendpath(dest, dp) if kind == 'directory': os.mkdir(fullpath) elif kind == 'file': pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb')) else: bailout("don't know how to export {%s} of kind %r", fid, kind) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) class WorkingTree(Tree): """Working copy tree. The inventory is held in the `Branch` working-inventory, and the files are in a directory on disk. It is possible for a `WorkingTree` to have a filename which is not listed in the Inventory and vice versa. """ def __init__(self, basedir, inv): self._inventory = inv self.basedir = basedir self.path2id = inv.path2id def __repr__(self): return "<%s of %s>" % (self.__class__.__name__, self.basedir) def abspath(self, filename): return os.path.join(self.basedir, filename) def has_filename(self, filename): return os.path.exists(self.abspath(filename)) def get_file(self, file_id): return self.get_file_byname(self.id2path(file_id)) def get_file_byname(self, filename): return file(self.abspath(filename), 'rb') def _get_store_filename(self, file_id): return self.abspath(self.id2path(file_id)) def has_id(self, file_id): # files that have been deleted are excluded if not self.inventory.has_id(file_id): return False return os.access(self.abspath(self.inventory.id2path(file_id)), os.F_OK) def get_file_size(self, file_id): return os.stat(self._get_store_filename(file_id))[ST_SIZE] def get_file_sha1(self, file_id): f = self.get_file(file_id) return sha_file(f) def file_class(self, filename): if self.path2id(filename): return 'V' elif self.is_ignored(filename): return 'I' else: return '?' def list_files(self): """Recursively list all files as (path, class, kind, id). Lists, but does not descend into unversioned directories. This does not include files that have been deleted in this tree. Skips the control directory. """ inv = self.inventory def descend(from_dir, from_dir_id, dp): ls = os.listdir(dp) ls.sort() for f in ls: ## TODO: If we find a subdirectory with its own .bzr ## directory, then that is a separate tree and we ## should exclude it. if bzrlib.BZRDIR == f: continue # path within tree fp = appendpath(from_dir, f) # absolute path fap = appendpath(dp, f) f_ie = inv.get_child(from_dir_id, f) if f_ie: c = 'V' elif self.is_ignored(fp): c = 'I' else: c = '?' fk = file_kind(fap) if f_ie: if f_ie.kind != fk: bailout("file %r entered as kind %r id %r, now of kind %r" % (fap, f_ie.kind, f_ie.file_id, fk)) yield fp, c, fk, (f_ie and f_ie.file_id) if fk != 'directory': continue if c != 'V': # don't descend unversioned directories continue for ff in descend(fp, f_ie.file_id, fap): yield ff for f in descend('', None, self.basedir): yield f def unknowns(self): for subp in self.extras(): if not self.is_ignored(subp): yield subp def extras(self): """Yield all unknown files in this WorkingTree. If there are any unknown directories then only the directory is returned, not all its children. But if there are unknown files under a versioned subdirectory, they are returned. Currently returned depth-first, sorted by name within directories. """ ## TODO: Work from given directory downwards for path, dir_entry in self.inventory.directories(): mutter("search for unknowns in %r" % path) dirabs = self.abspath(path) if not isdir(dirabs): # e.g. directory deleted continue fl = [] for subf in os.listdir(dirabs): if (subf != '.bzr' and (subf not in dir_entry.children)): fl.append(subf) fl.sort() for subf in fl: subp = appendpath(path, subf) yield subp def ignored_files(self): """Yield list of PATH, IGNORE_PATTERN""" for subp in self.extras(): pat = self.is_ignored(subp) if pat != None: yield subp, pat def get_ignore_list(self): """Return list of ignore patterns. Cached in the Tree object after the first call. """ if hasattr(self, '_ignorelist'): return self._ignorelist l = bzrlib.DEFAULT_IGNORE[:] if self.has_filename(bzrlib.IGNORE_FILENAME): f = self.get_file_byname(bzrlib.IGNORE_FILENAME) l.extend([line.rstrip("\n\r") for line in f.readlines()]) self._ignorelist = l return l def is_ignored(self, filename): """Check whether the filename matches an ignore pattern. Patterns containing '/' need to match the whole path; others match against only the last component. If the file is ignored, returns the pattern which caused it to be ignored, otherwise None. So this can simply be used as a boolean if desired.""" ## TODO: Use '**' to match directories, and other extended globbing stuff from cvs/rsync. for pat in self.get_ignore_list(): if '/' in pat: # as a special case, you can put ./ at the start of a pattern; # this is good to match in the top-level only; if pat[:2] == './': newpat = pat[2:] else: newpat = pat if fnmatch.fnmatchcase(filename, newpat): return pat else: if fnmatch.fnmatchcase(splitpath(filename)[-1], pat): return pat return None class RevisionTree(Tree): """Tree viewing a previous revision. File text can be retrieved from the text store. :todo: Some kind of `__repr__` method, but a good one probably means knowing the branch and revision number, or at least passing a description to the constructor. """ def __init__(self, store, inv): self._store = store self._inventory = inv def get_file(self, file_id): ie = self._inventory[file_id] f = self._store[ie.text_id] mutter(" get fileid{%s} from %r" % (file_id, self)) self._check_retrieved(ie, f) return f def get_file_size(self, file_id): return self._inventory[file_id].text_size def get_file_sha1(self, file_id): ie = self._inventory[file_id] return ie.text_sha1 def has_filename(self, filename): return bool(self.inventory.path2id(filename)) def list_files(self): # The only files returned by this are those from the version for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id class EmptyTree(Tree): def __init__(self): self._inventory = Inventory() def has_filename(self, filename): return False def list_files(self): if False: # just to make it a generator yield None ###################################################################### # diff # TODO: Merge these two functions into a single one that can operate # on either a whole tree or a set of files. # TODO: Return the diff in order by filename, not by category or in # random order. Can probably be done by lock-stepping through the # filenames from both trees. def file_status(filename, old_tree, new_tree): """Return single-letter status, old and new names for a file. The complexity here is in deciding how to represent renames; many complex cases are possible. """ old_inv = old_tree.inventory new_inv = new_tree.inventory new_id = new_inv.path2id(filename) old_id = old_inv.path2id(filename) if not new_id and not old_id: # easy: doesn't exist in either; not versioned at all if new_tree.is_ignored(filename): return 'I', None, None else: return '?', None, None elif new_id: # There is now a file of this name, great. pass else: # There is no longer a file of this name, but we can describe # what happened to the file that used to have # this name. There are two possibilities: either it was # deleted entirely, or renamed. assert old_id if new_inv.has_id(old_id): return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id) else: return 'D', old_inv.id2path(old_id), None # if the file_id is new in this revision, it is added if new_id and not old_inv.has_id(new_id): return 'A' # if there used to be a file of this name, but that ID has now # disappeared, it is deleted if old_id and not new_inv.has_id(old_id): return 'D' return 'wtf?' commit refs/heads/master mark :147 committer 1112083420 +1000 data 4 todo from :146 M 644 inline bzrlib/commands.py data 24692 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] --revision REV Show changes since REV, rather than predecessor. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Diff selected files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(verbose=True): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = cmdfn.__doc__ if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'commit': [], 'diff': [], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'relpath': ['filename'], 'remove': ['file+'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot prof = hotshot.Profile('.bzr.profile') ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load('.bzr.profile') #stats.strip_dirs() stats.sort_stats('cumulative', 'calls') stats.print_stats(20) else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :148 committer 1112096990 +1000 data 34 performance notes and measurements from :147 M 644 inline bzrlib/commands.py data 24888 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] --revision REV Show changes since REV, rather than predecessor. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Diff selected files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): inv = Branch('.').read_working_inventory() def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(verbose=True): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = cmdfn.__doc__ if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'commit': [], 'diff': [], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'relpath': ['filename'], 'remove': ['file+'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot prof = hotshot.Profile('.bzr.profile') ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load('.bzr.profile') #stats.strip_dirs() stats.sort_stats('cumulative', 'calls') stats.print_stats(20) else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') M 644 inline notes/performance.txt data 9288 For a tree holding 2.4.18 (two copies), 2.4.19, 2.4.20 With gzip -9: mbp@hope% du .bzr 195110 .bzr/text-store 20 .bzr/revision-store 12355 .bzr/inventory-store 216325 .bzr mbp@hope% du -s . 523128 . Without gzip: This is actually a pretty bad example because of deleting and re-importing 2.4.18, but still not totally unreasonable. ---- linux-2.4.0: 116399 kB after addding everything: 119505kB bzr status 2.68s user 0.13s system 84% cpu 3.330 total bzr commit 'import 2.4.0' 4.41s user 2.15s system 11% cpu 59.490 total 242446 . 122068 .bzr ---- Performance (2005-03-01) To add all files from linux-2.4.18: about 70s, mostly inventory serialization/deserialization. To commit: - finished, 6.520u/3.870s cpu, 33.940u/10.730s cum - 134.040 elapsed Interesting that it spends so long on external processing! I wonder if this is for running uuidgen? Let's try generating things internally. Great, this cuts it to 17.15s user 0.61s system 83% cpu 21.365 total to add, with no external command time. The commit now seems to spend most of its time copying to disk. - finished, 6.550u/3.320s cpu, 35.050u/9.870s cum - 89.650 elapsed I wonder where the external time is now? We were also using uuids() for revisions. Let's remove everything and re-add. Detecting everything was removed takes - finished, 2.460u/0.110s cpu, 0.000u/0.000s cum - 3.430 elapsed which may be mostly XML deserialization? Just getting the previous revision takes about this long: bzr invoked at Tue 2005-03-01 15:53:05.183741 EST +1100 by mbp@sourcefrog.net on hope arguments: ['/home/mbp/bin/bzr', 'get-revision-inventory', 'mbp@sourcefrog.net-20050301044608-8513202ab179aff4-44e8cd52a41aa705'] platform: Linux-2.6.10-4-686-i686-with-debian-3.1 - finished, 3.910u/0.390s cpu, 0.000u/0.000s cum - 6.690 elapsed Now committing the revision which removes all files should be fast. - finished, 1.280u/0.030s cpu, 0.000u/0.000s cum - 1.320 elapsed Now re-add with new code that doesn't call uuidgen: - finished, 1.990u/0.030s cpu, 0.000u/0.000s cum - 2.040 elapsed 16.61s user 0.55s system 74% cpu 22.965 total Status:: - finished, 2.500u/0.110s cpu, 0.010u/0.000s cum - 3.350 elapsed And commit:: Now patch up to 2.4.19. There were some bugs in handling missing directories, but with that fixed we do much better:: bzr status 5.86s user 1.06s system 10% cpu 1:05.55 total This is slow because it's diffing every file; we should use mtimes etc to make this faster. The cpu time is reasonable. I see difflib is pure Python; it might be faster to shell out to GNU diff when we need it. Export is very fast:: - finished, 4.220u/1.480s cpu, 0.010u/0.000s cum - 10.810 elapsed bzr export 1 ../linux-2.4.18.export1 3.92s user 1.72s system 21% cpu 26.030 total Now to find and add the new changes:: - finished, 2.190u/0.030s cpu, 0.000u/0.000s cum - 2.300 elapsed :: bzr commit 'import 2.4.19' 9.36s user 1.91s system 23% cpu 47.127 total And the result is exactly right. Try exporting:: mbp@hope% bzr export 4 ../linux-2.4.19.export4 bzr export 4 ../linux-2.4.19.export4 4.21s user 1.70s system 18% cpu 32.304 total and the export is exactly the same as the tarball. Now we can optimize the diff a bit more by not comparing files that have the right SHA-1 from within the commit For comparison:: patch -p1 < ../kernel.pkg/patch-2.4.20 1.61s user 1.03s system 13% cpu 19.106 total Now status after applying the .20 patch. With full-text verification:: bzr status 7.07s user 1.32s system 13% cpu 1:04.29 total with that turned off:: bzr status 5.86s user 0.56s system 25% cpu 25.577 total After adding: bzr status 6.14s user 0.61s system 25% cpu 26.583 total Should add some kind of profile counter for quick compares vs slow compares. bzr commit 'import 2.4.20' 7.57s user 1.36s system 20% cpu 43.568 total export: finished, 3.940u/1.820s cpu, 0.000u/0.000s cum, 50.990 elapsed also exports correctly now .21 bzr commit 'import 2.4.1' 5.59s user 0.51s system 60% cpu 10.122 total 265520 . 137704 .bzr import 2.4.2 317758 . 183463 .bzr with everything through to 2.4.29 imported, the .bzr directory is 1132MB, compared to 185MB for one tree. The .bzr.log is 100MB!. So the storage is 6.1 times larger, although we're holding 30 versions. It's pretty large but I think not ridiculous. By contrast the tarball for 2.4.0 is 104MB, and the tarball plus uncompressed patches are 315MB. Uncompressed, the text store is 1041MB. So it is only three times worse than patches, and could be compressed at presumably roughly equal efficiency. It is large, but also a very simple design and perhaps adequate for the moment. The text store with each file individually gziped is 264MB, which is also a very simple format and makes it less than twice the size of the source tree. This is actually rather pessimistic because I think there are some orphaned texts in there. Measured by du, the compressed full-text store is 363MB; also probably tolerable. The real fix is perhaps to use some kind of weave, not so much for storage efficiency as for fast annotation and therefore possible annotation-based merge. ----- 2005-03-25 Now we have recursive add, add is much faster. Adding all of the linux 2.4.19 kernel tree takes only finished, 5.460u/0.610s cpu, 0.010u/0.000s cum, 6.710 elapsed However, the store code currently flushes to disk after every write, which is probably excessive. So a commit takes finished, 8.740u/3.950s cpu, 0.010u/0.000s cum, 156.420 elapsed Status is now also quite fast, depsite that it still has to read all the working copies: mbp@hope% bzr status ~/work/linux-2.4.19 bzr status 5.51s user 0.79s system 99% cpu 6.337 total strace shows much of this is in write(2), probably because of logging. With more buffering on that file, removing all the explicit flushes, that is reduced to mbp@hope% time bzr status bzr status 5.23s user 0.42s system 97% cpu 5.780 total which is mostly opening, stating and reading files, as it should be. Still a few too many stat calls. Now fixed up handling of root directory. Without flushing everything to disk as it goes into the store: mbp@hope% bzr commit -m 'import linux 2.4.19' bzr commit -m 'import linux 2.4.19' 8.15s user 2.09s system 53% cpu 19.295 total mbp@hope% time bzr diff bzr diff 5.80s user 0.52s system 69% cpu 9.128 total mbp@hope% time bzr status bzr status 5.64s user 0.43s system 68% cpu 8.848 total patch -p1 < ../linux.pkg/patch-2.4.20 1.67s user 0.96s system 90% cpu 2.905 total The diff changes 3462 files according to diffstat. branch format: Bazaar-NG branch, format 0.0.4 in the working tree: 8674 unchanged 2463 modified 818 added 229 removed 0 renamed 0 unknown 4 ignored 614 versioned subdirectories That is, 3510 entries have changed, but there are 48 changed directories so the count is exactly right! bzr commit -v -m 'import 2.4.20' 8.23s user 1.09s system 48% cpu 19.411 total Kind of strange that this takes as much time as committing the whole thing; I suppose it has to read every file. This shows many files as being renamed; I don't know why that would be. Patch to 2.4.21: 2969 files changed, 366643 insertions(+), 147759 deletions(-) After auto-add: 2969 files changed, 372168 insertions(+), 153284 deletions(-) I wonder why it is not exactly the same? Maybe because the python diff algorithm is a bit differnt to GNU diff. ---- 2005-03-29 full check, retrieving all file texts once for the 2.4 kernel branch takes 10m elapsed, 1m cpu time. lots of random IO and seeking. ---- mbp@hope% time python =bzr deleted --show-ids README README-fa1d8447b4fd0140-adbf4342752f0fc3 python =bzr deleted --show-ids 1.55s user 0.09s system 96% cpu 1.701 total mbp@hope% time python -O =bzr deleted --show-ids README README-fa1d8447b4fd0140-adbf4342752f0fc3 python -O =bzr deleted --show-ids 1.47s user 0.10s system 101% cpu 1.547 total mbp@hope% time python -O =bzr deleted --show-ids README README-fa1d8447b4fd0140-adbf4342752f0fc3 python -O =bzr deleted --show-ids 1.49s user 0.07s system 99% cpu 1.565 total mbp@hope% time python =bzr deleted --show-ids README README-fa1d8447b4fd0140-adbf4342752f0fc3 python =bzr deleted --show-ids 1.55s user 0.08s system 99% cpu 1.637 total small but significant improvement from Python -O ---- Loading a large inventory through cElementTree is pretty quick; only about 0.117s. By contrast reading the inventory into our data structure takes about 0.7s. So I think the problem must be in converting everything to InventoryEntries and back again every time. Thought about that way it seems pretty inefficient: why create all those objects when most of them aren't called on most invocations? Instead perhaps the Inventory object should hold the ElementTree and pull things out of it only as necessary? We can even have an index pointing into the ElementTree by id, path, etc. commit refs/heads/master mark :149 committer 1112221637 +1000 data 72 experiment with new nested inventory file format not used by default yet from :148 M 644 inline bzrlib/newinventory.py data 1939 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from cElementTree import Element, ElementTree, SubElement def write_inventory(inv, f): el = Element('inventory', {'version': '2'}) root = Element('root_directory', {'id': 'bogus-root-id'}) el.append(root) def descend(parent_el, ie): kind = ie.kind el = Element(kind, {'name': ie.name, 'id': ie.file_id,}) if kind == 'file': if ie.text_id: el.set('text_id', ie.text_id) if ie.text_sha1: el.set('text_sha1', ie.text_sha1) if ie.text_size != None: el.set('text_size', ('%d' % ie.text_size)) elif kind != 'directory': bailout('unknown InventoryEntry kind %r' % kind) parent_el.append(el) if kind == 'directory': l = ie.children.items() l.sort() for child_name, child_ie in l: descend(el, child_ie) # walk down through inventory, adding all directories l = inv._root.children.items() l.sort() for entry_name, ie in l: descend(root, ie) ElementTree(el).write(f, 'utf-8') f.write('\n') M 644 inline notes/new-inventory-sample.xml data 20323 M 644 inline bzrlib/commands.py data 25233 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] --revision REV Show changes since REV, rather than predecessor. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Diff selected files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): inv = Branch('.').basis_tree().inventory def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(verbose=True): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = cmdfn.__doc__ if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'commit': [], 'diff': [], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'relpath': ['filename'], 'remove': ['file+'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot prof = hotshot.Profile('.bzr.profile') ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load('.bzr.profile') #stats.strip_dirs() stats.sort_stats('time') stats.print_stats(20) else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') M 644 inline bzrlib/inventory.py data 15023 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Inventories map files to their name in a revision.""" # TODO: Maybe store inventory_id in the file? Not really needed. __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os.path, types from sets import Set try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from xml import XMLMixin from errors import bailout import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') >>> i.add(InventoryEntry('123', 'src', kind='directory')) >>> i.add(InventoryEntry('2323', 'hello.c', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id=None)) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', parent_id='123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', parent_id='123')) >>> i.add(InventoryEntry('2325', 'wibble', parent_id='123', kind='directory')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', parent_id='2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' :todo: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ def __init__(self, file_id, name, kind='file', text_id=None, parent_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c') >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c') Traceback (most recent call last): BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", []) """ if len(splitpath(name)) != 1: bailout('InventoryEntry name is not a simple filename: %r' % name) self.file_id = file_id self.name = name assert kind in ['file', 'directory'] self.kind = kind self.text_id = text_id self.parent_id = parent_id self.text_sha1 = None self.text_size = None if kind == 'directory': self.children = {} def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.text_id, self.parent_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size is not None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1', 'parent_id']: v = getattr(self, f) if v is not None: e.set(f, v) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind')) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') self.parent_id = elt.get('parent_id') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __cmp__(self, other): if self is other: return 0 if not isinstance(other, InventoryEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.name, other.name) \ or cmp(self.text_sha1, other.text_sha1) \ or cmp(self.text_size, other.text_size) \ or cmp(self.text_id, other.text_id) \ or cmp(self.parent_id, other.parent_id) \ or cmp(self.kind, other.kind) class Inventory(XMLMixin): """Inventory of versioned files in a tree. An Inventory acts like a set of InventoryEntry items. You can also look files up by their file_id or name. May be read from and written to a metadata file in a tree. To manipulate the inventory (for example to add a file), it is read in, modified, and then written back out. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c')) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ ## TODO: Make sure only canonical filenames are stored. ## TODO: Do something sensible about the possible collisions on ## case-losing filesystems. Perhaps we should just always forbid ## such collisions. ## TODO: No special cases for root, rather just give it a file id ## like everything else. ## TODO: Probably change XML serialization to use nesting def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. """ self._root = InventoryEntry(None, '', kind='directory') self._byid = {None: self._root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, parent_id=None): """Return (path, entry) pairs, in order by name.""" kids = self[parent_id].children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(parent_id=ie.file_id): yield joinpath([name, cn]), cie def directories(self): """Return (path, entry) pairs for all directories. """ yield '', self._root for path, entry in self.iter_entries(): if entry.kind == 'directory': yield path, entry def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c')) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c')) >>> inv['123123'].name 'hello.c' """ return self._byid[file_id] def get_child(self, parent_id, filename): if parent_id == None: return self._root.children.get(filename) else: return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self._byid: bailout("inventory already contains entry with id {%s}" % entry.file_id) parent = self._byid[entry.parent_id] if parent.kind != 'directory': bailout("attempt to add under non-directory {%s}" % parent.file_id) if parent.children.has_key(entry.name): bailout("%s is already versioned" % appendpath(self.id2path(parent.file_id), entry.name)) self._byid[entry.file_id] = entry parent.children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: bailout("cannot re-add root of inventory") if file_id is None: file_id = bzrlib.branch.gen_file_id(relpath) parent_id = self.path2id(parts[:-1]) ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c')) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def id_set(self): return Set(self._byid) def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c')) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __cmp__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo')) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo')) >>> i1 == i2 True """ if self is other: return 0 if not isinstance(other, Inventory): return NotImplemented if self.id_set() ^ other.id_set(): return 1 for file_id in self._byid: c = cmp(self[file_id], other[file_id]) if c: return c return 0 def id2path(self, file_id): """Return as a list the path to file_id.""" p = [] while file_id != None: ie = self._byid[file_id] p.insert(0, ie.name) file_id = ie.parent_id return '/'.join(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. """ if isinstance(name, types.StringTypes): name = splitpath(name) parent = self[None] for f in name: try: cie = parent.children[f] assert cie.name == f parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): return self._byid.has_key(file_id) if __name__ == '__main__': import doctest, inventory doctest.testmod(inventory) M 644 inline notes/performance.txt data 9452 For a tree holding 2.4.18 (two copies), 2.4.19, 2.4.20 With gzip -9: mbp@hope% du .bzr 195110 .bzr/text-store 20 .bzr/revision-store 12355 .bzr/inventory-store 216325 .bzr mbp@hope% du -s . 523128 . Without gzip: This is actually a pretty bad example because of deleting and re-importing 2.4.18, but still not totally unreasonable. ---- linux-2.4.0: 116399 kB after addding everything: 119505kB bzr status 2.68s user 0.13s system 84% cpu 3.330 total bzr commit 'import 2.4.0' 4.41s user 2.15s system 11% cpu 59.490 total 242446 . 122068 .bzr ---- Performance (2005-03-01) To add all files from linux-2.4.18: about 70s, mostly inventory serialization/deserialization. To commit: - finished, 6.520u/3.870s cpu, 33.940u/10.730s cum - 134.040 elapsed Interesting that it spends so long on external processing! I wonder if this is for running uuidgen? Let's try generating things internally. Great, this cuts it to 17.15s user 0.61s system 83% cpu 21.365 total to add, with no external command time. The commit now seems to spend most of its time copying to disk. - finished, 6.550u/3.320s cpu, 35.050u/9.870s cum - 89.650 elapsed I wonder where the external time is now? We were also using uuids() for revisions. Let's remove everything and re-add. Detecting everything was removed takes - finished, 2.460u/0.110s cpu, 0.000u/0.000s cum - 3.430 elapsed which may be mostly XML deserialization? Just getting the previous revision takes about this long: bzr invoked at Tue 2005-03-01 15:53:05.183741 EST +1100 by mbp@sourcefrog.net on hope arguments: ['/home/mbp/bin/bzr', 'get-revision-inventory', 'mbp@sourcefrog.net-20050301044608-8513202ab179aff4-44e8cd52a41aa705'] platform: Linux-2.6.10-4-686-i686-with-debian-3.1 - finished, 3.910u/0.390s cpu, 0.000u/0.000s cum - 6.690 elapsed Now committing the revision which removes all files should be fast. - finished, 1.280u/0.030s cpu, 0.000u/0.000s cum - 1.320 elapsed Now re-add with new code that doesn't call uuidgen: - finished, 1.990u/0.030s cpu, 0.000u/0.000s cum - 2.040 elapsed 16.61s user 0.55s system 74% cpu 22.965 total Status:: - finished, 2.500u/0.110s cpu, 0.010u/0.000s cum - 3.350 elapsed And commit:: Now patch up to 2.4.19. There were some bugs in handling missing directories, but with that fixed we do much better:: bzr status 5.86s user 1.06s system 10% cpu 1:05.55 total This is slow because it's diffing every file; we should use mtimes etc to make this faster. The cpu time is reasonable. I see difflib is pure Python; it might be faster to shell out to GNU diff when we need it. Export is very fast:: - finished, 4.220u/1.480s cpu, 0.010u/0.000s cum - 10.810 elapsed bzr export 1 ../linux-2.4.18.export1 3.92s user 1.72s system 21% cpu 26.030 total Now to find and add the new changes:: - finished, 2.190u/0.030s cpu, 0.000u/0.000s cum - 2.300 elapsed :: bzr commit 'import 2.4.19' 9.36s user 1.91s system 23% cpu 47.127 total And the result is exactly right. Try exporting:: mbp@hope% bzr export 4 ../linux-2.4.19.export4 bzr export 4 ../linux-2.4.19.export4 4.21s user 1.70s system 18% cpu 32.304 total and the export is exactly the same as the tarball. Now we can optimize the diff a bit more by not comparing files that have the right SHA-1 from within the commit For comparison:: patch -p1 < ../kernel.pkg/patch-2.4.20 1.61s user 1.03s system 13% cpu 19.106 total Now status after applying the .20 patch. With full-text verification:: bzr status 7.07s user 1.32s system 13% cpu 1:04.29 total with that turned off:: bzr status 5.86s user 0.56s system 25% cpu 25.577 total After adding: bzr status 6.14s user 0.61s system 25% cpu 26.583 total Should add some kind of profile counter for quick compares vs slow compares. bzr commit 'import 2.4.20' 7.57s user 1.36s system 20% cpu 43.568 total export: finished, 3.940u/1.820s cpu, 0.000u/0.000s cum, 50.990 elapsed also exports correctly now .21 bzr commit 'import 2.4.1' 5.59s user 0.51s system 60% cpu 10.122 total 265520 . 137704 .bzr import 2.4.2 317758 . 183463 .bzr with everything through to 2.4.29 imported, the .bzr directory is 1132MB, compared to 185MB for one tree. The .bzr.log is 100MB!. So the storage is 6.1 times larger, although we're holding 30 versions. It's pretty large but I think not ridiculous. By contrast the tarball for 2.4.0 is 104MB, and the tarball plus uncompressed patches are 315MB. Uncompressed, the text store is 1041MB. So it is only three times worse than patches, and could be compressed at presumably roughly equal efficiency. It is large, but also a very simple design and perhaps adequate for the moment. The text store with each file individually gziped is 264MB, which is also a very simple format and makes it less than twice the size of the source tree. This is actually rather pessimistic because I think there are some orphaned texts in there. Measured by du, the compressed full-text store is 363MB; also probably tolerable. The real fix is perhaps to use some kind of weave, not so much for storage efficiency as for fast annotation and therefore possible annotation-based merge. ----- 2005-03-25 Now we have recursive add, add is much faster. Adding all of the linux 2.4.19 kernel tree takes only finished, 5.460u/0.610s cpu, 0.010u/0.000s cum, 6.710 elapsed However, the store code currently flushes to disk after every write, which is probably excessive. So a commit takes finished, 8.740u/3.950s cpu, 0.010u/0.000s cum, 156.420 elapsed Status is now also quite fast, depsite that it still has to read all the working copies: mbp@hope% bzr status ~/work/linux-2.4.19 bzr status 5.51s user 0.79s system 99% cpu 6.337 total strace shows much of this is in write(2), probably because of logging. With more buffering on that file, removing all the explicit flushes, that is reduced to mbp@hope% time bzr status bzr status 5.23s user 0.42s system 97% cpu 5.780 total which is mostly opening, stating and reading files, as it should be. Still a few too many stat calls. Now fixed up handling of root directory. Without flushing everything to disk as it goes into the store: mbp@hope% bzr commit -m 'import linux 2.4.19' bzr commit -m 'import linux 2.4.19' 8.15s user 2.09s system 53% cpu 19.295 total mbp@hope% time bzr diff bzr diff 5.80s user 0.52s system 69% cpu 9.128 total mbp@hope% time bzr status bzr status 5.64s user 0.43s system 68% cpu 8.848 total patch -p1 < ../linux.pkg/patch-2.4.20 1.67s user 0.96s system 90% cpu 2.905 total The diff changes 3462 files according to diffstat. branch format: Bazaar-NG branch, format 0.0.4 in the working tree: 8674 unchanged 2463 modified 818 added 229 removed 0 renamed 0 unknown 4 ignored 614 versioned subdirectories That is, 3510 entries have changed, but there are 48 changed directories so the count is exactly right! bzr commit -v -m 'import 2.4.20' 8.23s user 1.09s system 48% cpu 19.411 total Kind of strange that this takes as much time as committing the whole thing; I suppose it has to read every file. This shows many files as being renamed; I don't know why that would be. Patch to 2.4.21: 2969 files changed, 366643 insertions(+), 147759 deletions(-) After auto-add: 2969 files changed, 372168 insertions(+), 153284 deletions(-) I wonder why it is not exactly the same? Maybe because the python diff algorithm is a bit differnt to GNU diff. ---- 2005-03-29 full check, retrieving all file texts once for the 2.4 kernel branch takes 10m elapsed, 1m cpu time. lots of random IO and seeking. ---- mbp@hope% time python =bzr deleted --show-ids README README-fa1d8447b4fd0140-adbf4342752f0fc3 python =bzr deleted --show-ids 1.55s user 0.09s system 96% cpu 1.701 total mbp@hope% time python -O =bzr deleted --show-ids README README-fa1d8447b4fd0140-adbf4342752f0fc3 python -O =bzr deleted --show-ids 1.47s user 0.10s system 101% cpu 1.547 total mbp@hope% time python -O =bzr deleted --show-ids README README-fa1d8447b4fd0140-adbf4342752f0fc3 python -O =bzr deleted --show-ids 1.49s user 0.07s system 99% cpu 1.565 total mbp@hope% time python =bzr deleted --show-ids README README-fa1d8447b4fd0140-adbf4342752f0fc3 python =bzr deleted --show-ids 1.55s user 0.08s system 99% cpu 1.637 total small but significant improvement from Python -O ---- Loading a large inventory through cElementTree is pretty quick; only about 0.117s. By contrast reading the inventory into our data structure takes about 0.7s. So I think the problem must be in converting everything to InventoryEntries and back again every time. Thought about that way it seems pretty inefficient: why create all those objects when most of them aren't called on most invocations? Instead perhaps the Inventory object should hold the ElementTree and pull things out of it only as necessary? We can even have an index pointing into the ElementTree by id, path, etc. as of r148 bzr deleted 1.46s user 0.08s system 98% cpu 1.561 total Alternatively maybe keep an id2path and path2id cache? Keeping it coherent may be hard... commit refs/heads/master mark :150 committer 1112223689 +1000 data 58 experiment in writing XML by hand, not through ElementTree from :149 M 644 inline bzrlib/newinventory.py data 3001 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from cElementTree import Element, ElementTree, SubElement def write_inventory(inv, f): el = Element('inventory', {'version': '2'}) root = Element('root_directory', {'id': 'bogus-root-id'}) el.append(root) def descend(parent_el, ie): kind = ie.kind el = Element(kind, {'name': ie.name, 'id': ie.file_id,}) if kind == 'file': if ie.text_id: el.set('text_id', ie.text_id) if ie.text_sha1: el.set('text_sha1', ie.text_sha1) if ie.text_size != None: el.set('text_size', ('%d' % ie.text_size)) elif kind != 'directory': bailout('unknown InventoryEntry kind %r' % kind) el.tail = '\n' parent_el.append(el) if kind == 'directory': l = ie.children.items() l.sort() for child_name, child_ie in l: descend(el, child_ie) # walk down through inventory, adding all directories l = inv._root.children.items() l.sort() for entry_name, ie in l: descend(root, ie) ElementTree(el).write(f, 'utf-8') f.write('\n') def write_slacker_inventory(inv, f): def descend(ie): kind = ie.kind f.write('<%s name="%s" id="%s" ' % (kind, ie.name, ie.file_id)) if kind == 'file': if ie.text_id: f.write('text_id="%s" ' % ie.text_id) if ie.text_sha1: f.write('text_sha1="%s" ' % ie.text_sha1) if ie.text_size != None: f.write('text_size="%d" ' % ie.text_size) f.write('/>\n') elif kind == 'directory': f.write('>\n') l = ie.children.items() l.sort() for child_name, child_ie in l: descend(child_ie) f.write('\n') else: bailout('unknown InventoryEntry kind %r' % kind) f.write('\n') f.write('\n') l = inv._root.children.items() l.sort() for entry_name, ie in l: descend(ie) f.write('\n') f.write('\n') commit refs/heads/master mark :151 committer 1112225664 +1000 data 42 experimental nested-inventory load support from :150 M 644 inline bzrlib/commands.py data 25350 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] --revision REV Show changes since REV, rather than predecessor. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Diff selected files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): inv = Branch('.').basis_tree().inventory def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(verbose=True): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = cmdfn.__doc__ if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'commit': [], 'diff': [], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'relpath': ['filename'], 'remove': ['file+'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot prof = hotshot.Profile('.bzr.profile') ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load('.bzr.profile') #stats.strip_dirs() stats.sort_stats('time') stats.print_stats(20) else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') M 644 inline bzrlib/newinventory.py data 3939 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from cElementTree import Element, ElementTree, SubElement def write_inventory(inv, f): el = Element('inventory', {'version': '2'}) root = Element('root_directory', {'id': 'bogus-root-id'}) el.append(root) def descend(parent_el, ie): kind = ie.kind el = Element(kind, {'name': ie.name, 'id': ie.file_id,}) if kind == 'file': if ie.text_id: el.set('text_id', ie.text_id) if ie.text_sha1: el.set('text_sha1', ie.text_sha1) if ie.text_size != None: el.set('text_size', ('%d' % ie.text_size)) elif kind != 'directory': bailout('unknown InventoryEntry kind %r' % kind) el.tail = '\n' parent_el.append(el) if kind == 'directory': l = ie.children.items() l.sort() for child_name, child_ie in l: descend(el, child_ie) # walk down through inventory, adding all directories l = inv._root.children.items() l.sort() for entry_name, ie in l: descend(root, ie) ElementTree(el).write(f, 'utf-8') f.write('\n') def write_slacker_inventory(inv, f): def descend(ie): kind = ie.kind f.write('<%s name="%s" id="%s" ' % (kind, ie.name, ie.file_id)) if kind == 'file': if ie.text_id: f.write('text_id="%s" ' % ie.text_id) if ie.text_sha1: f.write('text_sha1="%s" ' % ie.text_sha1) if ie.text_size != None: f.write('text_size="%d" ' % ie.text_size) f.write('/>\n') elif kind == 'directory': f.write('>\n') l = ie.children.items() l.sort() for child_name, child_ie in l: descend(child_ie) f.write('\n') else: bailout('unknown InventoryEntry kind %r' % kind) f.write('\n') f.write('\n') l = inv._root.children.items() l.sort() for entry_name, ie in l: descend(ie) f.write('\n') f.write('\n') def read_new_inventory(f): from inventory import Inventory, InventoryEntry def descend(parent_ie, el): kind = el.tag name = el.get('name') file_id = el.get('id') ie = InventoryEntry(file_id, name, el.tag) parent_ie.children[name] = ie inv._byid[file_id] = ie if kind == 'directory': for child_el in el: descend(ie, child_el) elif kind == 'file': assert len(el) == 0 ie.text_id = el.get('text_id') v = el.get('text_size') ie.text_size = v and int(v) ie.text_sha1 = el.get('text_sha1') else: bailout("unknown inventory entry %r" % kind) inv_el = ElementTree().parse(f) assert inv_el.tag == 'inventory' root_el = inv_el[0] assert root_el.tag == 'root_directory' inv = Inventory() for el in root_el: descend(inv._root, el) commit refs/heads/master mark :152 committer 1112227268 +1000 data 48 order in which unknowns are reported has changed from :151 M 644 inline bzrlib/tests.py data 5574 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # XXX: We might prefer these to be in a text file rather than Python # source, but that only works in doctest from Python 2.4 and later, # which is not present in Warty. r""" Bazaar-NG test cases ******************** These are run by ``bzr.doctest``. >>> import bzrlib, os >>> from bzrlib import ScratchBranch >>> bzrlib.commands.cmd_rocks() it sure does! Hey, nice place to begin. The basic object is a Branch. We have a special helper class ScratchBranch that automatically makes a directory and cleans itself up, but is in other respects identical. ScratchBranches are initially empty: >>> b = bzrlib.ScratchBranch() >>> b.show_status() New files in that directory are, it is initially unknown: >>> file(b.base + '/hello.c', 'wt').write('int main() {}') >>> b.show_status() ? hello.c That's not quite true; some files (like editor backups) are ignored by default: >>> file(b.base + '/hello.c~', 'wt').write('int main() {}') >>> b.show_status() ? hello.c >>> list(b.unknowns()) ['hello.c'] The ``add`` command marks a file to be added in the next revision: >>> b.add('hello.c') >>> b.show_status() A hello.c You can also add files that otherwise would be ignored. The ignore patterns only apply to files that would be otherwise unknown, so they have no effect once it's added. >>> b.add('hello.c~') >>> b.show_status() A hello.c A hello.c~ It is an error to add a file that isn't present in the working copy: >>> b.add('nothere') Traceback (most recent call last): ... BzrError: ('cannot add: not a regular file or directory: nothere', []) If we add a file and then change our mind, we can either revert it or remove the file. If we revert, we are left with the working copy (in either I or ? state). If we remove, the working copy is gone. Let's do that to the backup, presumably added accidentally. >>> b.remove('hello.c~') >>> b.show_status() A hello.c Now to commit, creating a new revision. (Fake the date and name for reproducibility.) >>> b.commit('start hello world', timestamp=0, committer='foo@nowhere') >>> b.show_status() >>> b.show_status(show_all=True) . hello.c I hello.c~ We can look back at history >>> r = b.get_revision(b.lookup_revision(1)) >>> r.message 'start hello world' >>> b.write_log(show_timezone='utc') ---------------------------------------- revno: 1 committer: foo@nowhere timestamp: Thu 1970-01-01 00:00:00 +0000 message: start hello world (The other fields will be a bit unpredictable, depending on who ran this test and when.) As of 2005-02-21, we can also add subdirectories to the revision! >>> os.mkdir(b.base + "/lib") >>> b.show_status() ? lib/ >>> b.add('lib') >>> b.show_status() A lib/ >>> b.commit('add subdir') >>> b.show_status() >>> b.show_status(show_all=True) . hello.c I hello.c~ . lib/ and we can also add files within subdirectories: >>> file(b.base + '/lib/hello', 'w').write('hello!\n') >>> b.show_status() ? lib/hello Tests for adding subdirectories, etc. >>> b = bzrlib.branch.ScratchBranch() >>> os.mkdir(b.abspath('d1')) >>> os.mkdir(b.abspath('d2')) >>> os.mkdir(b.abspath('d2/d3')) >>> list(b.working_tree().unknowns()) ['d1', 'd2'] Create some files, but they're not seen as unknown yet: >>> file(b.abspath('d1/f1'), 'w').close() >>> file(b.abspath('d2/f2'), 'w').close() >>> file(b.abspath('d2/f3'), 'w').close() >>> [v[0] for v in b.inventory.directories()] [''] >>> list(b.working_tree().unknowns()) ['d1', 'd2'] Adding a directory, and we see the file underneath: >>> b.add('d1') >>> [v[0] for v in b.inventory.directories()] ['', 'd1'] >>> list(b.working_tree().unknowns()) ['d2', 'd1/f1'] >>> # d2 comes first because it's in the top directory >>> b.add('d2') >>> b.commit('add some stuff') >>> list(b.working_tree().unknowns()) ['d1/f1', 'd2/d3', 'd2/f2', 'd2/f3'] >>> b.add('d1/f1') >>> list(b.working_tree().unknowns()) ['d2/d3', 'd2/f2', 'd2/f3'] Tests for ignored files and patterns: >>> b = ScratchBranch(dirs=['src', 'doc'], ... files=['configure.in', 'configure', ... 'doc/configure', 'foo.c', ... 'foo']) >>> list(b.unknowns()) ['configure', 'configure.in', 'doc', 'foo', 'foo.c', 'src'] >>> b.add(['doc', 'foo.c', 'src', 'configure.in']) >>> list(b.unknowns()) ['configure', 'foo', 'doc/configure'] >>> f = file(b.abspath('.bzrignore'), 'w') >>> f.write('./configure\n' ... './foo\n') >>> f.close() >>> b.add('.bzrignore') >>> list(b.unknowns()) ['configure', 'doc/configure', 'foo'] """ commit refs/heads/master mark :153 committer 1112227285 +1000 data 43 update test for new ignore-pattern handling from :152 M 644 inline bzrlib/tests.py data 5554 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # XXX: We might prefer these to be in a text file rather than Python # source, but that only works in doctest from Python 2.4 and later, # which is not present in Warty. r""" Bazaar-NG test cases ******************** These are run by ``bzr.doctest``. >>> import bzrlib, os >>> from bzrlib import ScratchBranch >>> bzrlib.commands.cmd_rocks() it sure does! Hey, nice place to begin. The basic object is a Branch. We have a special helper class ScratchBranch that automatically makes a directory and cleans itself up, but is in other respects identical. ScratchBranches are initially empty: >>> b = bzrlib.ScratchBranch() >>> b.show_status() New files in that directory are, it is initially unknown: >>> file(b.base + '/hello.c', 'wt').write('int main() {}') >>> b.show_status() ? hello.c That's not quite true; some files (like editor backups) are ignored by default: >>> file(b.base + '/hello.c~', 'wt').write('int main() {}') >>> b.show_status() ? hello.c >>> list(b.unknowns()) ['hello.c'] The ``add`` command marks a file to be added in the next revision: >>> b.add('hello.c') >>> b.show_status() A hello.c You can also add files that otherwise would be ignored. The ignore patterns only apply to files that would be otherwise unknown, so they have no effect once it's added. >>> b.add('hello.c~') >>> b.show_status() A hello.c A hello.c~ It is an error to add a file that isn't present in the working copy: >>> b.add('nothere') Traceback (most recent call last): ... BzrError: ('cannot add: not a regular file or directory: nothere', []) If we add a file and then change our mind, we can either revert it or remove the file. If we revert, we are left with the working copy (in either I or ? state). If we remove, the working copy is gone. Let's do that to the backup, presumably added accidentally. >>> b.remove('hello.c~') >>> b.show_status() A hello.c Now to commit, creating a new revision. (Fake the date and name for reproducibility.) >>> b.commit('start hello world', timestamp=0, committer='foo@nowhere') >>> b.show_status() >>> b.show_status(show_all=True) . hello.c I hello.c~ We can look back at history >>> r = b.get_revision(b.lookup_revision(1)) >>> r.message 'start hello world' >>> b.write_log(show_timezone='utc') ---------------------------------------- revno: 1 committer: foo@nowhere timestamp: Thu 1970-01-01 00:00:00 +0000 message: start hello world (The other fields will be a bit unpredictable, depending on who ran this test and when.) As of 2005-02-21, we can also add subdirectories to the revision! >>> os.mkdir(b.base + "/lib") >>> b.show_status() ? lib/ >>> b.add('lib') >>> b.show_status() A lib/ >>> b.commit('add subdir') >>> b.show_status() >>> b.show_status(show_all=True) . hello.c I hello.c~ . lib/ and we can also add files within subdirectories: >>> file(b.base + '/lib/hello', 'w').write('hello!\n') >>> b.show_status() ? lib/hello Tests for adding subdirectories, etc. >>> b = bzrlib.branch.ScratchBranch() >>> os.mkdir(b.abspath('d1')) >>> os.mkdir(b.abspath('d2')) >>> os.mkdir(b.abspath('d2/d3')) >>> list(b.working_tree().unknowns()) ['d1', 'd2'] Create some files, but they're not seen as unknown yet: >>> file(b.abspath('d1/f1'), 'w').close() >>> file(b.abspath('d2/f2'), 'w').close() >>> file(b.abspath('d2/f3'), 'w').close() >>> [v[0] for v in b.inventory.directories()] [''] >>> list(b.working_tree().unknowns()) ['d1', 'd2'] Adding a directory, and we see the file underneath: >>> b.add('d1') >>> [v[0] for v in b.inventory.directories()] ['', 'd1'] >>> list(b.working_tree().unknowns()) ['d2', 'd1/f1'] >>> # d2 comes first because it's in the top directory >>> b.add('d2') >>> b.commit('add some stuff') >>> list(b.working_tree().unknowns()) ['d1/f1', 'd2/d3', 'd2/f2', 'd2/f3'] >>> b.add('d1/f1') >>> list(b.working_tree().unknowns()) ['d2/d3', 'd2/f2', 'd2/f3'] Tests for ignored files and patterns: >>> b = ScratchBranch(dirs=['src', 'doc'], ... files=['configure.in', 'configure', ... 'doc/configure', 'foo.c', ... 'foo']) >>> list(b.unknowns()) ['configure', 'configure.in', 'doc', 'foo', 'foo.c', 'src'] >>> b.add(['doc', 'foo.c', 'src', 'configure.in']) >>> list(b.unknowns()) ['configure', 'foo', 'doc/configure'] >>> f = file(b.abspath('.bzrignore'), 'w') >>> f.write('./configure\n' ... './foo\n') >>> f.close() >>> b.add('.bzrignore') >>> list(b.unknowns()) ['doc/configure'] """ commit refs/heads/master mark :154 committer 1112227494 +1000 data 25 prepare for 0.0.2 release from :153 M 644 inline NEWS data 1413 bzr-0.0.2 "black cube" 2003-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/__init__.py data 1485 # (C) 2005 Canonical Development Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr library""" from inventory import Inventory, InventoryEntry from branch import Branch, ScratchBranch from osutils import format_date from tree import Tree from diff import diff_trees from trace import mutter, warning import add BZRDIR = ".bzr" DEFAULT_IGNORE = ['.bzr.log', '*~', '#*#', '*$', '.#*', '*.tmp', '*.bak', '*.BAK', '*.orig', '*.o', '*.obj', '*.a', '*.py[oc]', '*.so', '*.exe', '*.elc', '{arch}', 'CVS', '.svn', '_darcs', 'SCCS', 'RCS', 'TAGS', '.make.state', '.sconsign'] IGNORE_FILENAME = ".bzrignore" __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __version__ = '0.0.2' commit refs/heads/master mark :155 committer 1112239461 +1000 data 54 add new explicit RootEntry to inventory (in-core only) from :154 M 644 inline NEWS data 1529 bzr-0.0.3 NOT RELEASED YET INTERNAL: * Refactored inventory storage to insert a root entry at the top. bzr-0.0.2 "black cube" 2003-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/inventory.py data 15718 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Inventories map files to their name in a revision.""" # TODO: Maybe store inventory_id in the file? Not really needed. __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os.path, types from sets import Set try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from xml import XMLMixin from errors import bailout import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') >>> i.add(InventoryEntry('123', 'src', kind='directory')) >>> i.add(InventoryEntry('2323', 'hello.c', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id=None)) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', parent_id='123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', parent_id='123')) >>> i.add(InventoryEntry('2325', 'wibble', parent_id='123', kind='directory')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', parent_id='2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' :todo: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ def __init__(self, file_id, name, kind='file', text_id=None, parent_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c') >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c') Traceback (most recent call last): BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", []) """ if len(splitpath(name)) != 1: bailout('InventoryEntry name is not a simple filename: %r' % name) self.file_id = file_id self.name = name assert kind in ['file', 'directory'] self.kind = kind self.text_id = text_id self.parent_id = parent_id self.text_sha1 = None self.text_size = None if kind == 'directory': self.children = {} def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.text_id, self.parent_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size is not None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1', 'parent_id']: v = getattr(self, f) if v is not None: e.set(f, v) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind')) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') self.parent_id = elt.get('parent_id') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __cmp__(self, other): if self is other: return 0 if not isinstance(other, InventoryEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.name, other.name) \ or cmp(self.text_sha1, other.text_sha1) \ or cmp(self.text_size, other.text_size) \ or cmp(self.text_id, other.text_id) \ or cmp(self.parent_id, other.parent_id) \ or cmp(self.kind, other.kind) class RootEntry(InventoryEntry): def __init__(self, file_id): self.file_id = file_id self.children = {} self.kind = 'root_directory' self.parent_id = None def __cmp__(self, other): if self is other: return 0 if not isinstance(other, RootEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.children, other.children) class Inventory(XMLMixin): """Inventory of versioned files in a tree. An Inventory acts like a set of InventoryEntry items. You can also look files up by their file_id or name. May be read from and written to a metadata file in a tree. To manipulate the inventory (for example to add a file), it is read in, modified, and then written back out. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted, other than through the Inventory API. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c')) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ ## TODO: Make sure only canonical filenames are stored. ## TODO: Do something sensible about the possible collisions on ## case-losing filesystems. Perhaps we should just always forbid ## such collisions. ## TODO: No special cases for root, rather just give it a file id ## like everything else. ## TODO: Probably change XML serialization to use nesting def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. The inventory is created with a default root directory, with an id of None. """ self.root = RootEntry(None) self._byid = {None: self.root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, from_dir=None): """Return (path, entry) pairs, in order by name.""" if from_dir == None: assert self.root from_dir = self.root elif isinstance(from_dir, basestring): from_dir = self._byid[from_dir] kids = from_dir.children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(from_dir=ie.file_id): yield '/'.join((name, cn)), cie def directories(self, from_dir=None): """Return (path, entry) pairs for all directories. """ assert self.root yield '', self.root for path, entry in self.iter_entries(): if entry.kind == 'directory': yield path, entry def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c')) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c')) >>> inv['123123'].name 'hello.c' """ return self._byid[file_id] def get_child(self, parent_id, filename): return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self._byid: bailout("inventory already contains entry with id {%s}" % entry.file_id) try: parent = self._byid[entry.parent_id] except KeyError: bailout("parent_id %r not in inventory" % entry.parent_id) if parent.children.has_key(entry.name): bailout("%s is already versioned" % appendpath(self.id2path(parent.file_id), entry.name)) self._byid[entry.file_id] = entry parent.children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: bailout("cannot re-add root of inventory") if file_id is None: file_id = bzrlib.branch.gen_file_id(relpath) parent_id = self.path2id(parts[:-1]) ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c')) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def id_set(self): return Set(self._byid) def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c')) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __cmp__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo')) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo')) >>> i1 == i2 True """ if self is other: return 0 if not isinstance(other, Inventory): return NotImplemented if self.id_set() ^ other.id_set(): return 1 for file_id in self._byid: c = cmp(self[file_id], other[file_id]) if c: return c return 0 def id2path(self, file_id): """Return as a list the path to file_id.""" p = [] while file_id != None: ie = self._byid[file_id] p.insert(0, ie.name) file_id = ie.parent_id return '/'.join(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. """ if isinstance(name, types.StringTypes): name = splitpath(name) parent = self[None] for f in name: try: cie = parent.children[f] assert cie.name == f parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): return self._byid.has_key(file_id) if __name__ == '__main__': import doctest, inventory doctest.testmod(inventory) commit refs/heads/master mark :156 committer 1112245340 +1000 data 25 new "directories" command from :155 M 644 inline NEWS data 1635 bzr-0.0.3 NOT RELEASED YET INTERNAL: * Refactored inventory storage to insert a root entry at the top. ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. bzr-0.0.2 "black cube" 2003-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 25528 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] --revision REV Show changes since REV, rather than predecessor. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Diff selected files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): inv = Branch('.').basis_tree().inventory def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(verbose=True): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = cmdfn.__doc__ if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'commit': [], 'diff': [], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'relpath': ['filename'], 'remove': ['file+'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot prof = hotshot.Profile('.bzr.profile') ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load('.bzr.profile') #stats.strip_dirs() stats.sort_stats('time') stats.print_stats(20) else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') M 644 inline bzrlib/inventory.py data 16332 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Inventories map files to their name in a revision.""" # TODO: Maybe store inventory_id in the file? Not really needed. __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os.path, types from sets import Set try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from xml import XMLMixin from errors import bailout import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') >>> i.add(InventoryEntry('123', 'src', kind='directory')) >>> i.add(InventoryEntry('2323', 'hello.c', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id=None)) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', parent_id='123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', parent_id='123')) >>> i.add(InventoryEntry('2325', 'wibble', parent_id='123', kind='directory')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', parent_id='2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' :todo: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ def __init__(self, file_id, name, kind='file', text_id=None, parent_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c') >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c') Traceback (most recent call last): BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", []) """ if len(splitpath(name)) != 1: bailout('InventoryEntry name is not a simple filename: %r' % name) self.file_id = file_id self.name = name assert kind in ['file', 'directory'] self.kind = kind self.text_id = text_id self.parent_id = parent_id self.text_sha1 = None self.text_size = None if kind == 'directory': self.children = {} def sorted_children(self): l = self.children.items() l.sort() return l def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.text_id, self.parent_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size is not None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1', 'parent_id']: v = getattr(self, f) if v is not None: e.set(f, v) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind')) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') self.parent_id = elt.get('parent_id') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __cmp__(self, other): if self is other: return 0 if not isinstance(other, InventoryEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.name, other.name) \ or cmp(self.text_sha1, other.text_sha1) \ or cmp(self.text_size, other.text_size) \ or cmp(self.text_id, other.text_id) \ or cmp(self.parent_id, other.parent_id) \ or cmp(self.kind, other.kind) class RootEntry(InventoryEntry): def __init__(self, file_id): self.file_id = file_id self.children = {} self.kind = 'root_directory' self.parent_id = None self.name = '' def __cmp__(self, other): if self is other: return 0 if not isinstance(other, RootEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.children, other.children) class Inventory(XMLMixin): """Inventory of versioned files in a tree. An Inventory acts like a set of InventoryEntry items. You can also look files up by their file_id or name. May be read from and written to a metadata file in a tree. To manipulate the inventory (for example to add a file), it is read in, modified, and then written back out. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted, other than through the Inventory API. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c')) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ ## TODO: Make sure only canonical filenames are stored. ## TODO: Do something sensible about the possible collisions on ## case-losing filesystems. Perhaps we should just always forbid ## such collisions. ## TODO: No special cases for root, rather just give it a file id ## like everything else. ## TODO: Probably change XML serialization to use nesting def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. The inventory is created with a default root directory, with an id of None. """ self.root = RootEntry(None) self._byid = {None: self.root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, from_dir=None): """Return (path, entry) pairs, in order by name.""" if from_dir == None: assert self.root from_dir = self.root elif isinstance(from_dir, basestring): from_dir = self._byid[from_dir] kids = from_dir.children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(from_dir=ie.file_id): yield '/'.join((name, cn)), cie def directories(self, from_dir=None): """Return (path, entry) pairs for all directories. """ def descend(parent_ie): parent_name = parent_ie.name if parent_name == '': parent_name = '.' yield parent_name, parent_ie # directory children in sorted order dn = [] for ie in parent_ie.children.itervalues(): if ie.kind == 'directory': dn.append((ie.name, ie)) dn.sort() for name, child_ie in dn: for sub_name, sub_ie in descend(child_ie): yield appendpath(parent_name, sub_name), sub_ie for name, ie in descend(self.root): yield name, ie def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c')) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c')) >>> inv['123123'].name 'hello.c' """ return self._byid[file_id] def get_child(self, parent_id, filename): return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self._byid: bailout("inventory already contains entry with id {%s}" % entry.file_id) try: parent = self._byid[entry.parent_id] except KeyError: bailout("parent_id %r not in inventory" % entry.parent_id) if parent.children.has_key(entry.name): bailout("%s is already versioned" % appendpath(self.id2path(parent.file_id), entry.name)) self._byid[entry.file_id] = entry parent.children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: bailout("cannot re-add root of inventory") if file_id is None: file_id = bzrlib.branch.gen_file_id(relpath) parent_id = self.path2id(parts[:-1]) ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c')) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def id_set(self): return Set(self._byid) def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c')) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __cmp__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo')) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo')) >>> i1 == i2 True """ if self is other: return 0 if not isinstance(other, Inventory): return NotImplemented if self.id_set() ^ other.id_set(): return 1 for file_id in self._byid: c = cmp(self[file_id], other[file_id]) if c: return c return 0 def id2path(self, file_id): """Return as a list the path to file_id.""" p = [] while file_id != None: ie = self._byid[file_id] p.insert(0, ie.name) file_id = ie.parent_id return '/'.join(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. """ if isinstance(name, types.StringTypes): name = splitpath(name) parent = self[None] for f in name: try: cie = parent.children[f] assert cie.name == f parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): return self._byid.has_key(file_id) if __name__ == '__main__': import doctest, inventory doctest.testmod(inventory) commit refs/heads/master mark :157 committer 1112330308 +1000 data 22 fix test case breakage from :156 M 644 inline bzrlib/commands.py data 25626 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] --revision REV Show changes since REV, rather than predecessor. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Diff selected files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): inv = Branch('.').basis_tree().inventory def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(verbose=True): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = cmdfn.__doc__ if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'commit': [], 'diff': [], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'relpath': ['filename'], 'remove': ['file+'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot prof = hotshot.Profile('.bzr.profile') ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load('.bzr.profile') #stats.strip_dirs() stats.sort_stats('time') stats.print_stats(20) else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') M 644 inline bzrlib/inventory.py data 16264 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Inventories map files to their name in a revision.""" # TODO: Maybe store inventory_id in the file? Not really needed. __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os.path, types from sets import Set try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from xml import XMLMixin from errors import bailout import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') >>> i.add(InventoryEntry('123', 'src', kind='directory')) >>> i.add(InventoryEntry('2323', 'hello.c', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id=None)) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', parent_id='123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', parent_id='123')) >>> i.add(InventoryEntry('2325', 'wibble', parent_id='123', kind='directory')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', parent_id='2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' :todo: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ def __init__(self, file_id, name, kind='file', text_id=None, parent_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c') >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c') Traceback (most recent call last): BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", []) """ if len(splitpath(name)) != 1: bailout('InventoryEntry name is not a simple filename: %r' % name) self.file_id = file_id self.name = name assert kind in ['file', 'directory'] self.kind = kind self.text_id = text_id self.parent_id = parent_id self.text_sha1 = None self.text_size = None if kind == 'directory': self.children = {} def sorted_children(self): l = self.children.items() l.sort() return l def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.text_id, self.parent_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size is not None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1', 'parent_id']: v = getattr(self, f) if v is not None: e.set(f, v) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind')) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') self.parent_id = elt.get('parent_id') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __cmp__(self, other): if self is other: return 0 if not isinstance(other, InventoryEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.name, other.name) \ or cmp(self.text_sha1, other.text_sha1) \ or cmp(self.text_size, other.text_size) \ or cmp(self.text_id, other.text_id) \ or cmp(self.parent_id, other.parent_id) \ or cmp(self.kind, other.kind) class RootEntry(InventoryEntry): def __init__(self, file_id): self.file_id = file_id self.children = {} self.kind = 'root_directory' self.parent_id = None self.name = '' def __cmp__(self, other): if self is other: return 0 if not isinstance(other, RootEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.children, other.children) class Inventory(XMLMixin): """Inventory of versioned files in a tree. An Inventory acts like a set of InventoryEntry items. You can also look files up by their file_id or name. May be read from and written to a metadata file in a tree. To manipulate the inventory (for example to add a file), it is read in, modified, and then written back out. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted, other than through the Inventory API. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c')) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ ## TODO: Make sure only canonical filenames are stored. ## TODO: Do something sensible about the possible collisions on ## case-losing filesystems. Perhaps we should just always forbid ## such collisions. ## TODO: No special cases for root, rather just give it a file id ## like everything else. ## TODO: Probably change XML serialization to use nesting def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. The inventory is created with a default root directory, with an id of None. """ self.root = RootEntry(None) self._byid = {None: self.root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, from_dir=None): """Return (path, entry) pairs, in order by name.""" if from_dir == None: assert self.root from_dir = self.root elif isinstance(from_dir, basestring): from_dir = self._byid[from_dir] kids = from_dir.children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(from_dir=ie.file_id): yield '/'.join((name, cn)), cie def directories(self, from_dir=None): """Return (path, entry) pairs for all directories. """ def descend(parent_ie): parent_name = parent_ie.name yield parent_name, parent_ie # directory children in sorted order dn = [] for ie in parent_ie.children.itervalues(): if ie.kind == 'directory': dn.append((ie.name, ie)) dn.sort() for name, child_ie in dn: for sub_name, sub_ie in descend(child_ie): yield appendpath(parent_name, sub_name), sub_ie for name, ie in descend(self.root): yield name, ie def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c')) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c')) >>> inv['123123'].name 'hello.c' """ return self._byid[file_id] def get_child(self, parent_id, filename): return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self._byid: bailout("inventory already contains entry with id {%s}" % entry.file_id) try: parent = self._byid[entry.parent_id] except KeyError: bailout("parent_id %r not in inventory" % entry.parent_id) if parent.children.has_key(entry.name): bailout("%s is already versioned" % appendpath(self.id2path(parent.file_id), entry.name)) self._byid[entry.file_id] = entry parent.children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: bailout("cannot re-add root of inventory") if file_id is None: file_id = bzrlib.branch.gen_file_id(relpath) parent_id = self.path2id(parts[:-1]) ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c')) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def id_set(self): return Set(self._byid) def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c')) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __cmp__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo')) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo')) >>> i1 == i2 True """ if self is other: return 0 if not isinstance(other, Inventory): return NotImplemented if self.id_set() ^ other.id_set(): return 1 for file_id in self._byid: c = cmp(self[file_id], other[file_id]) if c: return c return 0 def id2path(self, file_id): """Return as a list the path to file_id.""" p = [] while file_id != None: ie = self._byid[file_id] p.insert(0, ie.name) file_id = ie.parent_id return '/'.join(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. """ if isinstance(name, types.StringTypes): name = splitpath(name) parent = self[None] for f in name: try: cie = parent.children[f] assert cie.name == f parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): return self._byid.has_key(file_id) if __name__ == '__main__': import doctest, inventory doctest.testmod(inventory) M 644 inline bzrlib/tests.py data 5605 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # XXX: We might prefer these to be in a text file rather than Python # source, but that only works in doctest from Python 2.4 and later, # which is not present in Warty. r""" Bazaar-NG test cases ******************** These are run by ``bzr.doctest``. >>> import bzrlib, os >>> from bzrlib import ScratchBranch >>> bzrlib.commands.cmd_rocks() it sure does! Hey, nice place to begin. The basic object is a Branch. We have a special helper class ScratchBranch that automatically makes a directory and cleans itself up, but is in other respects identical. ScratchBranches are initially empty: >>> b = bzrlib.ScratchBranch() >>> b.show_status() New files in that directory are, it is initially unknown: >>> file(b.base + '/hello.c', 'wt').write('int main() {}') >>> b.show_status() ? hello.c That's not quite true; some files (like editor backups) are ignored by default: >>> file(b.base + '/hello.c~', 'wt').write('int main() {}') >>> b.show_status() ? hello.c >>> list(b.unknowns()) ['hello.c'] The ``add`` command marks a file to be added in the next revision: >>> b.add('hello.c') >>> b.show_status() A hello.c You can also add files that otherwise would be ignored. The ignore patterns only apply to files that would be otherwise unknown, so they have no effect once it's added. >>> b.add('hello.c~') >>> b.show_status() A hello.c A hello.c~ It is an error to add a file that isn't present in the working copy: >>> b.add('nothere') Traceback (most recent call last): ... BzrError: ('cannot add: not a regular file or directory: nothere', []) If we add a file and then change our mind, we can either revert it or remove the file. If we revert, we are left with the working copy (in either I or ? state). If we remove, the working copy is gone. Let's do that to the backup, presumably added accidentally. >>> b.remove('hello.c~') >>> b.show_status() A hello.c Now to commit, creating a new revision. (Fake the date and name for reproducibility.) >>> b.commit('start hello world', timestamp=0, committer='foo@nowhere') >>> b.show_status() >>> b.show_status(show_all=True) . hello.c I hello.c~ We can look back at history >>> r = b.get_revision(b.lookup_revision(1)) >>> r.message 'start hello world' >>> b.write_log(show_timezone='utc') ---------------------------------------- revno: 1 committer: foo@nowhere timestamp: Thu 1970-01-01 00:00:00 +0000 message: start hello world (The other fields will be a bit unpredictable, depending on who ran this test and when.) As of 2005-02-21, we can also add subdirectories to the revision! >>> os.mkdir(b.base + "/lib") >>> b.show_status() ? lib/ >>> b.add('lib') >>> b.show_status() A lib/ >>> b.commit('add subdir') >>> b.show_status() >>> b.show_status(show_all=True) . hello.c I hello.c~ . lib/ and we can also add files within subdirectories: >>> file(b.base + '/lib/hello', 'w').write('hello!\n') >>> b.show_status() ? lib/hello Tests for adding subdirectories, etc. >>> b = bzrlib.branch.ScratchBranch() >>> os.mkdir(b.abspath('d1')) >>> os.mkdir(b.abspath('d2')) >>> os.mkdir(b.abspath('d2/d3')) >>> list(b.working_tree().unknowns()) ['d1', 'd2'] Create some files, but they're not seen as unknown yet: >>> file(b.abspath('d1/f1'), 'w').close() >>> file(b.abspath('d2/f2'), 'w').close() >>> file(b.abspath('d2/f3'), 'w').close() >>> [v[0] for v in b.inventory.directories()] [''] >>> list(b.working_tree().unknowns()) ['d1', 'd2'] Adding a directory, and we see the file underneath: >>> b.add('d1') >>> [v[0] for v in b.inventory.directories()] ['', 'd1'] >>> list(b.working_tree().unknowns()) ['d2', 'd1/f1'] >>> # d2 comes first because it's in the top directory >>> b.add('d2') >>> b.commit('add some stuff') >>> list(b.working_tree().unknowns()) ['d1/f1', 'd2/d3', 'd2/f2', 'd2/f3'] >>> b.add('d1/f1') >>> list(b.working_tree().unknowns()) ['d2/d3', 'd2/f2', 'd2/f3'] Tests for ignored files and patterns: >>> b = ScratchBranch(dirs=['src', 'doc'], ... files=['configure.in', 'configure', ... 'doc/configure', 'foo.c', ... 'foo']) >>> list(b.unknowns()) ['configure', 'configure.in', 'doc', 'foo', 'foo.c', 'src'] >>> b.add(['doc', 'foo.c', 'src', 'configure.in']) >>> list(b.unknowns()) ['configure', 'foo', 'doc/configure'] >>> f = file(b.abspath('.bzrignore'), 'w') >>> f.write('./configure\n' ... './foo\n') >>> f.close() >>> b.add('.bzrignore') >>> list(b.unknowns()) ['doc/configure'] Detection of missing files and directories: """ M 644 inline bzrlib/tree.py data 13677 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tree classes, representing directory at point in time. """ from sets import Set import os.path, os, fnmatch from osutils import pumpfile, compare_files, filesize, quotefn, sha_file, \ joinpath, splitpath, appendpath, isdir, isfile, file_kind, fingerprint_file import errno from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE from inventory import Inventory from trace import mutter, note from errors import bailout import branch import bzrlib class Tree: """Abstract file tree. There are several subclasses: * `WorkingTree` exists as files on disk editable by the user. * `RevisionTree` is a tree as recorded at some point in the past. * `EmptyTree` Trees contain an `Inventory` object, and also know how to retrieve file texts mentioned in the inventory, either from a working directory or from a store. It is possible for trees to contain files that are not described in their inventory or vice versa; for this use `filenames()`. Trees can be compared, etc, regardless of whether they are working trees or versioned trees. """ def has_filename(self, filename): """True if the tree has given filename.""" raise NotImplementedError() def has_id(self, file_id): return self.inventory.has_id(file_id) def id_set(self): """Return set of all ids in this tree.""" return self.inventory.id_set() def id2path(self, file_id): return self.inventory.id2path(file_id) def _get_inventory(self): return self._inventory inventory = property(_get_inventory, doc="Inventory of this Tree") def _check_retrieved(self, ie, f): fp = fingerprint_file(f) f.seek(0) if ie.text_size is not None: if ie.text_size != fp['size']: bailout("mismatched size for file %r in %r" % (ie.file_id, self._store), ["inventory expects %d bytes" % ie.text_size, "file is actually %d bytes" % fp['size'], "store is probably damaged/corrupt"]) if ie.text_sha1 != fp['sha1']: bailout("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store), ["inventory expects %s" % ie.text_sha1, "file is actually %s" % fp['sha1'], "store is probably damaged/corrupt"]) def export(self, dest): """Export this tree to a new directory. `dest` should not exist, and will be created holding the contents of this tree. :todo: To handle subdirectories we need to create the directories first. :note: If the export fails, the destination directory will be left in a half-assed state. """ os.mkdir(dest) mutter('export version %r' % self) inv = self.inventory for dp, ie in inv.iter_entries(): kind = ie.kind fullpath = appendpath(dest, dp) if kind == 'directory': os.mkdir(fullpath) elif kind == 'file': pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb')) else: bailout("don't know how to export {%s} of kind %r", fid, kind) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) class WorkingTree(Tree): """Working copy tree. The inventory is held in the `Branch` working-inventory, and the files are in a directory on disk. It is possible for a `WorkingTree` to have a filename which is not listed in the Inventory and vice versa. """ def __init__(self, basedir, inv): self._inventory = inv self.basedir = basedir self.path2id = inv.path2id def __repr__(self): return "<%s of %s>" % (self.__class__.__name__, self.basedir) def abspath(self, filename): return os.path.join(self.basedir, filename) def has_filename(self, filename): return os.path.exists(self.abspath(filename)) def get_file(self, file_id): return self.get_file_byname(self.id2path(file_id)) def get_file_byname(self, filename): return file(self.abspath(filename), 'rb') def _get_store_filename(self, file_id): return self.abspath(self.id2path(file_id)) def has_id(self, file_id): # files that have been deleted are excluded if not self.inventory.has_id(file_id): return False return os.access(self.abspath(self.inventory.id2path(file_id)), os.F_OK) def get_file_size(self, file_id): return os.stat(self._get_store_filename(file_id))[ST_SIZE] def get_file_sha1(self, file_id): f = self.get_file(file_id) return sha_file(f) def file_class(self, filename): if self.path2id(filename): return 'V' elif self.is_ignored(filename): return 'I' else: return '?' def list_files(self): """Recursively list all files as (path, class, kind, id). Lists, but does not descend into unversioned directories. This does not include files that have been deleted in this tree. Skips the control directory. """ inv = self.inventory def descend(from_dir, from_dir_id, dp): ls = os.listdir(dp) ls.sort() for f in ls: ## TODO: If we find a subdirectory with its own .bzr ## directory, then that is a separate tree and we ## should exclude it. if bzrlib.BZRDIR == f: continue # path within tree fp = appendpath(from_dir, f) # absolute path fap = appendpath(dp, f) f_ie = inv.get_child(from_dir_id, f) if f_ie: c = 'V' elif self.is_ignored(fp): c = 'I' else: c = '?' fk = file_kind(fap) if f_ie: if f_ie.kind != fk: bailout("file %r entered as kind %r id %r, now of kind %r" % (fap, f_ie.kind, f_ie.file_id, fk)) yield fp, c, fk, (f_ie and f_ie.file_id) if fk != 'directory': continue if c != 'V': # don't descend unversioned directories continue for ff in descend(fp, f_ie.file_id, fap): yield ff for f in descend('', None, self.basedir): yield f def unknowns(self): for subp in self.extras(): if not self.is_ignored(subp): yield subp def extras(self): """Yield all unknown files in this WorkingTree. If there are any unknown directories then only the directory is returned, not all its children. But if there are unknown files under a versioned subdirectory, they are returned. Currently returned depth-first, sorted by name within directories. """ ## TODO: Work from given directory downwards for path, dir_entry in self.inventory.directories(): mutter("search for unknowns in %r" % path) dirabs = self.abspath(path) if not isdir(dirabs): # e.g. directory deleted continue fl = [] for subf in os.listdir(dirabs): if (subf != '.bzr' and (subf not in dir_entry.children)): fl.append(subf) fl.sort() for subf in fl: subp = appendpath(path, subf) yield subp def ignored_files(self): """Yield list of PATH, IGNORE_PATTERN""" for subp in self.extras(): pat = self.is_ignored(subp) if pat != None: yield subp, pat def get_ignore_list(self): """Return list of ignore patterns. Cached in the Tree object after the first call. """ if hasattr(self, '_ignorelist'): return self._ignorelist l = bzrlib.DEFAULT_IGNORE[:] if self.has_filename(bzrlib.IGNORE_FILENAME): f = self.get_file_byname(bzrlib.IGNORE_FILENAME) l.extend([line.rstrip("\n\r") for line in f.readlines()]) self._ignorelist = l return l def is_ignored(self, filename): """Check whether the filename matches an ignore pattern. Patterns containing '/' need to match the whole path; others match against only the last component. If the file is ignored, returns the pattern which caused it to be ignored, otherwise None. So this can simply be used as a boolean if desired.""" ## TODO: Use '**' to match directories, and other extended globbing stuff from cvs/rsync. for pat in self.get_ignore_list(): if '/' in pat: # as a special case, you can put ./ at the start of a pattern; # this is good to match in the top-level only; if pat[:2] == './': newpat = pat[2:] else: newpat = pat if fnmatch.fnmatchcase(filename, newpat): return pat else: if fnmatch.fnmatchcase(splitpath(filename)[-1], pat): return pat return None class RevisionTree(Tree): """Tree viewing a previous revision. File text can be retrieved from the text store. :todo: Some kind of `__repr__` method, but a good one probably means knowing the branch and revision number, or at least passing a description to the constructor. """ def __init__(self, store, inv): self._store = store self._inventory = inv def get_file(self, file_id): ie = self._inventory[file_id] f = self._store[ie.text_id] mutter(" get fileid{%s} from %r" % (file_id, self)) self._check_retrieved(ie, f) return f def get_file_size(self, file_id): return self._inventory[file_id].text_size def get_file_sha1(self, file_id): ie = self._inventory[file_id] return ie.text_sha1 def has_filename(self, filename): return bool(self.inventory.path2id(filename)) def list_files(self): # The only files returned by this are those from the version for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id class EmptyTree(Tree): def __init__(self): self._inventory = Inventory() def has_filename(self, filename): return False def list_files(self): if False: # just to make it a generator yield None ###################################################################### # diff # TODO: Merge these two functions into a single one that can operate # on either a whole tree or a set of files. # TODO: Return the diff in order by filename, not by category or in # random order. Can probably be done by lock-stepping through the # filenames from both trees. def file_status(filename, old_tree, new_tree): """Return single-letter status, old and new names for a file. The complexity here is in deciding how to represent renames; many complex cases are possible. """ old_inv = old_tree.inventory new_inv = new_tree.inventory new_id = new_inv.path2id(filename) old_id = old_inv.path2id(filename) if not new_id and not old_id: # easy: doesn't exist in either; not versioned at all if new_tree.is_ignored(filename): return 'I', None, None else: return '?', None, None elif new_id: # There is now a file of this name, great. pass else: # There is no longer a file of this name, but we can describe # what happened to the file that used to have # this name. There are two possibilities: either it was # deleted entirely, or renamed. assert old_id if new_inv.has_id(old_id): return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id) else: return 'D', old_inv.id2path(old_id), None # if the file_id is new in this revision, it is added if new_id and not old_inv.has_id(new_id): return 'A' # if there used to be a file of this name, but that ID has now # disappeared, it is deleted if old_id and not new_inv.has_id(old_id): return 'D' return 'wtf?' commit refs/heads/master mark :158 committer 1112331559 +1000 data 19 few more test cases from :157 M 644 inline bzrlib/tests.py data 5696 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # XXX: We might prefer these to be in a text file rather than Python # source, but that only works in doctest from Python 2.4 and later, # which is not present in Warty. r""" Bazaar-NG test cases ******************** These are run by ``bzr.doctest``. >>> import bzrlib, os >>> from bzrlib import ScratchBranch >>> bzrlib.commands.cmd_rocks() it sure does! Hey, nice place to begin. The basic object is a Branch. We have a special helper class ScratchBranch that automatically makes a directory and cleans itself up, but is in other respects identical. ScratchBranches are initially empty: >>> b = bzrlib.ScratchBranch() >>> b.show_status() New files in that directory are, it is initially unknown: >>> file(b.base + '/hello.c', 'wt').write('int main() {}') >>> b.show_status() ? hello.c That's not quite true; some files (like editor backups) are ignored by default: >>> file(b.base + '/hello.c~', 'wt').write('int main() {}') >>> b.show_status() ? hello.c >>> list(b.unknowns()) ['hello.c'] The ``add`` command marks a file to be added in the next revision: >>> b.add('hello.c') >>> b.show_status() A hello.c You can also add files that otherwise would be ignored. The ignore patterns only apply to files that would be otherwise unknown, so they have no effect once it's added. >>> b.add('hello.c~') >>> b.show_status() A hello.c A hello.c~ It is an error to add a file that isn't present in the working copy: >>> b.add('nothere') Traceback (most recent call last): ... BzrError: ('cannot add: not a regular file or directory: nothere', []) If we add a file and then change our mind, we can either revert it or remove the file. If we revert, we are left with the working copy (in either I or ? state). If we remove, the working copy is gone. Let's do that to the backup, presumably added accidentally. >>> b.remove('hello.c~') >>> b.show_status() A hello.c Now to commit, creating a new revision. (Fake the date and name for reproducibility.) >>> b.commit('start hello world', timestamp=0, committer='foo@nowhere') >>> b.show_status() >>> b.show_status(show_all=True) . hello.c I hello.c~ We can look back at history >>> r = b.get_revision(b.lookup_revision(1)) >>> r.message 'start hello world' >>> b.write_log(show_timezone='utc') ---------------------------------------- revno: 1 committer: foo@nowhere timestamp: Thu 1970-01-01 00:00:00 +0000 message: start hello world (The other fields will be a bit unpredictable, depending on who ran this test and when.) As of 2005-02-21, we can also add subdirectories to the revision! >>> os.mkdir(b.base + "/lib") >>> b.show_status() ? lib/ >>> b.add('lib') >>> b.show_status() A lib/ >>> b.commit('add subdir') >>> b.show_status() >>> b.show_status(show_all=True) . hello.c I hello.c~ . lib/ and we can also add files within subdirectories: >>> file(b.base + '/lib/hello', 'w').write('hello!\n') >>> b.show_status() ? lib/hello Tests for adding subdirectories, etc. >>> b = bzrlib.branch.ScratchBranch() >>> os.mkdir(b.abspath('d1')) >>> os.mkdir(b.abspath('d2')) >>> os.mkdir(b.abspath('d2/d3')) >>> list(b.working_tree().unknowns()) ['d1', 'd2'] Create some files, but they're not seen as unknown yet: >>> file(b.abspath('d1/f1'), 'w').close() >>> file(b.abspath('d2/f2'), 'w').close() >>> file(b.abspath('d2/f3'), 'w').close() >>> [v[0] for v in b.inventory.directories()] [''] >>> list(b.working_tree().unknowns()) ['d1', 'd2'] Adding a directory, and we see the file underneath: >>> b.add('d1') >>> [v[0] for v in b.inventory.directories()] ['', 'd1'] >>> list(b.working_tree().unknowns()) ['d2', 'd1/f1'] >>> # d2 comes first because it's in the top directory >>> b.add('d2') >>> b.commit('add some stuff') >>> list(b.working_tree().unknowns()) ['d1/f1', 'd2/d3', 'd2/f2', 'd2/f3'] >>> b.add('d1/f1') >>> list(b.working_tree().unknowns()) ['d2/d3', 'd2/f2', 'd2/f3'] Tests for ignored files and patterns: >>> b = ScratchBranch(dirs=['src', 'doc'], ... files=['configure.in', 'configure', ... 'doc/configure', 'foo.c', ... 'foo']) >>> list(b.unknowns()) ['configure', 'configure.in', 'doc', 'foo', 'foo.c', 'src'] >>> b.add(['doc', 'foo.c', 'src', 'configure.in']) >>> list(b.unknowns()) ['configure', 'foo', 'doc/configure'] >>> f = file(b.abspath('.bzrignore'), 'w') >>> f.write('./configure\n' ... './foo\n') >>> f.close() >>> b.add('.bzrignore') >>> list(b.unknowns()) ['doc/configure'] >>> b.commit("commit 1") >>> list(b.unknowns()) ['doc/configure'] >>> b.add("doc/configure") >>> b.commit("commit more") """ commit refs/heads/master mark :159 committer 1112328244 +1000 data 27 bzr commit --help now works from :158 M 644 inline NEWS data 1676 bzr-0.0.3 NOT RELEASED YET INTERNAL: * Refactored inventory storage to insert a root entry at the top. ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". bzr-0.0.2 "black cube" 2003-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 25703 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] --revision REV Show changes since REV, rather than predecessor. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Diff selected files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): inv = Branch('.').basis_tree().inventory def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(verbose=True): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = cmdfn.__doc__ if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'commit': [], 'diff': [], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'relpath': ['filename'], 'remove': ['file+'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot prof = hotshot.Profile('.bzr.profile') ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load('.bzr.profile') #stats.strip_dirs() stats.sort_stats('time') stats.print_stats(20) else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :160 committer 1112343731 +1000 data 191 - basic support for moving files to different directories - have not done support for renaming them yet, but should be straightforward - some tests, but many cases are not handled yet i think from :159 M 644 inline NEWS data 1825 bzr-0.0.3 NOT RELEASED YET INTERNAL: * Refactored inventory storage to insert a root entry at the top. ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * Basic "bzr mv" support for renames! (Not all scenarios work through the command at the moment, but the inventory support is there.) bzr-0.0.2 "black cube" 2003-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/branch.py data 29058 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f is None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f last_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be bailout('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. :todo: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. :todo: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. :todo: Keep the on-disk branch locked while the object exists. :todo: mkdir() method. """ def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. :param base: Base directory for the branch. :param init: If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. :param find_root: If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch""" return file(self.controlfilename(file_or_path), mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # read in binary mode to detect newline wierdness. fmt = self.controlfile('branch-format', 'rb').read() if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() inv = Inventory.read_xml(self.controlfile('inventory', 'r')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'w') inv.write_xml(tmpf) tmpf.close() os.rename(tmpfname, self.controlfilename('inventory')) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. This puts the files in the Added state, so that they will be recorded by the next commit. :todo: Perhaps have an option to add the ids even if the files do not (yet) exist. :todo: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. :todo: Option to specify file id. :todo: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on :todo: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True :todo: Do something useful with directories. :todo: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. :param timestamp: if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) mutter("append to revision-history") f = self.controlfile('revision-history', 'at') f.write(rev_id + '\n') f.close() if verbose: note("commited r%d" % self.revno()) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. :todo: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: bailout("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original'): """Write out human-readable log of commits to this branch :param utc: If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l revno += 1 precursor = p def rename(self, from_paths, to_name): """Rename files. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory dest_dir = isdir(self.abspath(to_name)) if dest_dir: # TODO: Wind back properly if some can't be moved? dest_dir_id = inv.path2id(to_name) if not dest_dir_id and to_name != '': bailout("destination %r is not a versioned directory" % to_name) for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), dest_dir_id, name_tail) os.rename(self.abspath(f), self.abspath(dest_path)) self._write_inventory(inv) else: if len(from_paths) != 1: bailout("when moving multiple files, destination must be a directory") bailout("rename to non-directory %r not implemented sorry" % to_name) def show_status(branch, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo :todo: Get state for single files. :todo: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = branch.basis_tree() old_inv = old.inventory new = branch.working_tree() new_inv = new.inventory for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("wierd file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/commands.py data 26151 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_mv(source_list, dest): b = Branch('.') b.rename([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] --revision REV Show changes since REV, rather than predecessor. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Diff selected files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(*a, **kw): sys.stdout.writelines(difflib.unified_diff(*a, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): inv = Branch('.').basis_tree().inventory def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(verbose=True): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = cmdfn.__doc__ if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'commit': [], 'diff': [], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'mv': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot prof = hotshot.Profile('.bzr.profile') ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load('.bzr.profile') #stats.strip_dirs() stats.sort_stats('time') stats.print_stats(20) else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') M 644 inline bzrlib/inventory.py data 17202 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Inventories map files to their name in a revision.""" # TODO: Maybe store inventory_id in the file? Not really needed. __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os.path, types, re from sets import Set try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from xml import XMLMixin from errors import bailout import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') >>> i.add(InventoryEntry('123', 'src', kind='directory')) >>> i.add(InventoryEntry('2323', 'hello.c', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id=None)) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', parent_id='123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', parent_id='123')) >>> i.add(InventoryEntry('2325', 'wibble', parent_id='123', kind='directory')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', parent_id='2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' :todo: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ def __init__(self, file_id, name, kind='file', text_id=None, parent_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c') >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c') Traceback (most recent call last): BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", []) """ if len(splitpath(name)) != 1: bailout('InventoryEntry name is not a simple filename: %r' % name) self.file_id = file_id self.name = name assert kind in ['file', 'directory'] self.kind = kind self.text_id = text_id self.parent_id = parent_id self.text_sha1 = None self.text_size = None if kind == 'directory': self.children = {} def sorted_children(self): l = self.children.items() l.sort() return l def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.text_id, self.parent_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size is not None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1', 'parent_id']: v = getattr(self, f) if v is not None: e.set(f, v) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind')) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') self.parent_id = elt.get('parent_id') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __cmp__(self, other): if self is other: return 0 if not isinstance(other, InventoryEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.name, other.name) \ or cmp(self.text_sha1, other.text_sha1) \ or cmp(self.text_size, other.text_size) \ or cmp(self.text_id, other.text_id) \ or cmp(self.parent_id, other.parent_id) \ or cmp(self.kind, other.kind) class RootEntry(InventoryEntry): def __init__(self, file_id): self.file_id = file_id self.children = {} self.kind = 'root_directory' self.parent_id = None self.name = '' def __cmp__(self, other): if self is other: return 0 if not isinstance(other, RootEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.children, other.children) class Inventory(XMLMixin): """Inventory of versioned files in a tree. An Inventory acts like a set of InventoryEntry items. You can also look files up by their file_id or name. May be read from and written to a metadata file in a tree. To manipulate the inventory (for example to add a file), it is read in, modified, and then written back out. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted, other than through the Inventory API. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c')) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ ## TODO: Make sure only canonical filenames are stored. ## TODO: Do something sensible about the possible collisions on ## case-losing filesystems. Perhaps we should just always forbid ## such collisions. ## TODO: No special cases for root, rather just give it a file id ## like everything else. ## TODO: Probably change XML serialization to use nesting def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. The inventory is created with a default root directory, with an id of None. """ self.root = RootEntry(None) self._byid = {None: self.root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, from_dir=None): """Return (path, entry) pairs, in order by name.""" if from_dir == None: assert self.root from_dir = self.root elif isinstance(from_dir, basestring): from_dir = self._byid[from_dir] kids = from_dir.children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(from_dir=ie.file_id): yield '/'.join((name, cn)), cie def directories(self, from_dir=None): """Return (path, entry) pairs for all directories. """ def descend(parent_ie): parent_name = parent_ie.name yield parent_name, parent_ie # directory children in sorted order dn = [] for ie in parent_ie.children.itervalues(): if ie.kind == 'directory': dn.append((ie.name, ie)) dn.sort() for name, child_ie in dn: for sub_name, sub_ie in descend(child_ie): yield appendpath(parent_name, sub_name), sub_ie for name, ie in descend(self.root): yield name, ie def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c')) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c')) >>> inv['123123'].name 'hello.c' """ return self._byid[file_id] def get_child(self, parent_id, filename): return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self._byid: bailout("inventory already contains entry with id {%s}" % entry.file_id) try: parent = self._byid[entry.parent_id] except KeyError: bailout("parent_id %r not in inventory" % entry.parent_id) if parent.children.has_key(entry.name): bailout("%s is already versioned" % appendpath(self.id2path(parent.file_id), entry.name)) self._byid[entry.file_id] = entry parent.children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: bailout("cannot re-add root of inventory") if file_id is None: file_id = bzrlib.branch.gen_file_id(relpath) parent_id = self.path2id(parts[:-1]) ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c')) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def id_set(self): return Set(self._byid) def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c')) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __cmp__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo')) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo')) >>> i1 == i2 True """ if self is other: return 0 if not isinstance(other, Inventory): return NotImplemented if self.id_set() ^ other.id_set(): return 1 for file_id in self._byid: c = cmp(self[file_id], other[file_id]) if c: return c return 0 def id2path(self, file_id): """Return as a list the path to file_id.""" p = [] while file_id != None: ie = self._byid[file_id] p.insert(0, ie.name) file_id = ie.parent_id return '/'.join(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. """ if isinstance(name, types.StringTypes): name = splitpath(name) parent = self[None] for f in name: try: cie = parent.children[f] assert cie.name == f parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): return self._byid.has_key(file_id) def rename(self, file_id, new_parent_id, new_name): """Move a file within the inventory. This can change either the name, or the parent, or both. This does not move the working file.""" if not is_valid_name(new_name): bailout("not an acceptable filename: %r" % new_name) new_parent = self._byid[new_parent_id] if new_name in new_parent.children: bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id))) file_ie = self._byid[file_id] old_parent = self._byid[file_ie.parent_id] # TODO: Don't leave things messed up if this fails del old_parent.children[file_ie.name] new_parent.children[new_name] = file_ie file_ie.name = new_name file_ie.parent_id = new_parent_id _NAME_RE = re.compile(r'^[^/\\]+$') def is_valid_name(name): return bool(_NAME_RE.match(name)) if __name__ == '__main__': import doctest, inventory doctest.testmod(inventory) M 644 inline bzrlib/tests.py data 6167 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # XXX: We might prefer these to be in a text file rather than Python # source, but that only works in doctest from Python 2.4 and later, # which is not present in Warty. r""" Bazaar-NG test cases ******************** These are run by ``bzr.doctest``. >>> import bzrlib, os >>> from bzrlib import ScratchBranch >>> from bzrlib.osutils import isdir, isfile >>> bzrlib.commands.cmd_rocks() it sure does! Hey, nice place to begin. The basic object is a Branch. We have a special helper class ScratchBranch that automatically makes a directory and cleans itself up, but is in other respects identical. ScratchBranches are initially empty: >>> b = bzrlib.ScratchBranch() >>> b.show_status() New files in that directory are, it is initially unknown: >>> file(b.base + '/hello.c', 'wt').write('int main() {}') >>> b.show_status() ? hello.c That's not quite true; some files (like editor backups) are ignored by default: >>> file(b.base + '/hello.c~', 'wt').write('int main() {}') >>> b.show_status() ? hello.c >>> list(b.unknowns()) ['hello.c'] The ``add`` command marks a file to be added in the next revision: >>> b.add('hello.c') >>> b.show_status() A hello.c You can also add files that otherwise would be ignored. The ignore patterns only apply to files that would be otherwise unknown, so they have no effect once it's added. >>> b.add('hello.c~') >>> b.show_status() A hello.c A hello.c~ It is an error to add a file that isn't present in the working copy: >>> b.add('nothere') Traceback (most recent call last): ... BzrError: ('cannot add: not a regular file or directory: nothere', []) If we add a file and then change our mind, we can either revert it or remove the file. If we revert, we are left with the working copy (in either I or ? state). If we remove, the working copy is gone. Let's do that to the backup, presumably added accidentally. >>> b.remove('hello.c~') >>> b.show_status() A hello.c Now to commit, creating a new revision. (Fake the date and name for reproducibility.) >>> b.commit('start hello world', timestamp=0, committer='foo@nowhere') >>> b.show_status() >>> b.show_status(show_all=True) . hello.c I hello.c~ We can look back at history >>> r = b.get_revision(b.lookup_revision(1)) >>> r.message 'start hello world' >>> b.write_log(show_timezone='utc') ---------------------------------------- revno: 1 committer: foo@nowhere timestamp: Thu 1970-01-01 00:00:00 +0000 message: start hello world (The other fields will be a bit unpredictable, depending on who ran this test and when.) As of 2005-02-21, we can also add subdirectories to the revision! >>> os.mkdir(b.base + "/lib") >>> b.show_status() ? lib/ >>> b.add('lib') >>> b.show_status() A lib/ >>> b.commit('add subdir') >>> b.show_status() >>> b.show_status(show_all=True) . hello.c I hello.c~ . lib/ and we can also add files within subdirectories: >>> file(b.base + '/lib/hello', 'w').write('hello!\n') >>> b.show_status() ? lib/hello Tests for adding subdirectories, etc. >>> b = bzrlib.branch.ScratchBranch() >>> os.mkdir(b.abspath('d1')) >>> os.mkdir(b.abspath('d2')) >>> os.mkdir(b.abspath('d2/d3')) >>> list(b.working_tree().unknowns()) ['d1', 'd2'] Create some files, but they're not seen as unknown yet: >>> file(b.abspath('d1/f1'), 'w').close() >>> file(b.abspath('d2/f2'), 'w').close() >>> file(b.abspath('d2/f3'), 'w').close() >>> [v[0] for v in b.inventory.directories()] [''] >>> list(b.working_tree().unknowns()) ['d1', 'd2'] Adding a directory, and we see the file underneath: >>> b.add('d1') >>> [v[0] for v in b.inventory.directories()] ['', 'd1'] >>> list(b.working_tree().unknowns()) ['d2', 'd1/f1'] >>> # d2 comes first because it's in the top directory >>> b.add('d2') >>> b.commit('add some stuff') >>> list(b.working_tree().unknowns()) ['d1/f1', 'd2/d3', 'd2/f2', 'd2/f3'] >>> b.add('d1/f1') >>> list(b.working_tree().unknowns()) ['d2/d3', 'd2/f2', 'd2/f3'] Tests for ignored files and patterns: >>> b = ScratchBranch(dirs=['src', 'doc'], ... files=['configure.in', 'configure', ... 'doc/configure', 'foo.c', ... 'foo']) >>> list(b.unknowns()) ['configure', 'configure.in', 'doc', 'foo', 'foo.c', 'src'] >>> b.add(['doc', 'foo.c', 'src', 'configure.in']) >>> list(b.unknowns()) ['configure', 'foo', 'doc/configure'] >>> f = file(b.abspath('.bzrignore'), 'w') >>> f.write('./configure\n' ... './foo\n') >>> f.close() >>> b.add('.bzrignore') >>> list(b.unknowns()) ['doc/configure'] >>> b.commit("commit 1") >>> list(b.unknowns()) ['doc/configure'] >>> b.add("doc/configure") >>> b.commit("commit more") >>> del b Renames, etc: >>> b = ScratchBranch(files=['foo'], dirs=['subdir']) >>> b.add(['foo', 'subdir']) >>> b.commit('add foo') >>> list(b.unknowns()) [] >>> b.rename(['foo'], 'subdir') foo => subdir/foo >>> b.show_status() R foo => subdir/foo >>> b.commit("move foo to subdir") >>> isfile(b.abspath('foo')) False >>> isfile(b.abspath('subdir/foo')) True """ commit refs/heads/master mark :161 committer 1112344021 +1000 data 18 pychecker warnings from :160 M 644 inline bzrlib/add.py data 2892 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, sys import bzrlib from osutils import quotefn, appendpath from errors import bailout from trace import mutter, note def smart_add(file_list, verbose=False, recurse=True): """Add files to version, optionall recursing into directories. This is designed more towards DWIM for humans than API simplicity. For the specific behaviour see the help for cmd_add(). """ assert file_list assert not isinstance(file_list, basestring) b = bzrlib.branch.Branch(file_list[0], find_root=True) inv = b.read_working_inventory() tree = b.working_tree() count = 0 for f in file_list: rf = b.relpath(f) af = b.abspath(rf) ## TODO: It's OK to add root but only in recursive mode bzrlib.mutter("smart add of %r" % f) if bzrlib.branch.is_control_file(af): bailout("cannot add control file %r" % af) kind = bzrlib.osutils.file_kind(f) if kind != 'file' and kind != 'directory': bailout("can't add file of kind %r" % kind) versioned = (inv.path2id(rf) != None) if rf == '': mutter("branch root doesn't need to be added") elif versioned: mutter("%r is already versioned" % f) else: file_id = bzrlib.branch.gen_file_id(rf) inv.add_path(rf, kind=kind, file_id=file_id) bzrlib.mutter("added %r kind %r file_id={%s}" % (rf, kind, file_id)) count += 1 if verbose: bzrlib.textui.show_status('A', kind, quotefn(f)) if kind == 'directory' and recurse: for subf in os.listdir(af): subp = appendpath(rf, subf) if subf == bzrlib.BZRDIR: mutter("skip control directory %r" % subp) elif tree.is_ignored(subp): mutter("skip ignored sub-file %r" % subp) else: mutter("queue to add sub-file %r" % (subp)) file_list.append(subp) if count > 0: if verbose: note('added %d' % count) b._write_inventory(inv) M 644 inline bzrlib/trace.py data 3567 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os, time, socket, stat import bzrlib ###################################################################### # messages and logging ## TODO: If --verbose is given then write to both stderr and ## _tracefile; perhaps replace _tracefile with a tee thing. global _tracefile, _starttime _tracefile = None # used to have % (os.environ['USER'], time.time(), os.getpid()), 'w') # If false, notes also go to stdout; should replace this with --silent # at some point. silent = False # TODO: Somehow tie this to the --verbose option? verbose = False def warning(msg): b = 'bzr: warning: ' + msg + '\n' sys.stderr.write(b) _tracefile.write(b) #_tracefile.flush() def mutter(msg): _tracefile.write(msg) _tracefile.write('\n') # _tracefile.flush() if verbose: sys.stderr.write('- ' + msg + '\n') def note(msg): b = '* ' + str(msg) + '\n' if not silent: sys.stderr.write(b) _tracefile.write(b) # _tracefile.flush() def log_error(msg): sys.stderr.write(msg) _tracefile.write(msg) # _tracefile.flush() def create_tracefile(argv): # TODO: Also show contents of /etc/lsb-release, if it can be parsed. # Perhaps that should eventually go into the platform library? # TODO: If the file doesn't exist, add a note describing it. # Messages are always written to here, so that we have some # information if something goes wrong. In a future version this # file will be removed on successful completion. global _starttime, _tracefile _starttime = os.times()[4] _tracefile = file('.bzr.log', 'at') t = _tracefile if os.fstat(t.fileno())[stat.ST_SIZE] == 0: t.write("\nthis is a debug log for diagnosing/reporting problems in bzr\n") t.write("you can delete or truncate this file, or include sections in\n") t.write("bug reports to bazaar-ng@lists.canonical.com\n\n") # TODO: If we failed to create the file, perhaps give a warning # but don't abort; send things to /dev/null instead? t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % bzrlib.osutils.format_date(time.time())) t.write(' version: %s\n' % bzrlib.__version__) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.getfqdn())) t.write(' arguments: %r\n' % argv) # This causes a vfork; I don't care enough about it. t.write(' platform: %s\n' % sys.platform) t.write(' python: %s\n' % (sys.version_info,)) import atexit atexit.register(_close_trace) def _close_trace(): times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum, %.3f elapsed" % (times[:4] + ((times[4] - _starttime),))) commit refs/heads/master mark :162 committer 1112608224 +1000 data 36 workaround for python2.3 difflib bug from :161 M 644 inline NEWS data 1988 bzr-0.0.3 NOT RELEASED YET INTERNAL: * Refactored inventory storage to insert a root entry at the top. ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * Basic "bzr mv" support for renames! (Not all scenarios work through the command at the moment, but the inventory support is there.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. bzr-0.0.2 "black cube" 2003-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 26407 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_mv(source_list, dest): b = Branch('.') b.rename([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] --revision REV Show changes since REV, rather than predecessor. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Diff selected files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): inv = Branch('.').basis_tree().inventory def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(verbose=True): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = cmdfn.__doc__ if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'commit': [], 'diff': [], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'mv': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot prof = hotshot.Profile('.bzr.profile') ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load('.bzr.profile') #stats.strip_dirs() stats.sort_stats('time') stats.print_stats(20) else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :163 committer 1112610913 +1000 data 29 merge win32 portability fixes from :162 M 644 inline NEWS data 2054 bzr-0.0.3 NOT RELEASED YET INTERNAL: * Refactored inventory storage to insert a root entry at the top. ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * Basic "bzr mv" support for renames! (Not all scenarios work through the command at the moment, but the inventory support is there.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2003-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/branch.py data 29682 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f is None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f last_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be bailout('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. :todo: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. :todo: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. :todo: Keep the on-disk branch locked while the object exists. :todo: mkdir() method. """ def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. :param base: Base directory for the branch. :param init: If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. :param find_root: If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch""" return file(self.controlfilename(file_or_path), mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'wb').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'rb').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() inv = Inventory.read_xml(self.controlfile('inventory', 'r')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'w') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. This puts the files in the Added state, so that they will be recorded by the next commit. :todo: Perhaps have an option to add the ids even if the files do not (yet) exist. :todo: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. :todo: Option to specify file id. :todo: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on :todo: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True :todo: Do something useful with directories. :todo: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. :param timestamp: if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) mutter("append to revision-history") f = self.controlfile('revision-history', 'at') f.write(rev_id + '\n') f.close() if verbose: note("commited r%d" % self.revno()) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. :todo: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: bailout("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original'): """Write out human-readable log of commits to this branch :param utc: If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l revno += 1 precursor = p def rename(self, from_paths, to_name): """Rename files. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory dest_dir = isdir(self.abspath(to_name)) if dest_dir: # TODO: Wind back properly if some can't be moved? dest_dir_id = inv.path2id(to_name) if not dest_dir_id and to_name != '': bailout("destination %r is not a versioned directory" % to_name) for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), dest_dir_id, name_tail) os.rename(self.abspath(f), self.abspath(dest_path)) self._write_inventory(inv) else: if len(from_paths) != 1: bailout("when moving multiple files, destination must be a directory") bailout("rename to non-directory %r not implemented sorry" % to_name) def show_status(branch, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo :todo: Get state for single files. :todo: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = branch.basis_tree() old_inv = old.inventory new = branch.working_tree() new_inv = new.inventory for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("wierd file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" try: shutil.rmtree(self.base) except OSError: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/store.py data 5411 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Stores are the main data-storage mechanism for Bazaar-NG. A store is a simple write-once container indexed by a universally unique ID, which is typically the SHA-1 of the content.""" __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import os, tempfile, types, osutils, gzip, errno from stat import ST_SIZE from StringIO import StringIO from trace import mutter ###################################################################### # stores class StoreError(Exception): pass class ImmutableStore: """Store that holds files indexed by unique names. Files can be added, but not modified once they are in. Typically the hash is used as the name, or something else known to be unique, such as a UUID. >>> st = ImmutableScratchStore() >>> st.add(StringIO('hello'), 'aa') >>> 'aa' in st True >>> 'foo' in st False You are not allowed to add an id that is already present. Entries can be retrieved as files, which may then be read. >>> st.add(StringIO('goodbye'), '123123') >>> st['123123'].read() 'goodbye' :todo: Atomic add by writing to a temporary file and renaming. :todo: Perhaps automatically transform to/from XML in a method? Would just need to tell the constructor what class to use... :todo: Even within a simple disk store like this, we could gzip the files. But since many are less than one disk block, that might not help a lot. """ def __init__(self, basedir): """ImmutableStore constructor.""" self._basedir = basedir def _path(self, id): return os.path.join(self._basedir, id) def __repr__(self): return "%s(%r)" % (self.__class__.__name__, self._basedir) def add(self, f, fileid, compressed=True): """Add contents of a file into the store. :param f: An open file, or file-like object.""" # FIXME: Only works on smallish files # TODO: Can be optimized by copying at the same time as # computing the sum. mutter("add store entry %r" % (fileid)) if isinstance(f, types.StringTypes): content = f else: content = f.read() p = self._path(fileid) if os.access(p, os.F_OK) or os.access(p + '.gz', os.F_OK): bailout("store %r already contains id %r" % (self._basedir, fileid)) if compressed: f = gzip.GzipFile(p + '.gz', 'wb') os.chmod(p + '.gz', 0444) else: f = file(p, 'wb') os.chmod(p, 0444) f.write(content) f.close() def __contains__(self, fileid): """""" p = self._path(fileid) return (os.access(p, os.R_OK) or os.access(p + '.gz', os.R_OK)) # TODO: Guard against the same thing being stored twice, compressed and uncompresse def __iter__(self): for f in os.listdir(self._basedir): if f[-3:] == '.gz': # TODO: case-insensitive? yield f[:-3] else: yield f def __len__(self): return len(os.listdir(self._basedir)) def __getitem__(self, fileid): """Returns a file reading from a particular entry.""" p = self._path(fileid) try: return gzip.GzipFile(p + '.gz', 'rb') except IOError, e: if e.errno == errno.ENOENT: return file(p, 'rb') else: raise e def total_size(self): """Return (count, bytes) This is the (compressed) size stored on disk, not the size of the content.""" total = 0 count = 0 for fid in self: count += 1 p = self._path(fid) try: total += os.stat(p)[ST_SIZE] except OSError: total += os.stat(p + '.gz')[ST_SIZE] return count, total class ImmutableScratchStore(ImmutableStore): """Self-destructing test subclass of ImmutableStore. The Store only exists for the lifetime of the Python object. Obviously you should not put anything precious in it. """ def __init__(self): ImmutableStore.__init__(self, tempfile.mkdtemp()) def __del__(self): for f in os.listdir(self._basedir): fpath = os.path.join(self._basedir, f) # needed on windows, and maybe some other filesystems os.chmod(fpath, 0600) os.remove(fpath) os.rmdir(self._basedir) mutter("%r destroyed" % self) commit refs/heads/master mark :164 committer 1112620226 +1000 data 21 new 'renames' command from :163 M 644 inline NEWS data 2124 bzr-0.0.3 NOT RELEASED YET INTERNAL: * Refactored inventory storage to insert a root entry at the top. ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * Basic "bzr mv" support for renames! (Not all scenarios work through the command at the moment, but the inventory support is there.) * New "renames" command lists files renamed since base revision. PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2003-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 26958 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_mv(source_list, dest): b = Branch('.') b.rename([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] --revision REV Show changes since REV, rather than predecessor. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Diff selected files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): inv = Branch('.').basis_tree().inventory def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(verbose=True): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = cmdfn.__doc__ if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'commit': [], 'diff': [], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'mv': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot prof = hotshot.Profile('.bzr.profile') ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load('.bzr.profile') #stats.strip_dirs() stats.sort_stats('time') stats.print_stats(20) else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') M 644 inline bzrlib/tests.py data 6233 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # XXX: We might prefer these to be in a text file rather than Python # source, but that only works in doctest from Python 2.4 and later, # which is not present in Warty. r""" Bazaar-NG test cases ******************** These are run by ``bzr.doctest``. >>> import bzrlib, os >>> from bzrlib import ScratchBranch >>> from bzrlib.osutils import isdir, isfile >>> bzrlib.commands.cmd_rocks() it sure does! Hey, nice place to begin. The basic object is a Branch. We have a special helper class ScratchBranch that automatically makes a directory and cleans itself up, but is in other respects identical. ScratchBranches are initially empty: >>> b = bzrlib.ScratchBranch() >>> b.show_status() New files in that directory are, it is initially unknown: >>> file(b.base + '/hello.c', 'wt').write('int main() {}') >>> b.show_status() ? hello.c That's not quite true; some files (like editor backups) are ignored by default: >>> file(b.base + '/hello.c~', 'wt').write('int main() {}') >>> b.show_status() ? hello.c >>> list(b.unknowns()) ['hello.c'] The ``add`` command marks a file to be added in the next revision: >>> b.add('hello.c') >>> b.show_status() A hello.c You can also add files that otherwise would be ignored. The ignore patterns only apply to files that would be otherwise unknown, so they have no effect once it's added. >>> b.add('hello.c~') >>> b.show_status() A hello.c A hello.c~ It is an error to add a file that isn't present in the working copy: >>> b.add('nothere') Traceback (most recent call last): ... BzrError: ('cannot add: not a regular file or directory: nothere', []) If we add a file and then change our mind, we can either revert it or remove the file. If we revert, we are left with the working copy (in either I or ? state). If we remove, the working copy is gone. Let's do that to the backup, presumably added accidentally. >>> b.remove('hello.c~') >>> b.show_status() A hello.c Now to commit, creating a new revision. (Fake the date and name for reproducibility.) >>> b.commit('start hello world', timestamp=0, committer='foo@nowhere') >>> b.show_status() >>> b.show_status(show_all=True) . hello.c I hello.c~ We can look back at history >>> r = b.get_revision(b.lookup_revision(1)) >>> r.message 'start hello world' >>> b.write_log(show_timezone='utc') ---------------------------------------- revno: 1 committer: foo@nowhere timestamp: Thu 1970-01-01 00:00:00 +0000 message: start hello world (The other fields will be a bit unpredictable, depending on who ran this test and when.) As of 2005-02-21, we can also add subdirectories to the revision! >>> os.mkdir(b.base + "/lib") >>> b.show_status() ? lib/ >>> b.add('lib') >>> b.show_status() A lib/ >>> b.commit('add subdir') >>> b.show_status() >>> b.show_status(show_all=True) . hello.c I hello.c~ . lib/ and we can also add files within subdirectories: >>> file(b.base + '/lib/hello', 'w').write('hello!\n') >>> b.show_status() ? lib/hello Tests for adding subdirectories, etc. >>> b = bzrlib.branch.ScratchBranch() >>> os.mkdir(b.abspath('d1')) >>> os.mkdir(b.abspath('d2')) >>> os.mkdir(b.abspath('d2/d3')) >>> list(b.working_tree().unknowns()) ['d1', 'd2'] Create some files, but they're not seen as unknown yet: >>> file(b.abspath('d1/f1'), 'w').close() >>> file(b.abspath('d2/f2'), 'w').close() >>> file(b.abspath('d2/f3'), 'w').close() >>> [v[0] for v in b.inventory.directories()] [''] >>> list(b.working_tree().unknowns()) ['d1', 'd2'] Adding a directory, and we see the file underneath: >>> b.add('d1') >>> [v[0] for v in b.inventory.directories()] ['', 'd1'] >>> list(b.working_tree().unknowns()) ['d2', 'd1/f1'] >>> # d2 comes first because it's in the top directory >>> b.add('d2') >>> b.commit('add some stuff') >>> list(b.working_tree().unknowns()) ['d1/f1', 'd2/d3', 'd2/f2', 'd2/f3'] >>> b.add('d1/f1') >>> list(b.working_tree().unknowns()) ['d2/d3', 'd2/f2', 'd2/f3'] Tests for ignored files and patterns: >>> b = ScratchBranch(dirs=['src', 'doc'], ... files=['configure.in', 'configure', ... 'doc/configure', 'foo.c', ... 'foo']) >>> list(b.unknowns()) ['configure', 'configure.in', 'doc', 'foo', 'foo.c', 'src'] >>> b.add(['doc', 'foo.c', 'src', 'configure.in']) >>> list(b.unknowns()) ['configure', 'foo', 'doc/configure'] >>> f = file(b.abspath('.bzrignore'), 'w') >>> f.write('./configure\n' ... './foo\n') >>> f.close() >>> b.add('.bzrignore') >>> list(b.unknowns()) ['doc/configure'] >>> b.commit("commit 1") >>> list(b.unknowns()) ['doc/configure'] >>> b.add("doc/configure") >>> b.commit("commit more") >>> del b Renames, etc: >>> b = ScratchBranch(files=['foo'], dirs=['subdir']) >>> b.add(['foo', 'subdir']) >>> b.commit('add foo') >>> list(b.unknowns()) [] >>> b.rename(['foo'], 'subdir') foo => subdir/foo >>> b.show_status() R foo => subdir/foo >>> bzrlib.commands.cmd_renames(b.base) foo => subdir/foo >>> b.commit("move foo to subdir") >>> isfile(b.abspath('foo')) False >>> isfile(b.abspath('subdir/foo')) True """ M 644 inline bzrlib/tree.py data 13970 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tree classes, representing directory at point in time. """ from sets import Set import os.path, os, fnmatch from osutils import pumpfile, compare_files, filesize, quotefn, sha_file, \ joinpath, splitpath, appendpath, isdir, isfile, file_kind, fingerprint_file import errno from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE from inventory import Inventory from trace import mutter, note from errors import bailout import branch import bzrlib class Tree: """Abstract file tree. There are several subclasses: * `WorkingTree` exists as files on disk editable by the user. * `RevisionTree` is a tree as recorded at some point in the past. * `EmptyTree` Trees contain an `Inventory` object, and also know how to retrieve file texts mentioned in the inventory, either from a working directory or from a store. It is possible for trees to contain files that are not described in their inventory or vice versa; for this use `filenames()`. Trees can be compared, etc, regardless of whether they are working trees or versioned trees. """ def has_filename(self, filename): """True if the tree has given filename.""" raise NotImplementedError() def has_id(self, file_id): return self.inventory.has_id(file_id) def id_set(self): """Return set of all ids in this tree.""" return self.inventory.id_set() def id2path(self, file_id): return self.inventory.id2path(file_id) def _get_inventory(self): return self._inventory inventory = property(_get_inventory, doc="Inventory of this Tree") def _check_retrieved(self, ie, f): fp = fingerprint_file(f) f.seek(0) if ie.text_size is not None: if ie.text_size != fp['size']: bailout("mismatched size for file %r in %r" % (ie.file_id, self._store), ["inventory expects %d bytes" % ie.text_size, "file is actually %d bytes" % fp['size'], "store is probably damaged/corrupt"]) if ie.text_sha1 != fp['sha1']: bailout("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store), ["inventory expects %s" % ie.text_sha1, "file is actually %s" % fp['sha1'], "store is probably damaged/corrupt"]) def export(self, dest): """Export this tree to a new directory. `dest` should not exist, and will be created holding the contents of this tree. :todo: To handle subdirectories we need to create the directories first. :note: If the export fails, the destination directory will be left in a half-assed state. """ os.mkdir(dest) mutter('export version %r' % self) inv = self.inventory for dp, ie in inv.iter_entries(): kind = ie.kind fullpath = appendpath(dest, dp) if kind == 'directory': os.mkdir(fullpath) elif kind == 'file': pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb')) else: bailout("don't know how to export {%s} of kind %r", fid, kind) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) class WorkingTree(Tree): """Working copy tree. The inventory is held in the `Branch` working-inventory, and the files are in a directory on disk. It is possible for a `WorkingTree` to have a filename which is not listed in the Inventory and vice versa. """ def __init__(self, basedir, inv): self._inventory = inv self.basedir = basedir self.path2id = inv.path2id def __repr__(self): return "<%s of %s>" % (self.__class__.__name__, self.basedir) def abspath(self, filename): return os.path.join(self.basedir, filename) def has_filename(self, filename): return os.path.exists(self.abspath(filename)) def get_file(self, file_id): return self.get_file_byname(self.id2path(file_id)) def get_file_byname(self, filename): return file(self.abspath(filename), 'rb') def _get_store_filename(self, file_id): return self.abspath(self.id2path(file_id)) def has_id(self, file_id): # files that have been deleted are excluded if not self.inventory.has_id(file_id): return False return os.access(self.abspath(self.inventory.id2path(file_id)), os.F_OK) def get_file_size(self, file_id): return os.stat(self._get_store_filename(file_id))[ST_SIZE] def get_file_sha1(self, file_id): f = self.get_file(file_id) return sha_file(f) def file_class(self, filename): if self.path2id(filename): return 'V' elif self.is_ignored(filename): return 'I' else: return '?' def list_files(self): """Recursively list all files as (path, class, kind, id). Lists, but does not descend into unversioned directories. This does not include files that have been deleted in this tree. Skips the control directory. """ inv = self.inventory def descend(from_dir, from_dir_id, dp): ls = os.listdir(dp) ls.sort() for f in ls: ## TODO: If we find a subdirectory with its own .bzr ## directory, then that is a separate tree and we ## should exclude it. if bzrlib.BZRDIR == f: continue # path within tree fp = appendpath(from_dir, f) # absolute path fap = appendpath(dp, f) f_ie = inv.get_child(from_dir_id, f) if f_ie: c = 'V' elif self.is_ignored(fp): c = 'I' else: c = '?' fk = file_kind(fap) if f_ie: if f_ie.kind != fk: bailout("file %r entered as kind %r id %r, now of kind %r" % (fap, f_ie.kind, f_ie.file_id, fk)) yield fp, c, fk, (f_ie and f_ie.file_id) if fk != 'directory': continue if c != 'V': # don't descend unversioned directories continue for ff in descend(fp, f_ie.file_id, fap): yield ff for f in descend('', None, self.basedir): yield f def unknowns(self): for subp in self.extras(): if not self.is_ignored(subp): yield subp def extras(self): """Yield all unknown files in this WorkingTree. If there are any unknown directories then only the directory is returned, not all its children. But if there are unknown files under a versioned subdirectory, they are returned. Currently returned depth-first, sorted by name within directories. """ ## TODO: Work from given directory downwards for path, dir_entry in self.inventory.directories(): mutter("search for unknowns in %r" % path) dirabs = self.abspath(path) if not isdir(dirabs): # e.g. directory deleted continue fl = [] for subf in os.listdir(dirabs): if (subf != '.bzr' and (subf not in dir_entry.children)): fl.append(subf) fl.sort() for subf in fl: subp = appendpath(path, subf) yield subp def ignored_files(self): """Yield list of PATH, IGNORE_PATTERN""" for subp in self.extras(): pat = self.is_ignored(subp) if pat != None: yield subp, pat def get_ignore_list(self): """Return list of ignore patterns. Cached in the Tree object after the first call. """ if hasattr(self, '_ignorelist'): return self._ignorelist l = bzrlib.DEFAULT_IGNORE[:] if self.has_filename(bzrlib.IGNORE_FILENAME): f = self.get_file_byname(bzrlib.IGNORE_FILENAME) l.extend([line.rstrip("\n\r") for line in f.readlines()]) self._ignorelist = l return l def is_ignored(self, filename): """Check whether the filename matches an ignore pattern. Patterns containing '/' need to match the whole path; others match against only the last component. If the file is ignored, returns the pattern which caused it to be ignored, otherwise None. So this can simply be used as a boolean if desired.""" ## TODO: Use '**' to match directories, and other extended globbing stuff from cvs/rsync. for pat in self.get_ignore_list(): if '/' in pat: # as a special case, you can put ./ at the start of a pattern; # this is good to match in the top-level only; if pat[:2] == './': newpat = pat[2:] else: newpat = pat if fnmatch.fnmatchcase(filename, newpat): return pat else: if fnmatch.fnmatchcase(splitpath(filename)[-1], pat): return pat return None class RevisionTree(Tree): """Tree viewing a previous revision. File text can be retrieved from the text store. :todo: Some kind of `__repr__` method, but a good one probably means knowing the branch and revision number, or at least passing a description to the constructor. """ def __init__(self, store, inv): self._store = store self._inventory = inv def get_file(self, file_id): ie = self._inventory[file_id] f = self._store[ie.text_id] mutter(" get fileid{%s} from %r" % (file_id, self)) self._check_retrieved(ie, f) return f def get_file_size(self, file_id): return self._inventory[file_id].text_size def get_file_sha1(self, file_id): ie = self._inventory[file_id] return ie.text_sha1 def has_filename(self, filename): return bool(self.inventory.path2id(filename)) def list_files(self): # The only files returned by this are those from the version for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id class EmptyTree(Tree): def __init__(self): self._inventory = Inventory() def has_filename(self, filename): return False def list_files(self): if False: # just to make it a generator yield None ###################################################################### # diff # TODO: Merge these two functions into a single one that can operate # on either a whole tree or a set of files. # TODO: Return the diff in order by filename, not by category or in # random order. Can probably be done by lock-stepping through the # filenames from both trees. def file_status(filename, old_tree, new_tree): """Return single-letter status, old and new names for a file. The complexity here is in deciding how to represent renames; many complex cases are possible. """ old_inv = old_tree.inventory new_inv = new_tree.inventory new_id = new_inv.path2id(filename) old_id = old_inv.path2id(filename) if not new_id and not old_id: # easy: doesn't exist in either; not versioned at all if new_tree.is_ignored(filename): return 'I', None, None else: return '?', None, None elif new_id: # There is now a file of this name, great. pass else: # There is no longer a file of this name, but we can describe # what happened to the file that used to have # this name. There are two possibilities: either it was # deleted entirely, or renamed. assert old_id if new_inv.has_id(old_id): return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id) else: return 'D', old_inv.id2path(old_id), None # if the file_id is new in this revision, it is added if new_id and not old_inv.has_id(new_id): return 'A' # if there used to be a file of this name, but that ID has now # disappeared, it is deleted if old_id and not new_inv.has_id(old_id): return 'D' return 'wtf?' def find_renames(old_inv, new_inv): for file_id in old_inv: if file_id not in new_inv: continue old_name = old_inv.id2path(file_id) new_name = new_inv.id2path(file_id) if old_name != new_name: yield (old_name, new_name) commit refs/heads/master mark :165 committer 1112622691 +1000 data 70 - put .bzr.log in /home/mbp - include the working directory in the log from :164 M 644 inline NEWS data 2262 bzr-0.0.3 NOT RELEASED YET ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * Basic "bzr mv" support for renames! (Not all scenarios work through the command at the moment, but the inventory support is there.) * New "renames" command lists files renamed since base revision. CHANGES: * .bzr.log is placed in $HOME, not pwd. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2003-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/trace.py data 3666 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os, time, socket, stat import bzrlib ###################################################################### # messages and logging ## TODO: If --verbose is given then write to both stderr and ## _tracefile; perhaps replace _tracefile with a tee thing. global _tracefile, _starttime _tracefile = None # used to have % (os.environ['USER'], time.time(), os.getpid()), 'w') # If false, notes also go to stdout; should replace this with --silent # at some point. silent = False # TODO: Somehow tie this to the --verbose option? verbose = False def warning(msg): b = 'bzr: warning: ' + msg + '\n' sys.stderr.write(b) _tracefile.write(b) #_tracefile.flush() def mutter(msg): _tracefile.write(msg) _tracefile.write('\n') # _tracefile.flush() if verbose: sys.stderr.write('- ' + msg + '\n') def note(msg): b = '* ' + str(msg) + '\n' if not silent: sys.stderr.write(b) _tracefile.write(b) # _tracefile.flush() def log_error(msg): sys.stderr.write(msg) _tracefile.write(msg) # _tracefile.flush() def create_tracefile(argv): # TODO: Also show contents of /etc/lsb-release, if it can be parsed. # Perhaps that should eventually go into the platform library? # TODO: If the file doesn't exist, add a note describing it. # Messages are always written to here, so that we have some # information if something goes wrong. In a future version this # file will be removed on successful completion. global _starttime, _tracefile _starttime = os.times()[4] # XXX: Is HOME always set on Windows? trace_fname = os.path.join(os.environ['HOME'], '.bzr.log') _tracefile = file(trace_fname, 'at') t = _tracefile if os.fstat(t.fileno())[stat.ST_SIZE] == 0: t.write("\nthis is a debug log for diagnosing/reporting problems in bzr\n") t.write("you can delete or truncate this file, or include sections in\n") t.write("bug reports to bazaar-ng@lists.canonical.com\n\n") # TODO: If we failed to create the file, perhaps give a warning # but don't abort; send things to /dev/null instead? t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % bzrlib.osutils.format_date(time.time())) t.write(' version: %s\n' % bzrlib.__version__) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.getfqdn())) t.write(' arguments: %r\n' % argv) t.write(' working dir: %s\n' % os.getcwdu()) t.write(' platform: %s\n' % sys.platform) t.write(' python: %s\n' % (sys.version_info,)) import atexit atexit.register(_close_trace) def _close_trace(): times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum, %.3f elapsed" % (times[:4] + ((times[4] - _starttime),))) commit refs/heads/master mark :166 committer 1112623074 +1000 data 24 - Write .bzr.log in utf8 from :165 M 644 inline NEWS data 2300 bzr-0.0.3 NOT RELEASED YET ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * Basic "bzr mv" support for renames! (Not all scenarios work through the command at the moment, but the inventory support is there.) * New "renames" command lists files renamed since base revision. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2003-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/trace.py data 3689 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os, time, socket, stat, codecs import bzrlib ###################################################################### # messages and logging ## TODO: If --verbose is given then write to both stderr and ## _tracefile; perhaps replace _tracefile with a tee thing. global _tracefile, _starttime _tracefile = None # used to have % (os.environ['USER'], time.time(), os.getpid()), 'w') # If false, notes also go to stdout; should replace this with --silent # at some point. silent = False # TODO: Somehow tie this to the --verbose option? verbose = False def warning(msg): b = 'bzr: warning: ' + msg + '\n' sys.stderr.write(b) _tracefile.write(b) #_tracefile.flush() def mutter(msg): _tracefile.write(msg) _tracefile.write('\n') # _tracefile.flush() if verbose: sys.stderr.write('- ' + msg + '\n') def note(msg): b = '* ' + str(msg) + '\n' if not silent: sys.stderr.write(b) _tracefile.write(b) # _tracefile.flush() def log_error(msg): sys.stderr.write(msg) _tracefile.write(msg) # _tracefile.flush() def create_tracefile(argv): # TODO: Also show contents of /etc/lsb-release, if it can be parsed. # Perhaps that should eventually go into the platform library? # TODO: If the file doesn't exist, add a note describing it. # Messages are always written to here, so that we have some # information if something goes wrong. In a future version this # file will be removed on successful completion. global _starttime, _tracefile _starttime = os.times()[4] # XXX: Is HOME always set on Windows? trace_fname = os.path.join(os.environ['HOME'], '.bzr.log') _tracefile = codecs.open(trace_fname, 'at', 'utf8') t = _tracefile if os.fstat(t.fileno())[stat.ST_SIZE] == 0: t.write("\nthis is a debug log for diagnosing/reporting problems in bzr\n") t.write("you can delete or truncate this file, or include sections in\n") t.write("bug reports to bazaar-ng@lists.canonical.com\n\n") # TODO: If we failed to create the file, perhaps give a warning # but don't abort; send things to /dev/null instead? t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % bzrlib.osutils.format_date(time.time())) t.write(' version: %s\n' % bzrlib.__version__) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.getfqdn())) t.write(' arguments: %r\n' % argv) t.write(' working dir: %s\n' % os.getcwdu()) t.write(' platform: %s\n' % sys.platform) t.write(' python: %s\n' % (sys.version_info,)) import atexit atexit.register(_close_trace) def _close_trace(): times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum, %.3f elapsed" % (times[:4] + ((times[4] - _starttime),))) commit refs/heads/master mark :167 committer 1112677849 +1000 data 14 update version from :166 M 644 inline bzrlib/__init__.py data 1488 # (C) 2005 Canonical Development Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr library""" from inventory import Inventory, InventoryEntry from branch import Branch, ScratchBranch from osutils import format_date from tree import Tree from diff import diff_trees from trace import mutter, warning import add BZRDIR = ".bzr" DEFAULT_IGNORE = ['.bzr.log', '*~', '#*#', '*$', '.#*', '*.tmp', '*.bak', '*.BAK', '*.orig', '*.o', '*.obj', '*.a', '*.py[oc]', '*.so', '*.exe', '*.elc', '{arch}', 'CVS', '.svn', '_darcs', 'SCCS', 'RCS', 'TAGS', '.make.state', '.sconsign'] IGNORE_FILENAME = ".bzrignore" __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __version__ = '0.0.3pre' commit refs/heads/master mark :168 committer 1112683742 +1000 data 20 new "rename" command from :167 M 644 inline NEWS data 2348 bzr-0.0.3 NOT RELEASED YET ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file. * Basic "bzr mv" support for renames! (Not all scenarios work through the command at the moment, but the inventory support is there.) * New "renames" command lists files renamed since base revision. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2003-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/branch.py data 30953 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f is None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f last_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be bailout('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. :todo: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. :todo: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. :todo: Keep the on-disk branch locked while the object exists. :todo: mkdir() method. """ def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. :param base: Base directory for the branch. :param init: If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. :param find_root: If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch""" return file(self.controlfilename(file_or_path), mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'wb').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'rb').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() inv = Inventory.read_xml(self.controlfile('inventory', 'r')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'w') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. This puts the files in the Added state, so that they will be recorded by the next commit. :todo: Perhaps have an option to add the ids even if the files do not (yet) exist. :todo: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. :todo: Option to specify file id. :todo: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on :todo: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True :todo: Do something useful with directories. :todo: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. :param timestamp: if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) mutter("append to revision-history") f = self.controlfile('revision-history', 'at') f.write(rev_id + '\n') f.close() if verbose: note("commited r%d" % self.revno()) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. :todo: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: bailout("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original'): """Write out human-readable log of commits to this branch :param utc: If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l revno += 1 precursor = p def rename_one(self, from_rel, to_rel): tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) os.rename(self.abspath(from_rel), self.abspath(to_rel)) self._write_inventory(inv) def rename(self, from_paths, to_name): """Rename files. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory dest_dir = isdir(self.abspath(to_name)) if dest_dir: # TODO: Wind back properly if some can't be moved? dest_dir_id = inv.path2id(to_name) if not dest_dir_id and to_name != '': bailout("destination %r is not a versioned directory" % to_name) for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), dest_dir_id, name_tail) os.rename(self.abspath(f), self.abspath(dest_path)) self._write_inventory(inv) else: if len(from_paths) != 1: bailout("when moving multiple files, destination must be a directory") bailout("rename to non-directory %r not implemented sorry" % to_name) def show_status(branch, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo :todo: Get state for single files. :todo: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = branch.basis_tree() old_inv = old.inventory new = branch.working_tree() new_inv = new.inventory for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("wierd file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" try: shutil.rmtree(self.base) except OSError: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/commands.py data 27517 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) def cmd_mv(source_list, dest): b = Branch('.') b.rename([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] --revision REV Show changes since REV, rather than predecessor. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Diff selected files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): inv = Branch('.').basis_tree().inventory def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(verbose=True): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = cmdfn.__doc__ if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'commit': [], 'diff': [], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'mv': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot prof = hotshot.Profile('.bzr.profile') ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load('.bzr.profile') #stats.strip_dirs() stats.sort_stats('time') stats.print_stats(20) else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :169 committer 1112689203 +1000 data 49 Start of shell-based black-box testing in test.sh from :168 M 644 inline test.sh data 1574 #! /bin/sh -pe # Simple shell-based tests for bzr. # This is meant to exercise the external behaviour, command line # parsing and similar things and compliment the inwardly-turned # testing done by doctest. # This must already exist and be in the right place if ! [ -d bzr-test.tmp ] then echo "please create bzr-test.tmp" exit 1 fi rm -rf bzr-test.tmp mkdir bzr-test.tmp exec > bzr-test.log exec 2>&1 set -x cd bzr-test.tmp rm -rf .bzr # some information commands bzr help bzr version # invalid commands are detected ! bzr pants # some experiments with renames bzr init echo "hello world" > test.txt bzr unknowns # should be the only unknown file [ "`bzr unknowns`" = test.txt ] # can't rename unversioned files; use the regular unix rename command ! bzr rename test.txt new-test.txt # ok, so now add it and see what happens bzr add test.txt [ -z "`bzr unknowns`" ] # after adding even before committing you can rename files bzr rename test.txt newname.txt [ "`bzr status`" = "A newname.txt" ] bzr commit -m "add first revision" # now more complicated renames mkdir sub1 ! bzr rename newname.txt sub1 ! bzr rename newname.txt sub1/foo.txt bzr add sub1 ! bzr rename newname.txt sub1 bzr rename newname.txt sub1/foo.txt [ -f sub1/foo.txt ] [ ! -f newname.txt ] bzr rename sub1/foo.txt newname.txt [ -f newname.txt ] bzr rename newname.txt sub1/foo.txt bzr rename sub1/foo.txt sub1/bar.txt cd sub1 mkdir sub2 bzr add sub2 bzr rename bar.txt sub2/bar.txt cd sub2 bzr rename bar.txt ../../bar.txt cd ../../ bzr commit -m "more renames" M 644 inline .bzrignore data 94 ./doc/*.html *.py[oc] *~ .arch-ids .bzr.profile .arch-inventory {arch} CHANGELOG bzr-test.log M 644 inline NEWS data 2406 bzr-0.0.3 NOT RELEASED YET ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file. * Basic "bzr mv" support for renames! (Not all scenarios work through the command at the moment, but the inventory support is there.) * New "renames" command lists files renamed since base revision. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2003-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. commit refs/heads/master mark :170 committer 1112689491 +1000 data 19 start adding quotes from :169 M 644 inline doc/quotes.txt data 123 If you're just quilting the good ideas of others, then that seems more like standing on the shoulders of giants -- bje commit refs/heads/master mark :171 committer 1112690044 +1000 data 51 better error message when working file rename fails from :170 M 644 inline bzrlib/branch.py data 31202 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f is None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f last_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be bailout('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. :todo: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. :todo: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. :todo: Keep the on-disk branch locked while the object exists. :todo: mkdir() method. """ def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. :param base: Base directory for the branch. :param init: If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. :param find_root: If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch""" return file(self.controlfilename(file_or_path), mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'wb').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'rb').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() inv = Inventory.read_xml(self.controlfile('inventory', 'r')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'w') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. This puts the files in the Added state, so that they will be recorded by the next commit. :todo: Perhaps have an option to add the ids even if the files do not (yet) exist. :todo: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. :todo: Option to specify file id. :todo: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on :todo: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True :todo: Do something useful with directories. :todo: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. :param timestamp: if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) mutter("append to revision-history") f = self.controlfile('revision-history', 'at') f.write(rev_id + '\n') f.close() if verbose: note("commited r%d" % self.revno()) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. :todo: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: bailout("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original'): """Write out human-readable log of commits to this branch :param utc: If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l revno += 1 precursor = p def rename_one(self, from_rel, to_rel): tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def rename(self, from_paths, to_name): """Rename files. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory dest_dir = isdir(self.abspath(to_name)) if dest_dir: # TODO: Wind back properly if some can't be moved? dest_dir_id = inv.path2id(to_name) if not dest_dir_id and to_name != '': bailout("destination %r is not a versioned directory" % to_name) for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), dest_dir_id, name_tail) os.rename(self.abspath(f), self.abspath(dest_path)) self._write_inventory(inv) else: if len(from_paths) != 1: bailout("when moving multiple files, destination must be a directory") bailout("rename to non-directory %r not implemented sorry" % to_name) def show_status(branch, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo :todo: Get state for single files. :todo: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = branch.basis_tree() old_inv = old.inventory new = branch.working_tree() new_inv = new.inventory for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("wierd file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" try: shutil.rmtree(self.base) except OSError: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline test.sh data 1609 #! /bin/sh -pe # Simple shell-based tests for bzr. # This is meant to exercise the external behaviour, command line # parsing and similar things and compliment the inwardly-turned # testing done by doctest. # This must already exist and be in the right place if ! [ -d bzr-test.tmp ] then echo "please create bzr-test.tmp" exit 1 fi rm -rf bzr-test.tmp mkdir bzr-test.tmp exec > bzr-test.log exec 2>&1 set -x cd bzr-test.tmp rm -rf .bzr # some information commands bzr help bzr version # invalid commands are detected ! bzr pants # some experiments with renames bzr init echo "hello world" > test.txt bzr unknowns # should be the only unknown file [ "`bzr unknowns`" = test.txt ] # can't rename unversioned files; use the regular unix rename command ! bzr rename test.txt new-test.txt # ok, so now add it and see what happens bzr add test.txt [ -z "`bzr unknowns`" ] # after adding even before committing you can rename files bzr rename test.txt newname.txt [ "`bzr status`" = "A newname.txt" ] bzr commit -m "add first revision" # now more complicated renames mkdir sub1 ! bzr rename newname.txt sub1 ! bzr rename newname.txt sub1/foo.txt bzr add sub1 ! bzr rename newname.txt sub1 bzr rename newname.txt sub1/foo.txt [ -f sub1/foo.txt ] [ ! -f newname.txt ] bzr rename sub1/foo.txt newname.txt [ -f newname.txt ] bzr rename newname.txt sub1/foo.txt bzr rename sub1/foo.txt sub1/bar.txt cd sub1 mkdir sub2 bzr add sub2 bzr rename bar.txt sub2/bar.txt cd sub2 bzr rename bar.txt ../../bar.txt cd ../../ bzr commit -m "more renames" ! bzr rename sub1 sub1/knotted-up commit refs/heads/master mark :172 committer 1112691932 +1000 data 80 - clearer check against attempts to introduce directory loops in the inventory from :171 M 644 inline bzrlib/inventory.py data 17987 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Inventories map files to their name in a revision.""" # TODO: Maybe store inventory_id in the file? Not really needed. __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os.path, types, re from sets import Set try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from xml import XMLMixin from errors import bailout import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') >>> i.add(InventoryEntry('123', 'src', kind='directory')) >>> i.add(InventoryEntry('2323', 'hello.c', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id=None)) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', parent_id='123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', parent_id='123')) >>> i.add(InventoryEntry('2325', 'wibble', parent_id='123', kind='directory')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', parent_id='2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' :todo: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ def __init__(self, file_id, name, kind='file', text_id=None, parent_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c') >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c') Traceback (most recent call last): BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", []) """ if len(splitpath(name)) != 1: bailout('InventoryEntry name is not a simple filename: %r' % name) self.file_id = file_id self.name = name assert kind in ['file', 'directory'] self.kind = kind self.text_id = text_id self.parent_id = parent_id self.text_sha1 = None self.text_size = None if kind == 'directory': self.children = {} def sorted_children(self): l = self.children.items() l.sort() return l def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.text_id, self.parent_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size is not None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1', 'parent_id']: v = getattr(self, f) if v is not None: e.set(f, v) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind')) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') self.parent_id = elt.get('parent_id') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __cmp__(self, other): if self is other: return 0 if not isinstance(other, InventoryEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.name, other.name) \ or cmp(self.text_sha1, other.text_sha1) \ or cmp(self.text_size, other.text_size) \ or cmp(self.text_id, other.text_id) \ or cmp(self.parent_id, other.parent_id) \ or cmp(self.kind, other.kind) class RootEntry(InventoryEntry): def __init__(self, file_id): self.file_id = file_id self.children = {} self.kind = 'root_directory' self.parent_id = None self.name = '' def __cmp__(self, other): if self is other: return 0 if not isinstance(other, RootEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.children, other.children) class Inventory(XMLMixin): """Inventory of versioned files in a tree. An Inventory acts like a set of InventoryEntry items. You can also look files up by their file_id or name. May be read from and written to a metadata file in a tree. To manipulate the inventory (for example to add a file), it is read in, modified, and then written back out. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted, other than through the Inventory API. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c')) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ ## TODO: Make sure only canonical filenames are stored. ## TODO: Do something sensible about the possible collisions on ## case-losing filesystems. Perhaps we should just always forbid ## such collisions. ## TODO: No special cases for root, rather just give it a file id ## like everything else. ## TODO: Probably change XML serialization to use nesting def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. The inventory is created with a default root directory, with an id of None. """ self.root = RootEntry(None) self._byid = {None: self.root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, from_dir=None): """Return (path, entry) pairs, in order by name.""" if from_dir == None: assert self.root from_dir = self.root elif isinstance(from_dir, basestring): from_dir = self._byid[from_dir] kids = from_dir.children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(from_dir=ie.file_id): yield '/'.join((name, cn)), cie def directories(self, from_dir=None): """Return (path, entry) pairs for all directories. """ def descend(parent_ie): parent_name = parent_ie.name yield parent_name, parent_ie # directory children in sorted order dn = [] for ie in parent_ie.children.itervalues(): if ie.kind == 'directory': dn.append((ie.name, ie)) dn.sort() for name, child_ie in dn: for sub_name, sub_ie in descend(child_ie): yield appendpath(parent_name, sub_name), sub_ie for name, ie in descend(self.root): yield name, ie def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c')) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c')) >>> inv['123123'].name 'hello.c' """ return self._byid[file_id] def get_child(self, parent_id, filename): return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self._byid: bailout("inventory already contains entry with id {%s}" % entry.file_id) try: parent = self._byid[entry.parent_id] except KeyError: bailout("parent_id %r not in inventory" % entry.parent_id) if parent.children.has_key(entry.name): bailout("%s is already versioned" % appendpath(self.id2path(parent.file_id), entry.name)) self._byid[entry.file_id] = entry parent.children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: bailout("cannot re-add root of inventory") if file_id is None: file_id = bzrlib.branch.gen_file_id(relpath) parent_id = self.path2id(parts[:-1]) ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c')) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def id_set(self): return Set(self._byid) def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c')) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __cmp__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo')) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo')) >>> i1 == i2 True """ if self is other: return 0 if not isinstance(other, Inventory): return NotImplemented if self.id_set() ^ other.id_set(): return 1 for file_id in self._byid: c = cmp(self[file_id], other[file_id]) if c: return c return 0 def get_idpath(self, file_id): """Return a list of file_ids for the path to an entry. The list contains one element for each directory followed by the id of the file itself. So the length of the returned list is equal to the depth of the file in the tree, counting the root directory as depth 0. """ p = [] while file_id != None: ie = self._byid[file_id] p.insert(0, ie.file_id) file_id = ie.parent_id return p def id2path(self, file_id): """Return as a list the path to file_id.""" p = [] while file_id != None: ie = self._byid[file_id] p.insert(0, ie.name) file_id = ie.parent_id return '/'.join(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. """ if isinstance(name, types.StringTypes): name = splitpath(name) parent = self[None] for f in name: try: cie = parent.children[f] assert cie.name == f parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): return self._byid.has_key(file_id) def rename(self, file_id, new_parent_id, new_name): """Move a file within the inventory. This can change either the name, or the parent, or both. This does not move the working file.""" if not is_valid_name(new_name): bailout("not an acceptable filename: %r" % new_name) new_parent = self._byid[new_parent_id] if new_name in new_parent.children: bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id))) new_parent_idpath = self.get_idpath(new_parent_id) if file_id in new_parent_idpath: bailout("cannot move directory %r into a subdirectory of itself, %r" % (self.id2path(file_id), self.id2path(new_parent_id))) file_ie = self._byid[file_id] old_parent = self._byid[file_ie.parent_id] # TODO: Don't leave things messed up if this fails del old_parent.children[file_ie.name] new_parent.children[new_name] = file_ie file_ie.name = new_name file_ie.parent_id = new_parent_id _NAME_RE = re.compile(r'^[^/\\]+$') def is_valid_name(name): return bool(_NAME_RE.match(name)) if __name__ == '__main__': import doctest, inventory doctest.testmod(inventory) commit refs/heads/master mark :173 committer 1112692194 +1000 data 42 more random bytes in revision and file ids from :172 M 644 inline bzrlib/branch.py data 31204 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f is None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f last_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be bailout('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. :todo: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. :todo: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. :todo: Keep the on-disk branch locked while the object exists. :todo: mkdir() method. """ def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. :param base: Base directory for the branch. :param init: If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. :param find_root: If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch""" return file(self.controlfilename(file_or_path), mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'wb').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'rb').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() inv = Inventory.read_xml(self.controlfile('inventory', 'r')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'w') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. This puts the files in the Added state, so that they will be recorded by the next commit. :todo: Perhaps have an option to add the ids even if the files do not (yet) exist. :todo: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. :todo: Option to specify file id. :todo: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on :todo: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True :todo: Do something useful with directories. :todo: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. :param timestamp: if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) mutter("append to revision-history") f = self.controlfile('revision-history', 'at') f.write(rev_id + '\n') f.close() if verbose: note("commited r%d" % self.revno()) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. :todo: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: bailout("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original'): """Write out human-readable log of commits to this branch :param utc: If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l revno += 1 precursor = p def rename_one(self, from_rel, to_rel): tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def rename(self, from_paths, to_name): """Rename files. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory dest_dir = isdir(self.abspath(to_name)) if dest_dir: # TODO: Wind back properly if some can't be moved? dest_dir_id = inv.path2id(to_name) if not dest_dir_id and to_name != '': bailout("destination %r is not a versioned directory" % to_name) for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), dest_dir_id, name_tail) os.rename(self.abspath(f), self.abspath(dest_path)) self._write_inventory(inv) else: if len(from_paths) != 1: bailout("when moving multiple files, destination must be a directory") bailout("rename to non-directory %r not implemented sorry" % to_name) def show_status(branch, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo :todo: Get state for single files. :todo: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = branch.basis_tree() old_inv = old.inventory new = branch.working_tree() new_inv = new.inventory for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("wierd file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" try: shutil.rmtree(self.base) except OSError: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(12)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(12)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :174 committer 1112708796 +1000 data 51 - New 'move' command; now separated out from rename from :173 M 644 inline NEWS data 2535 bzr-0.0.3 NOT RELEASED YET ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * Basic "bzr mv" support for renames! (Not all scenarios work through the command at the moment, but the inventory support is there.) * New "renames" command lists files renamed since base revision. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2003-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/branch.py data 32326 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f is None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f last_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be bailout('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. :todo: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. :todo: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. :todo: Keep the on-disk branch locked while the object exists. :todo: mkdir() method. """ def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. :param base: Base directory for the branch. :param init: If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. :param find_root: If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch""" return file(self.controlfilename(file_or_path), mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'wb').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'rb').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() inv = Inventory.read_xml(self.controlfile('inventory', 'r')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'w') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. This puts the files in the Added state, so that they will be recorded by the next commit. :todo: Perhaps have an option to add the ids even if the files do not (yet) exist. :todo: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. :todo: Option to specify file id. :todo: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on :todo: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True :todo: Do something useful with directories. :todo: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. :param timestamp: if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) mutter("append to revision-history") f = self.controlfile('revision-history', 'at') f.write(rev_id + '\n') f.close() if verbose: note("commited r%d" % self.revno()) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. :todo: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: bailout("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original'): """Write out human-readable log of commits to this branch :param utc: If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l revno += 1 precursor = p def rename_one(self, from_rel, to_rel): tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r is not a versioned directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind != 'directory': bailout("destination %r is not a versioned directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(branch, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo :todo: Get state for single files. :todo: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = branch.basis_tree() old_inv = old.inventory new = branch.working_tree() new_inv = new.inventory for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("wierd file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" try: shutil.rmtree(self.base) except OSError: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(12)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(12)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/commands.py data 27610 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] --revision REV Show changes since REV, rather than predecessor. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Diff selected files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): inv = Branch('.').basis_tree().inventory def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(verbose=True): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = cmdfn.__doc__ if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'commit': [], 'diff': [], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot prof = hotshot.Profile('.bzr.profile') ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load('.bzr.profile') #stats.strip_dirs() stats.sort_stats('time') stats.print_stats(20) else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') M 644 inline bzrlib/tests.py data 6231 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # XXX: We might prefer these to be in a text file rather than Python # source, but that only works in doctest from Python 2.4 and later, # which is not present in Warty. r""" Bazaar-NG test cases ******************** These are run by ``bzr.doctest``. >>> import bzrlib, os >>> from bzrlib import ScratchBranch >>> from bzrlib.osutils import isdir, isfile >>> bzrlib.commands.cmd_rocks() it sure does! Hey, nice place to begin. The basic object is a Branch. We have a special helper class ScratchBranch that automatically makes a directory and cleans itself up, but is in other respects identical. ScratchBranches are initially empty: >>> b = bzrlib.ScratchBranch() >>> b.show_status() New files in that directory are, it is initially unknown: >>> file(b.base + '/hello.c', 'wt').write('int main() {}') >>> b.show_status() ? hello.c That's not quite true; some files (like editor backups) are ignored by default: >>> file(b.base + '/hello.c~', 'wt').write('int main() {}') >>> b.show_status() ? hello.c >>> list(b.unknowns()) ['hello.c'] The ``add`` command marks a file to be added in the next revision: >>> b.add('hello.c') >>> b.show_status() A hello.c You can also add files that otherwise would be ignored. The ignore patterns only apply to files that would be otherwise unknown, so they have no effect once it's added. >>> b.add('hello.c~') >>> b.show_status() A hello.c A hello.c~ It is an error to add a file that isn't present in the working copy: >>> b.add('nothere') Traceback (most recent call last): ... BzrError: ('cannot add: not a regular file or directory: nothere', []) If we add a file and then change our mind, we can either revert it or remove the file. If we revert, we are left with the working copy (in either I or ? state). If we remove, the working copy is gone. Let's do that to the backup, presumably added accidentally. >>> b.remove('hello.c~') >>> b.show_status() A hello.c Now to commit, creating a new revision. (Fake the date and name for reproducibility.) >>> b.commit('start hello world', timestamp=0, committer='foo@nowhere') >>> b.show_status() >>> b.show_status(show_all=True) . hello.c I hello.c~ We can look back at history >>> r = b.get_revision(b.lookup_revision(1)) >>> r.message 'start hello world' >>> b.write_log(show_timezone='utc') ---------------------------------------- revno: 1 committer: foo@nowhere timestamp: Thu 1970-01-01 00:00:00 +0000 message: start hello world (The other fields will be a bit unpredictable, depending on who ran this test and when.) As of 2005-02-21, we can also add subdirectories to the revision! >>> os.mkdir(b.base + "/lib") >>> b.show_status() ? lib/ >>> b.add('lib') >>> b.show_status() A lib/ >>> b.commit('add subdir') >>> b.show_status() >>> b.show_status(show_all=True) . hello.c I hello.c~ . lib/ and we can also add files within subdirectories: >>> file(b.base + '/lib/hello', 'w').write('hello!\n') >>> b.show_status() ? lib/hello Tests for adding subdirectories, etc. >>> b = bzrlib.branch.ScratchBranch() >>> os.mkdir(b.abspath('d1')) >>> os.mkdir(b.abspath('d2')) >>> os.mkdir(b.abspath('d2/d3')) >>> list(b.working_tree().unknowns()) ['d1', 'd2'] Create some files, but they're not seen as unknown yet: >>> file(b.abspath('d1/f1'), 'w').close() >>> file(b.abspath('d2/f2'), 'w').close() >>> file(b.abspath('d2/f3'), 'w').close() >>> [v[0] for v in b.inventory.directories()] [''] >>> list(b.working_tree().unknowns()) ['d1', 'd2'] Adding a directory, and we see the file underneath: >>> b.add('d1') >>> [v[0] for v in b.inventory.directories()] ['', 'd1'] >>> list(b.working_tree().unknowns()) ['d2', 'd1/f1'] >>> # d2 comes first because it's in the top directory >>> b.add('d2') >>> b.commit('add some stuff') >>> list(b.working_tree().unknowns()) ['d1/f1', 'd2/d3', 'd2/f2', 'd2/f3'] >>> b.add('d1/f1') >>> list(b.working_tree().unknowns()) ['d2/d3', 'd2/f2', 'd2/f3'] Tests for ignored files and patterns: >>> b = ScratchBranch(dirs=['src', 'doc'], ... files=['configure.in', 'configure', ... 'doc/configure', 'foo.c', ... 'foo']) >>> list(b.unknowns()) ['configure', 'configure.in', 'doc', 'foo', 'foo.c', 'src'] >>> b.add(['doc', 'foo.c', 'src', 'configure.in']) >>> list(b.unknowns()) ['configure', 'foo', 'doc/configure'] >>> f = file(b.abspath('.bzrignore'), 'w') >>> f.write('./configure\n' ... './foo\n') >>> f.close() >>> b.add('.bzrignore') >>> list(b.unknowns()) ['doc/configure'] >>> b.commit("commit 1") >>> list(b.unknowns()) ['doc/configure'] >>> b.add("doc/configure") >>> b.commit("commit more") >>> del b Renames, etc: >>> b = ScratchBranch(files=['foo'], dirs=['subdir']) >>> b.add(['foo', 'subdir']) >>> b.commit('add foo') >>> list(b.unknowns()) [] >>> b.move(['foo'], 'subdir') foo => subdir/foo >>> b.show_status() R foo => subdir/foo >>> bzrlib.commands.cmd_renames(b.base) foo => subdir/foo >>> b.commit("move foo to subdir") >>> isfile(b.abspath('foo')) False >>> isfile(b.abspath('subdir/foo')) True """ commit refs/heads/master mark :175 committer 1112709026 +1000 data 36 fix up moving files into branch root from :174 M 644 inline bzrlib/branch.py data 32336 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f is None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f last_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be bailout('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. :todo: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. :todo: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. :todo: Keep the on-disk branch locked while the object exists. :todo: mkdir() method. """ def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. :param base: Base directory for the branch. :param init: If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. :param find_root: If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch""" return file(self.controlfilename(file_or_path), mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'wb').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'rb').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() inv = Inventory.read_xml(self.controlfile('inventory', 'r')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'w') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. This puts the files in the Added state, so that they will be recorded by the next commit. :todo: Perhaps have an option to add the ids even if the files do not (yet) exist. :todo: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. :todo: Option to specify file id. :todo: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on :todo: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True :todo: Do something useful with directories. :todo: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. :param timestamp: if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) mutter("append to revision-history") f = self.controlfile('revision-history', 'at') f.write(rev_id + '\n') f.close() if verbose: note("commited r%d" % self.revno()) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. :todo: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: bailout("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original'): """Write out human-readable log of commits to this branch :param utc: If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l revno += 1 precursor = p def rename_one(self, from_rel, to_rel): tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(branch, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo :todo: Get state for single files. :todo: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = branch.basis_tree() old_inv = old.inventory new = branch.working_tree() new_inv = new.inventory for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("wierd file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" try: shutil.rmtree(self.base) except OSError: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(12)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(12)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :176 committer 1112753146 +1000 data 38 New cat command contributed by janmar. from :175 M 644 inline NEWS data 2581 bzr-0.0.3 NOT RELEASED YET ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * Basic "bzr mv" support for renames! (Not all scenarios work through the command at the moment, but the inventory support is there.) * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2003-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/branch.py data 32539 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f is None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f last_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be bailout('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. :todo: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. :todo: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. :todo: Keep the on-disk branch locked while the object exists. :todo: mkdir() method. """ def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. :param base: Base directory for the branch. :param init: If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. :param find_root: If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch""" return file(self.controlfilename(file_or_path), mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'wb').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'rb').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() inv = Inventory.read_xml(self.controlfile('inventory', 'r')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'w') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. This puts the files in the Added state, so that they will be recorded by the next commit. :todo: Perhaps have an option to add the ids even if the files do not (yet) exist. :todo: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. :todo: Option to specify file id. :todo: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" tree = self.revision_tree(self.lookup_revision(revno)) tree.print_file(self.inventory.path2id(file)) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on :todo: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True :todo: Do something useful with directories. :todo: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. :param timestamp: if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) mutter("append to revision-history") f = self.controlfile('revision-history', 'at') f.write(rev_id + '\n') f.close() if verbose: note("commited r%d" % self.revno()) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. :todo: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: bailout("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original'): """Write out human-readable log of commits to this branch :param utc: If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l revno += 1 precursor = p def rename_one(self, from_rel, to_rel): tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(branch, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo :todo: Get state for single files. :todo: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = branch.basis_tree() old_inv = old.inventory new = branch.working_tree() new_inv = new.inventory for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("wierd file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" try: shutil.rmtree(self.base) except OSError: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(12)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(12)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/commands.py data 28008 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i is None: bailout("%s is not a versioned file" % filename) else: print i def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] --revision REV Show changes since REV, rather than predecessor. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Diff selected files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): inv = Branch('.').basis_tree().inventory def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(verbose=True): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = cmdfn.__doc__ if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': [], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot prof = hotshot.Profile('.bzr.profile') ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load('.bzr.profile') #stats.strip_dirs() stats.sort_stats('time') stats.print_stats(20) else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') M 644 inline bzrlib/tree.py data 14154 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tree classes, representing directory at point in time. """ from sets import Set import os.path, os, fnmatch from osutils import pumpfile, compare_files, filesize, quotefn, sha_file, \ joinpath, splitpath, appendpath, isdir, isfile, file_kind, fingerprint_file import errno from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE from inventory import Inventory from trace import mutter, note from errors import bailout import branch import bzrlib class Tree: """Abstract file tree. There are several subclasses: * `WorkingTree` exists as files on disk editable by the user. * `RevisionTree` is a tree as recorded at some point in the past. * `EmptyTree` Trees contain an `Inventory` object, and also know how to retrieve file texts mentioned in the inventory, either from a working directory or from a store. It is possible for trees to contain files that are not described in their inventory or vice versa; for this use `filenames()`. Trees can be compared, etc, regardless of whether they are working trees or versioned trees. """ def has_filename(self, filename): """True if the tree has given filename.""" raise NotImplementedError() def has_id(self, file_id): return self.inventory.has_id(file_id) def id_set(self): """Return set of all ids in this tree.""" return self.inventory.id_set() def id2path(self, file_id): return self.inventory.id2path(file_id) def _get_inventory(self): return self._inventory inventory = property(_get_inventory, doc="Inventory of this Tree") def _check_retrieved(self, ie, f): fp = fingerprint_file(f) f.seek(0) if ie.text_size is not None: if ie.text_size != fp['size']: bailout("mismatched size for file %r in %r" % (ie.file_id, self._store), ["inventory expects %d bytes" % ie.text_size, "file is actually %d bytes" % fp['size'], "store is probably damaged/corrupt"]) if ie.text_sha1 != fp['sha1']: bailout("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store), ["inventory expects %s" % ie.text_sha1, "file is actually %s" % fp['sha1'], "store is probably damaged/corrupt"]) def print_file(self, fileid): """Print file with id `fileid` to stdout.""" import sys pumpfile(self.get_file(fileid), sys.stdout) def export(self, dest): """Export this tree to a new directory. `dest` should not exist, and will be created holding the contents of this tree. :todo: To handle subdirectories we need to create the directories first. :note: If the export fails, the destination directory will be left in a half-assed state. """ os.mkdir(dest) mutter('export version %r' % self) inv = self.inventory for dp, ie in inv.iter_entries(): kind = ie.kind fullpath = appendpath(dest, dp) if kind == 'directory': os.mkdir(fullpath) elif kind == 'file': pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb')) else: bailout("don't know how to export {%s} of kind %r", fid, kind) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) class WorkingTree(Tree): """Working copy tree. The inventory is held in the `Branch` working-inventory, and the files are in a directory on disk. It is possible for a `WorkingTree` to have a filename which is not listed in the Inventory and vice versa. """ def __init__(self, basedir, inv): self._inventory = inv self.basedir = basedir self.path2id = inv.path2id def __repr__(self): return "<%s of %s>" % (self.__class__.__name__, self.basedir) def abspath(self, filename): return os.path.join(self.basedir, filename) def has_filename(self, filename): return os.path.exists(self.abspath(filename)) def get_file(self, file_id): return self.get_file_byname(self.id2path(file_id)) def get_file_byname(self, filename): return file(self.abspath(filename), 'rb') def _get_store_filename(self, file_id): return self.abspath(self.id2path(file_id)) def has_id(self, file_id): # files that have been deleted are excluded if not self.inventory.has_id(file_id): return False return os.access(self.abspath(self.inventory.id2path(file_id)), os.F_OK) def get_file_size(self, file_id): return os.stat(self._get_store_filename(file_id))[ST_SIZE] def get_file_sha1(self, file_id): f = self.get_file(file_id) return sha_file(f) def file_class(self, filename): if self.path2id(filename): return 'V' elif self.is_ignored(filename): return 'I' else: return '?' def list_files(self): """Recursively list all files as (path, class, kind, id). Lists, but does not descend into unversioned directories. This does not include files that have been deleted in this tree. Skips the control directory. """ inv = self.inventory def descend(from_dir, from_dir_id, dp): ls = os.listdir(dp) ls.sort() for f in ls: ## TODO: If we find a subdirectory with its own .bzr ## directory, then that is a separate tree and we ## should exclude it. if bzrlib.BZRDIR == f: continue # path within tree fp = appendpath(from_dir, f) # absolute path fap = appendpath(dp, f) f_ie = inv.get_child(from_dir_id, f) if f_ie: c = 'V' elif self.is_ignored(fp): c = 'I' else: c = '?' fk = file_kind(fap) if f_ie: if f_ie.kind != fk: bailout("file %r entered as kind %r id %r, now of kind %r" % (fap, f_ie.kind, f_ie.file_id, fk)) yield fp, c, fk, (f_ie and f_ie.file_id) if fk != 'directory': continue if c != 'V': # don't descend unversioned directories continue for ff in descend(fp, f_ie.file_id, fap): yield ff for f in descend('', None, self.basedir): yield f def unknowns(self): for subp in self.extras(): if not self.is_ignored(subp): yield subp def extras(self): """Yield all unknown files in this WorkingTree. If there are any unknown directories then only the directory is returned, not all its children. But if there are unknown files under a versioned subdirectory, they are returned. Currently returned depth-first, sorted by name within directories. """ ## TODO: Work from given directory downwards for path, dir_entry in self.inventory.directories(): mutter("search for unknowns in %r" % path) dirabs = self.abspath(path) if not isdir(dirabs): # e.g. directory deleted continue fl = [] for subf in os.listdir(dirabs): if (subf != '.bzr' and (subf not in dir_entry.children)): fl.append(subf) fl.sort() for subf in fl: subp = appendpath(path, subf) yield subp def ignored_files(self): """Yield list of PATH, IGNORE_PATTERN""" for subp in self.extras(): pat = self.is_ignored(subp) if pat != None: yield subp, pat def get_ignore_list(self): """Return list of ignore patterns. Cached in the Tree object after the first call. """ if hasattr(self, '_ignorelist'): return self._ignorelist l = bzrlib.DEFAULT_IGNORE[:] if self.has_filename(bzrlib.IGNORE_FILENAME): f = self.get_file_byname(bzrlib.IGNORE_FILENAME) l.extend([line.rstrip("\n\r") for line in f.readlines()]) self._ignorelist = l return l def is_ignored(self, filename): """Check whether the filename matches an ignore pattern. Patterns containing '/' need to match the whole path; others match against only the last component. If the file is ignored, returns the pattern which caused it to be ignored, otherwise None. So this can simply be used as a boolean if desired.""" ## TODO: Use '**' to match directories, and other extended globbing stuff from cvs/rsync. for pat in self.get_ignore_list(): if '/' in pat: # as a special case, you can put ./ at the start of a pattern; # this is good to match in the top-level only; if pat[:2] == './': newpat = pat[2:] else: newpat = pat if fnmatch.fnmatchcase(filename, newpat): return pat else: if fnmatch.fnmatchcase(splitpath(filename)[-1], pat): return pat return None class RevisionTree(Tree): """Tree viewing a previous revision. File text can be retrieved from the text store. :todo: Some kind of `__repr__` method, but a good one probably means knowing the branch and revision number, or at least passing a description to the constructor. """ def __init__(self, store, inv): self._store = store self._inventory = inv def get_file(self, file_id): ie = self._inventory[file_id] f = self._store[ie.text_id] mutter(" get fileid{%s} from %r" % (file_id, self)) self._check_retrieved(ie, f) return f def get_file_size(self, file_id): return self._inventory[file_id].text_size def get_file_sha1(self, file_id): ie = self._inventory[file_id] return ie.text_sha1 def has_filename(self, filename): return bool(self.inventory.path2id(filename)) def list_files(self): # The only files returned by this are those from the version for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id class EmptyTree(Tree): def __init__(self): self._inventory = Inventory() def has_filename(self, filename): return False def list_files(self): if False: # just to make it a generator yield None ###################################################################### # diff # TODO: Merge these two functions into a single one that can operate # on either a whole tree or a set of files. # TODO: Return the diff in order by filename, not by category or in # random order. Can probably be done by lock-stepping through the # filenames from both trees. def file_status(filename, old_tree, new_tree): """Return single-letter status, old and new names for a file. The complexity here is in deciding how to represent renames; many complex cases are possible. """ old_inv = old_tree.inventory new_inv = new_tree.inventory new_id = new_inv.path2id(filename) old_id = old_inv.path2id(filename) if not new_id and not old_id: # easy: doesn't exist in either; not versioned at all if new_tree.is_ignored(filename): return 'I', None, None else: return '?', None, None elif new_id: # There is now a file of this name, great. pass else: # There is no longer a file of this name, but we can describe # what happened to the file that used to have # this name. There are two possibilities: either it was # deleted entirely, or renamed. assert old_id if new_inv.has_id(old_id): return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id) else: return 'D', old_inv.id2path(old_id), None # if the file_id is new in this revision, it is added if new_id and not old_inv.has_id(new_id): return 'A' # if there used to be a file of this name, but that ID has now # disappeared, it is deleted if old_id and not new_inv.has_id(old_id): return 'D' return 'wtf?' def find_renames(old_inv, new_inv): for file_id in old_inv: if file_id not in new_inv: continue old_name = old_inv.id2path(file_id) new_name = new_inv.id2path(file_id) if old_name != new_name: yield (old_name, new_name) commit refs/heads/master mark :177 committer 1112754040 +1000 data 78 - better output from test.sh- test.sh exercises cat command (currently broken) from :176 M 644 inline test.sh data 1920 #! /bin/sh -pe # Simple shell-based tests for bzr. # This is meant to exercise the external behaviour, command line # parsing and similar things and compliment the inwardly-turned # testing done by doctest. # This must already exist and be in the right place if ! [ -d bzr-test.tmp ] then echo "please create bzr-test.tmp" exit 1 fi rm -rf bzr-test.tmp mkdir bzr-test.tmp # save it for real errors exec 3>&2 exec > bzr-test.log exec 2>&1 set -x quitter() { echo "tests failed, look in bzr-test.log" >&3; exit 2; } trap quitter ERR cd bzr-test.tmp rm -rf .bzr # some information commands bzr help bzr version # invalid commands are detected ! bzr pants # some experiments with renames bzr init echo "hello world" > test.txt bzr unknowns # should be the only unknown file [ "`bzr unknowns`" = test.txt ] # can't rename unversioned files; use the regular unix rename command ! bzr rename test.txt new-test.txt # ok, so now add it and see what happens bzr add test.txt [ -z "`bzr unknowns`" ] # after adding even before committing you can rename files bzr rename test.txt newname.txt [ "`bzr status`" = "A newname.txt" ] [ `bzr revno` = 0 ] bzr commit -m "add first revision" [ `bzr revno` = 1 ] # now more complicated renames mkdir sub1 ! bzr rename newname.txt sub1 ! bzr rename newname.txt sub1/foo.txt bzr add sub1 ! bzr rename newname.txt sub1 bzr rename newname.txt sub1/foo.txt [ -f sub1/foo.txt ] [ ! -f newname.txt ] bzr rename sub1/foo.txt newname.txt [ -f newname.txt ] bzr rename newname.txt sub1/foo.txt bzr rename sub1/foo.txt sub1/bar.txt cd sub1 mkdir sub2 bzr add sub2 bzr rename bar.txt sub2/bar.txt cd sub2 bzr rename bar.txt ../../bar.txt cd ../../ bzr commit -m "more renames" [ `bzr revno` = 2 ] # now try pulling that file back out, checking it was stored properly [ "`bzr cat -r 1 newname.txt`" = "hello world" ] ! bzr rename sub1 sub1/knotted-up commit refs/heads/master mark :178 committer 1112758715 +1000 data 612 - Use a non-null file_id for the branch root directory. At the moment this is fixed; in the future it should be stored in the directory and perhaps be randomized at each branch init. It is not written out to the inventory at all as yet. - Various branch code cleanups to support this. - If an exception occurs, log traceback into .bzr.log and print a message saying it's there. - New file-id-path command and more help. - Some pychecker fixups. - InventoryEntry constructor parameters now require an entry kind and a parent_id. - Fix up cat command when reading a file from a previous revision. from :177 M 644 inline bzrlib/branch.py data 32712 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f is None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f last_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be bailout('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. :todo: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. :todo: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. :todo: Keep the on-disk branch locked while the object exists. :todo: mkdir() method. """ def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. :param base: Base directory for the branch. :param init: If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. :param find_root: If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch""" return file(self.controlfilename(file_or_path), mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'wb').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'rb').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() inv = Inventory.read_xml(self.controlfile('inventory', 'r')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'w') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. This puts the files in the Added state, so that they will be recorded by the next commit. :todo: Perhaps have an option to add the ids even if the files do not (yet) exist. :todo: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. :todo: Option to specify file id. :todo: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on :todo: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True :todo: Do something useful with directories. :todo: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. :param timestamp: if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) mutter("append to revision-history") f = self.controlfile('revision-history', 'at') f.write(rev_id + '\n') f.close() if verbose: note("commited r%d" % self.revno()) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. :todo: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: bailout("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original'): """Write out human-readable log of commits to this branch :param utc: If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l revno += 1 precursor = p def rename_one(self, from_rel, to_rel): tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(branch, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo :todo: Get state for single files. :todo: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = branch.basis_tree() old_inv = old.inventory new = branch.working_tree() new_inv = new.inventory for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("wierd file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" try: shutil.rmtree(self.base) except OSError: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(12)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(12)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/commands.py data 28841 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] --revision REV Show changes since REV, rather than predecessor. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Diff selected files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): inv = Branch('.').basis_tree().inventory def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(verbose=True): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = cmdfn.__doc__ if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': [], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot prof = hotshot.Profile('.bzr.profile') ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load('.bzr.profile') #stats.strip_dirs() stats.sort_stats('time') stats.print_stats(20) else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') traceback.print_exc(None, bzrlib.trace._tracefile) log_error('see ~/.bzr.log for more information\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error(' see .bzr.log for details\n') traceback.print_exc(None, bzrlib.trace._tracefile) traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') M 644 inline bzrlib/diff.py data 5324 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set from trace import mutter def diff_trees(old_tree, new_tree): """Compute diff between two trees. They may be in different branches and may be working or historical trees. Yields a sequence of (state, id, old_name, new_name, kind). Each filename and each id is listed only once. """ ## TODO: Compare files before diffing; only mention those that have changed ## TODO: Set nice names in the headers, maybe include diffstat ## TODO: Perhaps make this a generator rather than using ## a callback object? ## TODO: Allow specifying a list of files to compare, rather than ## doing the whole tree? (Not urgent.) ## TODO: Allow diffing any two inventories, not just the ## current one against one. We mgiht need to specify two ## stores to look for the files if diffing two branches. That ## might imply this shouldn't be primarily a Branch method. ## XXX: This doesn't report on unknown files; that can be done ## from a separate method. old_it = old_tree.list_files() new_it = new_tree.list_files() def next(it): try: return it.next() except StopIteration: return None old_item = next(old_it) new_item = next(new_it) # We step through the two sorted iterators in parallel, trying to # keep them lined up. while (old_item != None) or (new_item != None): # OK, we still have some remaining on both, but they may be # out of step. if old_item != None: old_name, old_class, old_kind, old_id = old_item else: old_name = None if new_item != None: new_name, new_class, new_kind, new_id = new_item else: new_name = None mutter(" diff pairwise %r" % (old_item,)) mutter(" %r" % (new_item,)) if old_item: # can't handle the old tree being a WorkingTree assert old_class == 'V' if new_item and (new_class != 'V'): yield new_class, None, None, new_name, new_kind new_item = next(new_it) elif (not new_item) or (old_item and (old_name < new_name)): mutter(" extra entry in old-tree sequence") if new_tree.has_id(old_id): # will be mentioned as renamed under new name pass else: yield 'D', old_id, old_name, None, old_kind old_item = next(old_it) elif (not old_item) or (new_item and (new_name < old_name)): mutter(" extra entry in new-tree sequence") if old_tree.has_id(new_id): yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind else: yield 'A', new_id, None, new_name, new_kind new_item = next(new_it) elif old_id != new_id: assert old_name == new_name # both trees have a file of this name, but it is not the # same file. in other words, the old filename has been # overwritten by either a newly-added or a renamed file. # (should we return something about the overwritten file?) if old_tree.has_id(new_id): # renaming, overlying a deleted file yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind else: yield 'A', new_id, None, new_name, new_kind new_item = next(new_it) old_item = next(old_it) else: assert old_id == new_id assert old_id != None assert old_name == new_name assert old_kind == new_kind if old_kind == 'directory': yield '.', new_id, old_name, new_name, new_kind elif old_tree.get_file_size(old_id) != new_tree.get_file_size(old_id): mutter(" file size has changed, must be different") yield 'M', new_id, old_name, new_name, new_kind elif old_tree.get_file_sha1(old_id) == new_tree.get_file_sha1(old_id): mutter(" SHA1 indicates they're identical") ## assert compare_files(old_tree.get_file(i), new_tree.get_file(i)) yield '.', new_id, old_name, new_name, new_kind else: mutter(" quick compare shows different") yield 'M', new_id, old_name, new_name, new_kind new_item = next(new_it) old_item = next(old_it) M 644 inline bzrlib/inventory.py data 19258 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Inventories map files to their name in a revision.""" # TODO: Maybe store inventory_id in the file? Not really needed. __author__ = "Martin Pool " # This should really be an id randomly assigned when the tree is # created, but it's not for now. ROOT_ID = "TREE_ROOT" import sys, os.path, types, re from sets import Set try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from xml import XMLMixin from errors import bailout import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') 'TREE_ROOT' >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID)) >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123')) >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' :todo: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ # TODO: split InventoryEntry into subclasses for files, # directories, etc etc. def __init__(self, file_id, name, kind, parent_id, text_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID) >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID) Traceback (most recent call last): BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", []) """ if len(splitpath(name)) != 1: bailout('InventoryEntry name is not a simple filename: %r' % name) self.file_id = file_id self.name = name assert kind in ['file', 'directory'] self.kind = kind self.text_id = text_id self.parent_id = parent_id self.text_sha1 = None self.text_size = None if kind == 'directory': self.children = {} else: assert kind == 'file' def sorted_children(self): l = self.children.items() l.sort() return l def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.parent_id, text_id=self.text_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size != None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1']: v = getattr(self, f) if v != None: e.set(f, v) # to be conservative, we don't externalize the root pointers # for now, leaving them as null in the xml form. in a future # version it will be implied by nested elements. if self.parent_id != ROOT_ID: assert isinstance(self.parent_id, basestring) e.set('parent_id', self.parent_id) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' ## original format inventories don't have a parent_id for ## nodes in the root directory, but it's cleaner to use one ## internally. parent_id = elt.get('parent_id') if parent_id == None: parent_id = ROOT_ID self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __cmp__(self, other): if self is other: return 0 if not isinstance(other, InventoryEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.name, other.name) \ or cmp(self.text_sha1, other.text_sha1) \ or cmp(self.text_size, other.text_size) \ or cmp(self.text_id, other.text_id) \ or cmp(self.parent_id, other.parent_id) \ or cmp(self.kind, other.kind) class RootEntry(InventoryEntry): def __init__(self, file_id): self.file_id = file_id self.children = {} self.kind = 'root_directory' self.parent_id = None self.name = '' def __cmp__(self, other): if self is other: return 0 if not isinstance(other, RootEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.children, other.children) class Inventory(XMLMixin): """Inventory of versioned files in a tree. An Inventory acts like a set of InventoryEntry items. You can also look files up by their file_id or name. May be read from and written to a metadata file in a tree. To manipulate the inventory (for example to add a file), it is read in, modified, and then written back out. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted, other than through the Inventory API. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID)) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ ## TODO: Make sure only canonical filenames are stored. ## TODO: Do something sensible about the possible collisions on ## case-losing filesystems. Perhaps we should just always forbid ## such collisions. ## TODO: No special cases for root, rather just give it a file id ## like everything else. ## TODO: Probably change XML serialization to use nesting def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. The inventory is created with a default root directory, with an id of None. """ self.root = RootEntry(ROOT_ID) self._byid = {self.root.file_id: self.root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, from_dir=None): """Return (path, entry) pairs, in order by name.""" if from_dir == None: assert self.root from_dir = self.root elif isinstance(from_dir, basestring): from_dir = self._byid[from_dir] kids = from_dir.children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(from_dir=ie.file_id): yield '/'.join((name, cn)), cie def directories(self): """Return (path, entry) pairs for all directories. """ def descend(parent_ie): parent_name = parent_ie.name yield parent_name, parent_ie # directory children in sorted order dn = [] for ie in parent_ie.children.itervalues(): if ie.kind == 'directory': dn.append((ie.name, ie)) dn.sort() for name, child_ie in dn: for sub_name, sub_ie in descend(child_ie): yield appendpath(parent_name, sub_name), sub_ie for name, ie in descend(self.root): yield name, ie def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID)) >>> inv['123123'].name 'hello.c' """ if file_id == None: bailout("can't look up file_id None") try: return self._byid[file_id] except KeyError: bailout("file_id {%s} not in inventory" % file_id) def get_child(self, parent_id, filename): return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self._byid: bailout("inventory already contains entry with id {%s}" % entry.file_id) try: parent = self._byid[entry.parent_id] except KeyError: bailout("parent_id {%s} not in inventory" % entry.parent_id) if parent.children.has_key(entry.name): bailout("%s is already versioned" % appendpath(self.id2path(parent.file_id), entry.name)) self._byid[entry.file_id] = entry parent.children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: bailout("cannot re-add root of inventory") if file_id == None: file_id = bzrlib.branch.gen_file_id(relpath) parent_id = self.path2id(parts[:-1]) assert parent_id != None ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def id_set(self): return Set(self._byid) def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID)) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __cmp__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 True """ if self is other: return 0 if not isinstance(other, Inventory): return NotImplemented if self.id_set() ^ other.id_set(): return 1 for file_id in self._byid: c = cmp(self[file_id], other[file_id]) if c: return c return 0 def get_idpath(self, file_id): """Return a list of file_ids for the path to an entry. The list contains one element for each directory followed by the id of the file itself. So the length of the returned list is equal to the depth of the file in the tree, counting the root directory as depth 1. """ p = [] while file_id != None: try: ie = self._byid[file_id] except KeyError: bailout("file_id {%s} not found in inventory" % file_id) p.insert(0, ie.file_id) file_id = ie.parent_id return p def id2path(self, file_id): """Return as a list the path to file_id.""" # get all names, skipping root p = [self[fid].name for fid in self.get_idpath(file_id)[1:]] return '/'.join(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. Returns None iff the path is not found. """ if isinstance(name, types.StringTypes): name = splitpath(name) mutter("lookup path %r" % name) parent = self.root for f in name: try: cie = parent.children[f] assert cie.name == f assert cie.parent_id == parent.file_id parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): return self._byid.has_key(file_id) def rename(self, file_id, new_parent_id, new_name): """Move a file within the inventory. This can change either the name, or the parent, or both. This does not move the working file.""" if not is_valid_name(new_name): bailout("not an acceptable filename: %r" % new_name) new_parent = self._byid[new_parent_id] if new_name in new_parent.children: bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id))) new_parent_idpath = self.get_idpath(new_parent_id) if file_id in new_parent_idpath: bailout("cannot move directory %r into a subdirectory of itself, %r" % (self.id2path(file_id), self.id2path(new_parent_id))) file_ie = self._byid[file_id] old_parent = self._byid[file_ie.parent_id] # TODO: Don't leave things messed up if this fails del old_parent.children[file_ie.name] new_parent.children[new_name] = file_ie file_ie.name = new_name file_ie.parent_id = new_parent_id _NAME_RE = re.compile(r'^[^/\\]+$') def is_valid_name(name): return bool(_NAME_RE.match(name)) M 644 inline bzrlib/osutils.py data 7419 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, types, re, time, types from stat import S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE from errors import bailout def make_readonly(filename): """Make a filename read-only.""" # TODO: probably needs to be fixed for windows mod = os.stat(filename).st_mode mod = mod & 0777555 os.chmod(filename, mod) def make_writable(filename): mod = os.stat(filename).st_mode mod = mod | 0200 os.chmod(filename, mod) _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])') def quotefn(f): """Return shell-quoted filename""" ## We could be a bit more terse by using double-quotes etc f = _QUOTE_RE.sub(r'\\\1', f) if f[0] == '~': f[0:1] = r'\~' return f def file_kind(f): mode = os.lstat(f)[ST_MODE] if S_ISREG(mode): return 'file' elif S_ISDIR(mode): return 'directory' elif S_ISLNK(mode): return 'symlink' else: bailout("can't handle file kind with mode %o of %r" % (mode, f)) def isdir(f): """True if f is an accessible directory.""" try: return S_ISDIR(os.lstat(f)[ST_MODE]) except OSError: return False def isfile(f): """True if f is a regular file.""" try: return S_ISREG(os.lstat(f)[ST_MODE]) except OSError: return False def pumpfile(fromfile, tofile): """Copy contents of one file to another.""" tofile.write(fromfile.read()) def uuid(): """Return a new UUID""" ## XXX: Could alternatively read /proc/sys/kernel/random/uuid on ## Linux, but we need something portable for other systems; ## preferably an implementation in Python. try: return chomp(file('/proc/sys/kernel/random/uuid').readline()) except IOError: return chomp(os.popen('uuidgen').readline()) def chomp(s): if s and (s[-1] == '\n'): return s[:-1] else: return s def sha_file(f): import sha ## TODO: Maybe read in chunks to handle big files if hasattr(f, 'tell'): assert f.tell() == 0 s = sha.new() s.update(f.read()) return s.hexdigest() def sha_string(f): import sha s = sha.new() s.update(f) return s.hexdigest() def fingerprint_file(f): import sha s = sha.new() b = f.read() s.update(b) size = len(b) return {'size': size, 'sha1': s.hexdigest()} def username(): """Return email-style username. Something similar to 'Martin Pool ' :todo: Check it's reasonably well-formed. :todo: Allow taking it from a dotfile to help people on windows who can't easily set variables. :todo: Cope without pwd module, which is only on unix. """ e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: return e import socket try: import pwd uid = os.getuid() w = pwd.getpwuid(uid) gecos = w.pw_gecos comma = gecos.find(',') if comma == -1: realname = gecos else: realname = gecos[:comma] return '%s <%s@%s>' % (realname, w.pw_name, socket.getfqdn()) except ImportError: pass import getpass, socket return '<%s@%s>' % (getpass.getuser(), socket.getfqdn()) def user_email(): """Return just the email component of a username.""" e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: import re m = re.search(r'[\w+.-]+@[\w+.-]+', e) if not m: bailout('%r is not a reasonable email address' % e) return m.group(0) import getpass, socket return '%s@%s' % (getpass.getuser(), socket.getfqdn()) def compare_files(a, b): """Returns true if equal in contents""" # TODO: don't read the whole thing in one go. BUFSIZE = 4096 while True: ai = a.read(BUFSIZE) bi = b.read(BUFSIZE) if ai != bi: return False if ai == '': return True def local_time_offset(t=None): """Return offset of local zone from GMT, either at present or at time t.""" # python2.3 localtime() can't take None if t is None: t = time.time() if time.localtime(t).tm_isdst and time.daylight: return -time.altzone else: return -time.timezone def format_date(t, offset=0, timezone='original'): ## TODO: Perhaps a global option to use either universal or local time? ## Or perhaps just let people set $TZ? import time assert isinstance(t, float) if timezone == 'utc': tt = time.gmtime(t) offset = 0 elif timezone == 'original': if offset == None: offset = 0 tt = time.gmtime(t + offset) elif timezone == 'local': tt = time.localtime(t) offset = local_time_offset(t) else: bailout("unsupported timezone format %r", ['options are "utc", "original", "local"']) return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt) + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)) def compact_date(when): return time.strftime('%Y%m%d%H%M%S', time.gmtime(when)) def filesize(f): """Return size of given open file.""" return os.fstat(f.fileno())[ST_SIZE] if hasattr(os, 'urandom'): # python 2.4 and later rand_bytes = os.urandom else: # FIXME: No good on non-Linux _rand_file = file('/dev/urandom', 'rb') rand_bytes = _rand_file.read ## TODO: We could later have path objects that remember their list ## decomposition (might be too tricksy though.) def splitpath(p): """Turn string into list of parts. >>> splitpath('a') ['a'] >>> splitpath('a/b') ['a', 'b'] >>> splitpath('a/./b') ['a', 'b'] >>> splitpath('a/.b') ['a', '.b'] >>> splitpath('a/../b') Traceback (most recent call last): ... BzrError: ("sorry, '..' not allowed in path", []) """ assert isinstance(p, types.StringTypes) ps = [f for f in p.split('/') if (f != '.' and f != '')] for f in ps: if f == '..': bailout("sorry, %r not allowed in path" % f) return ps def joinpath(p): assert isinstance(p, list) for f in p: if (f == '..') or (f is None) or (f == ''): bailout("sorry, %r not allowed in path" % f) return '/'.join(p) def appendpath(p1, p2): if p1 == '': return p2 else: return p1 + '/' + p2 def extern_command(cmd, ignore_errors = False): mutter('external command: %s' % `cmd`) if os.system(cmd): if not ignore_errors: bailout('command failed') M 644 inline bzrlib/tree.py data 14242 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tree classes, representing directory at point in time. """ from sets import Set import os.path, os, fnmatch from osutils import pumpfile, compare_files, filesize, quotefn, sha_file, \ joinpath, splitpath, appendpath, isdir, isfile, file_kind, fingerprint_file import errno from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE from inventory import Inventory from trace import mutter, note from errors import bailout import branch import bzrlib class Tree: """Abstract file tree. There are several subclasses: * `WorkingTree` exists as files on disk editable by the user. * `RevisionTree` is a tree as recorded at some point in the past. * `EmptyTree` Trees contain an `Inventory` object, and also know how to retrieve file texts mentioned in the inventory, either from a working directory or from a store. It is possible for trees to contain files that are not described in their inventory or vice versa; for this use `filenames()`. Trees can be compared, etc, regardless of whether they are working trees or versioned trees. """ def has_filename(self, filename): """True if the tree has given filename.""" raise NotImplementedError() def has_id(self, file_id): return self.inventory.has_id(file_id) def id_set(self): """Return set of all ids in this tree.""" return self.inventory.id_set() def id2path(self, file_id): return self.inventory.id2path(file_id) def _get_inventory(self): return self._inventory inventory = property(_get_inventory, doc="Inventory of this Tree") def _check_retrieved(self, ie, f): fp = fingerprint_file(f) f.seek(0) if ie.text_size is not None: if ie.text_size != fp['size']: bailout("mismatched size for file %r in %r" % (ie.file_id, self._store), ["inventory expects %d bytes" % ie.text_size, "file is actually %d bytes" % fp['size'], "store is probably damaged/corrupt"]) if ie.text_sha1 != fp['sha1']: bailout("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store), ["inventory expects %s" % ie.text_sha1, "file is actually %s" % fp['sha1'], "store is probably damaged/corrupt"]) def print_file(self, fileid): """Print file with id `fileid` to stdout.""" import sys pumpfile(self.get_file(fileid), sys.stdout) def export(self, dest): """Export this tree to a new directory. `dest` should not exist, and will be created holding the contents of this tree. :todo: To handle subdirectories we need to create the directories first. :note: If the export fails, the destination directory will be left in a half-assed state. """ os.mkdir(dest) mutter('export version %r' % self) inv = self.inventory for dp, ie in inv.iter_entries(): kind = ie.kind fullpath = appendpath(dest, dp) if kind == 'directory': os.mkdir(fullpath) elif kind == 'file': pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb')) else: bailout("don't know how to export {%s} of kind %r", fid, kind) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) class WorkingTree(Tree): """Working copy tree. The inventory is held in the `Branch` working-inventory, and the files are in a directory on disk. It is possible for a `WorkingTree` to have a filename which is not listed in the Inventory and vice versa. """ def __init__(self, basedir, inv): self._inventory = inv self.basedir = basedir self.path2id = inv.path2id def __repr__(self): return "<%s of %s>" % (self.__class__.__name__, self.basedir) def abspath(self, filename): return os.path.join(self.basedir, filename) def has_filename(self, filename): return os.path.exists(self.abspath(filename)) def get_file(self, file_id): return self.get_file_byname(self.id2path(file_id)) def get_file_byname(self, filename): return file(self.abspath(filename), 'rb') def _get_store_filename(self, file_id): ## XXX: badly named; this isn't in the store at all return self.abspath(self.id2path(file_id)) def has_id(self, file_id): # files that have been deleted are excluded if not self.inventory.has_id(file_id): return False return os.access(self.abspath(self.inventory.id2path(file_id)), os.F_OK) def get_file_size(self, file_id): return os.stat(self._get_store_filename(file_id))[ST_SIZE] def get_file_sha1(self, file_id): f = self.get_file(file_id) return sha_file(f) def file_class(self, filename): if self.path2id(filename): return 'V' elif self.is_ignored(filename): return 'I' else: return '?' def list_files(self): """Recursively list all files as (path, class, kind, id). Lists, but does not descend into unversioned directories. This does not include files that have been deleted in this tree. Skips the control directory. """ inv = self.inventory def descend(from_dir_relpath, from_dir_id, dp): ls = os.listdir(dp) ls.sort() for f in ls: ## TODO: If we find a subdirectory with its own .bzr ## directory, then that is a separate tree and we ## should exclude it. if bzrlib.BZRDIR == f: continue # path within tree fp = appendpath(from_dir_relpath, f) # absolute path fap = appendpath(dp, f) f_ie = inv.get_child(from_dir_id, f) if f_ie: c = 'V' elif self.is_ignored(fp): c = 'I' else: c = '?' fk = file_kind(fap) if f_ie: if f_ie.kind != fk: bailout("file %r entered as kind %r id %r, now of kind %r" % (fap, f_ie.kind, f_ie.file_id, fk)) yield fp, c, fk, (f_ie and f_ie.file_id) if fk != 'directory': continue if c != 'V': # don't descend unversioned directories continue for ff in descend(fp, f_ie.file_id, fap): yield ff for f in descend('', inv.root.file_id, self.basedir): yield f def unknowns(self): for subp in self.extras(): if not self.is_ignored(subp): yield subp def extras(self): """Yield all unknown files in this WorkingTree. If there are any unknown directories then only the directory is returned, not all its children. But if there are unknown files under a versioned subdirectory, they are returned. Currently returned depth-first, sorted by name within directories. """ ## TODO: Work from given directory downwards for path, dir_entry in self.inventory.directories(): mutter("search for unknowns in %r" % path) dirabs = self.abspath(path) if not isdir(dirabs): # e.g. directory deleted continue fl = [] for subf in os.listdir(dirabs): if (subf != '.bzr' and (subf not in dir_entry.children)): fl.append(subf) fl.sort() for subf in fl: subp = appendpath(path, subf) yield subp def ignored_files(self): """Yield list of PATH, IGNORE_PATTERN""" for subp in self.extras(): pat = self.is_ignored(subp) if pat != None: yield subp, pat def get_ignore_list(self): """Return list of ignore patterns. Cached in the Tree object after the first call. """ if hasattr(self, '_ignorelist'): return self._ignorelist l = bzrlib.DEFAULT_IGNORE[:] if self.has_filename(bzrlib.IGNORE_FILENAME): f = self.get_file_byname(bzrlib.IGNORE_FILENAME) l.extend([line.rstrip("\n\r") for line in f.readlines()]) self._ignorelist = l return l def is_ignored(self, filename): """Check whether the filename matches an ignore pattern. Patterns containing '/' need to match the whole path; others match against only the last component. If the file is ignored, returns the pattern which caused it to be ignored, otherwise None. So this can simply be used as a boolean if desired.""" ## TODO: Use '**' to match directories, and other extended globbing stuff from cvs/rsync. for pat in self.get_ignore_list(): if '/' in pat: # as a special case, you can put ./ at the start of a pattern; # this is good to match in the top-level only; if pat[:2] == './': newpat = pat[2:] else: newpat = pat if fnmatch.fnmatchcase(filename, newpat): return pat else: if fnmatch.fnmatchcase(splitpath(filename)[-1], pat): return pat return None class RevisionTree(Tree): """Tree viewing a previous revision. File text can be retrieved from the text store. :todo: Some kind of `__repr__` method, but a good one probably means knowing the branch and revision number, or at least passing a description to the constructor. """ def __init__(self, store, inv): self._store = store self._inventory = inv def get_file(self, file_id): ie = self._inventory[file_id] f = self._store[ie.text_id] mutter(" get fileid{%s} from %r" % (file_id, self)) self._check_retrieved(ie, f) return f def get_file_size(self, file_id): return self._inventory[file_id].text_size def get_file_sha1(self, file_id): ie = self._inventory[file_id] return ie.text_sha1 def has_filename(self, filename): return bool(self.inventory.path2id(filename)) def list_files(self): # The only files returned by this are those from the version for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id class EmptyTree(Tree): def __init__(self): self._inventory = Inventory() def has_filename(self, filename): return False def list_files(self): if False: # just to make it a generator yield None ###################################################################### # diff # TODO: Merge these two functions into a single one that can operate # on either a whole tree or a set of files. # TODO: Return the diff in order by filename, not by category or in # random order. Can probably be done by lock-stepping through the # filenames from both trees. def file_status(filename, old_tree, new_tree): """Return single-letter status, old and new names for a file. The complexity here is in deciding how to represent renames; many complex cases are possible. """ old_inv = old_tree.inventory new_inv = new_tree.inventory new_id = new_inv.path2id(filename) old_id = old_inv.path2id(filename) if not new_id and not old_id: # easy: doesn't exist in either; not versioned at all if new_tree.is_ignored(filename): return 'I', None, None else: return '?', None, None elif new_id: # There is now a file of this name, great. pass else: # There is no longer a file of this name, but we can describe # what happened to the file that used to have # this name. There are two possibilities: either it was # deleted entirely, or renamed. assert old_id if new_inv.has_id(old_id): return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id) else: return 'D', old_inv.id2path(old_id), None # if the file_id is new in this revision, it is added if new_id and not old_inv.has_id(new_id): return 'A' # if there used to be a file of this name, but that ID has now # disappeared, it is deleted if old_id and not new_inv.has_id(old_id): return 'D' return 'wtf?' def find_renames(old_inv, new_inv): for file_id in old_inv: if file_id not in new_inv: continue old_name = old_inv.id2path(file_id) new_name = new_inv.id2path(file_id) if old_name != new_name: yield (old_name, new_name) commit refs/heads/master mark :179 committer 1112758980 +1000 data 23 better message on error from :178 M 644 inline bzrlib/commands.py data 28865 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib from os import path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_find_filename(fileid): n = find_filename(fileid) if n is None: bailout("%s is not a live file id" % fileid) else: print n def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] --revision REV Show changes since REV, rather than predecessor. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Diff selected files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() old_inv = old_tree.inventory new_inv = new_tree.inventory # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): d = None # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): inv = Branch('.').basis_tree().inventory def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(verbose=True): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): import time print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(verbose=False): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = cmdfn.__doc__ if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': [], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': assert 0 elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot prof = hotshot.Profile('.bzr.profile') ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load('.bzr.profile') #stats.strip_dirs() stats.sort_stats('time') stats.print_stats(20) else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') traceback.print_exc(None, bzrlib.trace._tracefile) log_error('(see $HOME/.bzr.log for debug information)\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error('(see $HOME/.bzr.log for debug information)\n') traceback.print_exc(None, bzrlib.trace._tracefile) ## traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :180 committer 1112759156 +1000 data 52 - mv command is gone, but renames seem to be working from :179 M 644 inline NEWS data 2441 bzr-0.0.3 NOT RELEASED YET ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2003-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. commit refs/heads/master mark :181 committer 1112759725 +1000 data 25 prepare for 0.0.3 release from :180 M 644 inline NEWS data 2435 bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/__init__.py data 1485 # (C) 2005 Canonical Development Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr library""" from inventory import Inventory, InventoryEntry from branch import Branch, ScratchBranch from osutils import format_date from tree import Tree from diff import diff_trees from trace import mutter, warning import add BZRDIR = ".bzr" DEFAULT_IGNORE = ['.bzr.log', '*~', '#*#', '*$', '.#*', '*.tmp', '*.bak', '*.BAK', '*.orig', '*.o', '*.obj', '*.a', '*.py[oc]', '*.so', '*.exe', '*.elc', '{arch}', 'CVS', '.svn', '_darcs', 'SCCS', 'RCS', 'TAGS', '.make.state', '.sconsign'] IGNORE_FILENAME = ".bzrignore" __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __version__ = '0.0.3' commit refs/heads/master mark :182 committer 1112763729 +1000 data 18 bzr 0.0.3 release! from :181 commit refs/heads/master mark :183 committer 1112790075 +1000 data 16 pychecker fixups from :182 M 644 inline bzrlib/osutils.py data 7446 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, types, re, time from stat import S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE from errors import bailout, BzrError from trace import mutter def make_readonly(filename): """Make a filename read-only.""" # TODO: probably needs to be fixed for windows mod = os.stat(filename).st_mode mod = mod & 0777555 os.chmod(filename, mod) def make_writable(filename): mod = os.stat(filename).st_mode mod = mod | 0200 os.chmod(filename, mod) _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])') def quotefn(f): """Return shell-quoted filename""" ## We could be a bit more terse by using double-quotes etc f = _QUOTE_RE.sub(r'\\\1', f) if f[0] == '~': f[0:1] = r'\~' return f def file_kind(f): mode = os.lstat(f)[ST_MODE] if S_ISREG(mode): return 'file' elif S_ISDIR(mode): return 'directory' elif S_ISLNK(mode): return 'symlink' else: raise BzrError("can't handle file kind with mode %o of %r" % (mode, f)) def isdir(f): """True if f is an accessible directory.""" try: return S_ISDIR(os.lstat(f)[ST_MODE]) except OSError: return False def isfile(f): """True if f is a regular file.""" try: return S_ISREG(os.lstat(f)[ST_MODE]) except OSError: return False def pumpfile(fromfile, tofile): """Copy contents of one file to another.""" tofile.write(fromfile.read()) def uuid(): """Return a new UUID""" ## XXX: Could alternatively read /proc/sys/kernel/random/uuid on ## Linux, but we need something portable for other systems; ## preferably an implementation in Python. try: return chomp(file('/proc/sys/kernel/random/uuid').readline()) except IOError: return chomp(os.popen('uuidgen').readline()) def chomp(s): if s and (s[-1] == '\n'): return s[:-1] else: return s def sha_file(f): import sha ## TODO: Maybe read in chunks to handle big files if hasattr(f, 'tell'): assert f.tell() == 0 s = sha.new() s.update(f.read()) return s.hexdigest() def sha_string(f): import sha s = sha.new() s.update(f) return s.hexdigest() def fingerprint_file(f): import sha s = sha.new() b = f.read() s.update(b) size = len(b) return {'size': size, 'sha1': s.hexdigest()} def username(): """Return email-style username. Something similar to 'Martin Pool ' :todo: Check it's reasonably well-formed. :todo: Allow taking it from a dotfile to help people on windows who can't easily set variables. :todo: Cope without pwd module, which is only on unix. """ e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: return e import socket try: import pwd uid = os.getuid() w = pwd.getpwuid(uid) gecos = w.pw_gecos comma = gecos.find(',') if comma == -1: realname = gecos else: realname = gecos[:comma] return '%s <%s@%s>' % (realname, w.pw_name, socket.getfqdn()) except ImportError: pass import getpass, socket return '<%s@%s>' % (getpass.getuser(), socket.getfqdn()) _EMAIL_RE = re.compile(r'[\w+.-]+@[\w+.-]+') def user_email(): """Return just the email component of a username.""" e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: m = _EMAIL_RE.search(e) if not m: bailout('%r is not a reasonable email address' % e) return m.group(0) import getpass, socket return '%s@%s' % (getpass.getuser(), socket.getfqdn()) def compare_files(a, b): """Returns true if equal in contents""" # TODO: don't read the whole thing in one go. BUFSIZE = 4096 while True: ai = a.read(BUFSIZE) bi = b.read(BUFSIZE) if ai != bi: return False if ai == '': return True def local_time_offset(t=None): """Return offset of local zone from GMT, either at present or at time t.""" # python2.3 localtime() can't take None if t == None: t = time.time() if time.localtime(t).tm_isdst and time.daylight: return -time.altzone else: return -time.timezone def format_date(t, offset=0, timezone='original'): ## TODO: Perhaps a global option to use either universal or local time? ## Or perhaps just let people set $TZ? assert isinstance(t, float) if timezone == 'utc': tt = time.gmtime(t) offset = 0 elif timezone == 'original': if offset == None: offset = 0 tt = time.gmtime(t + offset) elif timezone == 'local': tt = time.localtime(t) offset = local_time_offset(t) else: bailout("unsupported timezone format %r", ['options are "utc", "original", "local"']) return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt) + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)) def compact_date(when): return time.strftime('%Y%m%d%H%M%S', time.gmtime(when)) def filesize(f): """Return size of given open file.""" return os.fstat(f.fileno())[ST_SIZE] if hasattr(os, 'urandom'): # python 2.4 and later rand_bytes = os.urandom else: # FIXME: No good on non-Linux _rand_file = file('/dev/urandom', 'rb') rand_bytes = _rand_file.read ## TODO: We could later have path objects that remember their list ## decomposition (might be too tricksy though.) def splitpath(p): """Turn string into list of parts. >>> splitpath('a') ['a'] >>> splitpath('a/b') ['a', 'b'] >>> splitpath('a/./b') ['a', 'b'] >>> splitpath('a/.b') ['a', '.b'] >>> splitpath('a/../b') Traeceback (most recent call last): ... BzrError: ("sorry, '..' not allowed in path", []) """ assert isinstance(p, types.StringTypes) ps = [f for f in p.split('/') if (f != '.' and f != '')] for f in ps: if f == '..': bailout("sorry, %r not allowed in path" % f) return ps def joinpath(p): assert isinstance(p, list) for f in p: if (f == '..') or (f == None) or (f == ''): bailout("sorry, %r not allowed in path" % f) return '/'.join(p) def appendpath(p1, p2): if p1 == '': return p2 else: return p1 + '/' + p2 def extern_command(cmd, ignore_errors = False): mutter('external command: %s' % `cmd`) if os.system(cmd): if not ignore_errors: bailout('command failed') commit refs/heads/master mark :184 committer 1112796392 +1000 data 16 pychecker fixups from :183 M 644 inline NEWS data 2467 bzr-0.0.4 NOT RELEASED YET bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/__init__.py data 1488 # (C) 2005 Canonical Development Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr library""" from inventory import Inventory, InventoryEntry from branch import Branch, ScratchBranch from osutils import format_date from tree import Tree from diff import diff_trees from trace import mutter, warning import add BZRDIR = ".bzr" DEFAULT_IGNORE = ['.bzr.log', '*~', '#*#', '*$', '.#*', '*.tmp', '*.bak', '*.BAK', '*.orig', '*.o', '*.obj', '*.a', '*.py[oc]', '*.so', '*.exe', '*.elc', '{arch}', 'CVS', '.svn', '_darcs', 'SCCS', 'RCS', 'TAGS', '.make.state', '.sconsign'] IGNORE_FILENAME = ".bzrignore" __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __version__ = '0.0.4pre' M 644 inline bzrlib/branch.py data 32697 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. :todo: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. :todo: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. :todo: Keep the on-disk branch locked while the object exists. :todo: mkdir() method. """ def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. :param base: Base directory for the branch. :param init: If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. :param find_root: If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch""" return file(self.controlfilename(file_or_path), mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'wb').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'rb').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() inv = Inventory.read_xml(self.controlfile('inventory', 'r')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'w') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. This puts the files in the Added state, so that they will be recorded by the next commit. :todo: Perhaps have an option to add the ids even if the files do not (yet) exist. :todo: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. :todo: Option to specify file id. :todo: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on :todo: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True :todo: Do something useful with directories. :todo: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. :param timestamp: if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) mutter("append to revision-history") f = self.controlfile('revision-history', 'at') f.write(rev_id + '\n') f.close() if verbose: note("commited r%d" % self.revno()) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. :todo: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] else: return None def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original'): """Write out human-readable log of commits to this branch :param utc: If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l revno += 1 precursor = p def rename_one(self, from_rel, to_rel): tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(self, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo :todo: Get state for single files. :todo: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = self.basis_tree() new = self.working_tree() for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("wierd file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" try: shutil.rmtree(self.base) except OSError: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(12)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(12)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/check.py data 4116 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ###################################################################### # consistency checks import sys from sets import Set from trace import mutter from errors import bailout import osutils def check(branch, progress=True): out = sys.stdout if progress: def p(m): mutter('checking ' + m) out.write('\rchecking: %-50.50s' % m) out.flush() else: def p(m): mutter('checking ' + m) p('history of %r' % branch.base) last_ptr = None checked_revs = Set() history = branch.revision_history() revno = 0 revcount = len(history) checked_texts = {} for rid in history: revno += 1 p('revision %d/%d' % (revno, revcount)) mutter(' revision {%s}' % rid) rev = branch.get_revision(rid) if rev.revision_id != rid: bailout('wrong internal revision id in revision {%s}' % rid) if rev.precursor != last_ptr: bailout('mismatched precursor in revision {%s}' % rid) last_ptr = rid if rid in checked_revs: bailout('repeated revision {%s}' % rid) checked_revs.add(rid) ## TODO: Check all the required fields are present on the revision. inv = branch.get_inventory(rev.inventory_id) seen_ids = Set() seen_names = Set() p('revision %d/%d file ids' % (revno, revcount)) for file_id in inv: if file_id in seen_ids: bailout('duplicated file_id {%s} in inventory for revision {%s}' % (file_id, rid)) seen_ids.add(file_id) i = 0 len_inv = len(inv) for file_id in inv: i += 1 if (i % 100) == 0: p('revision %d/%d file text %d/%d' % (revno, revcount, i, len_inv)) ie = inv[file_id] if ie.parent_id != None: if ie.parent_id not in seen_ids: bailout('missing parent {%s} in inventory for revision {%s}' % (ie.parent_id, rid)) if ie.kind == 'file': if ie.text_id in checked_texts: fp = checked_texts[ie.text_id] else: if not ie.text_id in branch.text_store: bailout('text {%s} not in text_store' % ie.text_id) tf = branch.text_store[ie.text_id] fp = osutils.fingerprint_file(tf) checked_texts[ie.text_id] = fp if ie.text_size != fp['size']: bailout('text {%s} wrong size' % ie.text_id) if ie.text_sha1 != fp['sha1']: bailout('text {%s} wrong sha1' % ie.text_id) elif ie.kind == 'directory': if ie.text_sha1 != None or ie.text_size != None or ie.text_id != None: bailout('directory {%s} has text in revision {%s}' % (file_id, rid)) p('revision %d/%d file paths' % (revno, revcount)) for path, ie in inv.iter_entries(): if path in seen_names: bailout('duplicated path %r in inventory for revision {%s}' % (path, revid)) seen_names.add(path) p('done') if progress: print M 644 inline bzrlib/commands.py data 28657 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, time, types, shutil, tempfile, traceback, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] --revision REV Show changes since REV, rather than predecessor. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Diff selected files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = cmdfn.__doc__ if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': [], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': raise BzrError("arg form %r not implemented yet" % ap) elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot prof = hotshot.Profile('.bzr.profile') ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load('.bzr.profile') #stats.strip_dirs() stats.sort_stats('time') stats.print_stats(20) return ret else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') traceback.print_exc(None, bzrlib.trace._tracefile) log_error('(see $HOME/.bzr.log for debug information)\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error('(see $HOME/.bzr.log for debug information)\n') traceback.print_exc(None, bzrlib.trace._tracefile) ## traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') M 644 inline bzrlib/info.py data 3300 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import time import bzrlib from osutils import format_date def show_info(b): # TODO: Maybe show space used by working tree, versioned files, # unknown files, text store. print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl == None: return pl else: return 's' count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %8d %s' % (count_status[fs], name) print ' %8d versioned subdirector%s' % (count_version_dirs, plural(count_version_dirs, 'y', 'ies')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %8d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %8d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %8d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) print print 'text store:' c, t = b.text_store.total_size() print ' %8d file texts' % c print ' %8d kB' % (t/1024) print print 'revision store:' c, t = b.revision_store.total_size() print ' %8d revisions' % c print ' %8d kB' % (t/1024) print print 'inventory store:' c, t = b.inventory_store.total_size() print ' %8d inventories' % c print ' %8d kB' % (t/1024) M 644 inline bzrlib/inventory.py data 19282 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Inventories map files to their name in a revision.""" # TODO: Maybe store inventory_id in the file? Not really needed. __author__ = "Martin Pool " # This should really be an id randomly assigned when the tree is # created, but it's not for now. ROOT_ID = "TREE_ROOT" import sys, os.path, types, re from sets import Set try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from xml import XMLMixin from errors import bailout, BzrError import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') 'TREE_ROOT' >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID)) >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123')) >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' :todo: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ # TODO: split InventoryEntry into subclasses for files, # directories, etc etc. def __init__(self, file_id, name, kind, parent_id, text_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID) >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID) Traceback (most recent call last): BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", []) """ if len(splitpath(name)) != 1: bailout('InventoryEntry name is not a simple filename: %r' % name) self.file_id = file_id self.name = name assert kind in ['file', 'directory'] self.kind = kind self.text_id = text_id self.parent_id = parent_id self.text_sha1 = None self.text_size = None if kind == 'directory': self.children = {} else: assert kind == 'file' def sorted_children(self): l = self.children.items() l.sort() return l def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.parent_id, text_id=self.text_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size != None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1']: v = getattr(self, f) if v != None: e.set(f, v) # to be conservative, we don't externalize the root pointers # for now, leaving them as null in the xml form. in a future # version it will be implied by nested elements. if self.parent_id != ROOT_ID: assert isinstance(self.parent_id, basestring) e.set('parent_id', self.parent_id) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' ## original format inventories don't have a parent_id for ## nodes in the root directory, but it's cleaner to use one ## internally. parent_id = elt.get('parent_id') if parent_id == None: parent_id = ROOT_ID self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __cmp__(self, other): if self is other: return 0 if not isinstance(other, InventoryEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.name, other.name) \ or cmp(self.text_sha1, other.text_sha1) \ or cmp(self.text_size, other.text_size) \ or cmp(self.text_id, other.text_id) \ or cmp(self.parent_id, other.parent_id) \ or cmp(self.kind, other.kind) class RootEntry(InventoryEntry): def __init__(self, file_id): self.file_id = file_id self.children = {} self.kind = 'root_directory' self.parent_id = None self.name = '' def __cmp__(self, other): if self is other: return 0 if not isinstance(other, RootEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.children, other.children) class Inventory(XMLMixin): """Inventory of versioned files in a tree. An Inventory acts like a set of InventoryEntry items. You can also look files up by their file_id or name. May be read from and written to a metadata file in a tree. To manipulate the inventory (for example to add a file), it is read in, modified, and then written back out. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted, other than through the Inventory API. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID)) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ ## TODO: Make sure only canonical filenames are stored. ## TODO: Do something sensible about the possible collisions on ## case-losing filesystems. Perhaps we should just always forbid ## such collisions. ## TODO: No special cases for root, rather just give it a file id ## like everything else. ## TODO: Probably change XML serialization to use nesting def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. The inventory is created with a default root directory, with an id of None. """ self.root = RootEntry(ROOT_ID) self._byid = {self.root.file_id: self.root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, from_dir=None): """Return (path, entry) pairs, in order by name.""" if from_dir == None: assert self.root from_dir = self.root elif isinstance(from_dir, basestring): from_dir = self._byid[from_dir] kids = from_dir.children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(from_dir=ie.file_id): yield '/'.join((name, cn)), cie def directories(self): """Return (path, entry) pairs for all directories. """ def descend(parent_ie): parent_name = parent_ie.name yield parent_name, parent_ie # directory children in sorted order dn = [] for ie in parent_ie.children.itervalues(): if ie.kind == 'directory': dn.append((ie.name, ie)) dn.sort() for name, child_ie in dn: for sub_name, sub_ie in descend(child_ie): yield appendpath(parent_name, sub_name), sub_ie for name, ie in descend(self.root): yield name, ie def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID)) >>> inv['123123'].name 'hello.c' """ if file_id == None: raise BzrError("can't look up file_id None") try: return self._byid[file_id] except KeyError: raise BzrError("file_id {%s} not in inventory" % file_id) def get_child(self, parent_id, filename): return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self._byid: bailout("inventory already contains entry with id {%s}" % entry.file_id) try: parent = self._byid[entry.parent_id] except KeyError: bailout("parent_id {%s} not in inventory" % entry.parent_id) if parent.children.has_key(entry.name): bailout("%s is already versioned" % appendpath(self.id2path(parent.file_id), entry.name)) self._byid[entry.file_id] = entry parent.children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: bailout("cannot re-add root of inventory") if file_id == None: file_id = bzrlib.branch.gen_file_id(relpath) parent_id = self.path2id(parts[:-1]) assert parent_id != None ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def id_set(self): return Set(self._byid) def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID)) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __cmp__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 True """ if self is other: return 0 if not isinstance(other, Inventory): return NotImplemented if self.id_set() ^ other.id_set(): return 1 for file_id in self._byid: c = cmp(self[file_id], other[file_id]) if c: return c return 0 def get_idpath(self, file_id): """Return a list of file_ids for the path to an entry. The list contains one element for each directory followed by the id of the file itself. So the length of the returned list is equal to the depth of the file in the tree, counting the root directory as depth 1. """ p = [] while file_id != None: try: ie = self._byid[file_id] except KeyError: bailout("file_id {%s} not found in inventory" % file_id) p.insert(0, ie.file_id) file_id = ie.parent_id return p def id2path(self, file_id): """Return as a list the path to file_id.""" # get all names, skipping root p = [self[fid].name for fid in self.get_idpath(file_id)[1:]] return '/'.join(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. Returns None iff the path is not found. """ if isinstance(name, types.StringTypes): name = splitpath(name) mutter("lookup path %r" % name) parent = self.root for f in name: try: cie = parent.children[f] assert cie.name == f assert cie.parent_id == parent.file_id parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): return self._byid.has_key(file_id) def rename(self, file_id, new_parent_id, new_name): """Move a file within the inventory. This can change either the name, or the parent, or both. This does not move the working file.""" if not is_valid_name(new_name): bailout("not an acceptable filename: %r" % new_name) new_parent = self._byid[new_parent_id] if new_name in new_parent.children: bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id))) new_parent_idpath = self.get_idpath(new_parent_id) if file_id in new_parent_idpath: bailout("cannot move directory %r into a subdirectory of itself, %r" % (self.id2path(file_id), self.id2path(new_parent_id))) file_ie = self._byid[file_id] old_parent = self._byid[file_ie.parent_id] # TODO: Don't leave things messed up if this fails del old_parent.children[file_ie.name] new_parent.children[new_name] = file_ie file_ie.name = new_name file_ie.parent_id = new_parent_id _NAME_RE = re.compile(r'^[^/\\]+$') def is_valid_name(name): return bool(_NAME_RE.match(name)) M 644 inline bzrlib/osutils.py data 7445 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, types, re, time from stat import S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE from errors import bailout, BzrError from trace import mutter def make_readonly(filename): """Make a filename read-only.""" # TODO: probably needs to be fixed for windows mod = os.stat(filename).st_mode mod = mod & 0777555 os.chmod(filename, mod) def make_writable(filename): mod = os.stat(filename).st_mode mod = mod | 0200 os.chmod(filename, mod) _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])') def quotefn(f): """Return shell-quoted filename""" ## We could be a bit more terse by using double-quotes etc f = _QUOTE_RE.sub(r'\\\1', f) if f[0] == '~': f[0:1] = r'\~' return f def file_kind(f): mode = os.lstat(f)[ST_MODE] if S_ISREG(mode): return 'file' elif S_ISDIR(mode): return 'directory' elif S_ISLNK(mode): return 'symlink' else: raise BzrError("can't handle file kind with mode %o of %r" % (mode, f)) def isdir(f): """True if f is an accessible directory.""" try: return S_ISDIR(os.lstat(f)[ST_MODE]) except OSError: return False def isfile(f): """True if f is a regular file.""" try: return S_ISREG(os.lstat(f)[ST_MODE]) except OSError: return False def pumpfile(fromfile, tofile): """Copy contents of one file to another.""" tofile.write(fromfile.read()) def uuid(): """Return a new UUID""" ## XXX: Could alternatively read /proc/sys/kernel/random/uuid on ## Linux, but we need something portable for other systems; ## preferably an implementation in Python. try: return chomp(file('/proc/sys/kernel/random/uuid').readline()) except IOError: return chomp(os.popen('uuidgen').readline()) def chomp(s): if s and (s[-1] == '\n'): return s[:-1] else: return s def sha_file(f): import sha ## TODO: Maybe read in chunks to handle big files if hasattr(f, 'tell'): assert f.tell() == 0 s = sha.new() s.update(f.read()) return s.hexdigest() def sha_string(f): import sha s = sha.new() s.update(f) return s.hexdigest() def fingerprint_file(f): import sha s = sha.new() b = f.read() s.update(b) size = len(b) return {'size': size, 'sha1': s.hexdigest()} def username(): """Return email-style username. Something similar to 'Martin Pool ' :todo: Check it's reasonably well-formed. :todo: Allow taking it from a dotfile to help people on windows who can't easily set variables. :todo: Cope without pwd module, which is only on unix. """ e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: return e import socket try: import pwd uid = os.getuid() w = pwd.getpwuid(uid) gecos = w.pw_gecos comma = gecos.find(',') if comma == -1: realname = gecos else: realname = gecos[:comma] return '%s <%s@%s>' % (realname, w.pw_name, socket.getfqdn()) except ImportError: pass import getpass, socket return '<%s@%s>' % (getpass.getuser(), socket.getfqdn()) _EMAIL_RE = re.compile(r'[\w+.-]+@[\w+.-]+') def user_email(): """Return just the email component of a username.""" e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: m = _EMAIL_RE.search(e) if not m: bailout('%r is not a reasonable email address' % e) return m.group(0) import getpass, socket return '%s@%s' % (getpass.getuser(), socket.getfqdn()) def compare_files(a, b): """Returns true if equal in contents""" # TODO: don't read the whole thing in one go. BUFSIZE = 4096 while True: ai = a.read(BUFSIZE) bi = b.read(BUFSIZE) if ai != bi: return False if ai == '': return True def local_time_offset(t=None): """Return offset of local zone from GMT, either at present or at time t.""" # python2.3 localtime() can't take None if t == None: t = time.time() if time.localtime(t).tm_isdst and time.daylight: return -time.altzone else: return -time.timezone def format_date(t, offset=0, timezone='original'): ## TODO: Perhaps a global option to use either universal or local time? ## Or perhaps just let people set $TZ? assert isinstance(t, float) if timezone == 'utc': tt = time.gmtime(t) offset = 0 elif timezone == 'original': if offset == None: offset = 0 tt = time.gmtime(t + offset) elif timezone == 'local': tt = time.localtime(t) offset = local_time_offset(t) else: bailout("unsupported timezone format %r", ['options are "utc", "original", "local"']) return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt) + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)) def compact_date(when): return time.strftime('%Y%m%d%H%M%S', time.gmtime(when)) def filesize(f): """Return size of given open file.""" return os.fstat(f.fileno())[ST_SIZE] if hasattr(os, 'urandom'): # python 2.4 and later rand_bytes = os.urandom else: # FIXME: No good on non-Linux _rand_file = file('/dev/urandom', 'rb') rand_bytes = _rand_file.read ## TODO: We could later have path objects that remember their list ## decomposition (might be too tricksy though.) def splitpath(p): """Turn string into list of parts. >>> splitpath('a') ['a'] >>> splitpath('a/b') ['a', 'b'] >>> splitpath('a/./b') ['a', 'b'] >>> splitpath('a/.b') ['a', '.b'] >>> splitpath('a/../b') Traceback (most recent call last): ... BzrError: ("sorry, '..' not allowed in path", []) """ assert isinstance(p, types.StringTypes) ps = [f for f in p.split('/') if (f != '.' and f != '')] for f in ps: if f == '..': bailout("sorry, %r not allowed in path" % f) return ps def joinpath(p): assert isinstance(p, list) for f in p: if (f == '..') or (f == None) or (f == ''): bailout("sorry, %r not allowed in path" % f) return '/'.join(p) def appendpath(p1, p2): if p1 == '': return p2 else: return p1 + '/' + p2 def extern_command(cmd, ignore_errors = False): mutter('external command: %s' % `cmd`) if os.system(cmd): if not ignore_errors: bailout('command failed') M 644 inline bzrlib/revision.py data 2867 # (C) 2005 Canonical # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from xml import XMLMixin try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from errors import BzrError class Revision(XMLMixin): """Single revision on a branch. Revisions may know their revision_hash, but only once they've been written out. This is not stored because you cannot write the hash into the file it describes. :todo: Perhaps make predecessor be a child element, not an attribute? """ def __init__(self, **args): self.inventory_id = None self.revision_id = None self.timestamp = None self.message = None self.timezone = None self.committer = None self.precursor = None self.__dict__.update(args) def __repr__(self): return "" % self.revision_id def to_element(self): root = Element('revision', committer = self.committer, timestamp = '%.9f' % self.timestamp, revision_id = self.revision_id, inventory_id = self.inventory_id, timezone = str(self.timezone)) if self.precursor: root.set('precursor', self.precursor) root.text = '\n' msg = SubElement(root, 'message') msg.text = self.message msg.tail = '\n' return root def from_element(cls, elt): # is deprecated... if elt.tag not in ('revision', 'changeset'): raise BzrError("unexpected tag in revision file: %r" % elt) cs = cls(committer = elt.get('committer'), timestamp = float(elt.get('timestamp')), precursor = elt.get('precursor'), revision_id = elt.get('revision_id'), inventory_id = elt.get('inventory_id')) v = elt.get('timezone') cs.timezone = v and int(v) cs.message = elt.findtext('message') # text of return cs from_element = classmethod(from_element) M 644 inline bzrlib/store.py data 5365 # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Stores are the main data-storage mechanism for Bazaar-NG. A store is a simple write-once container indexed by a universally unique ID, which is typically the SHA-1 of the content.""" __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import os, tempfile, types, osutils, gzip, errno from stat import ST_SIZE from StringIO import StringIO from trace import mutter ###################################################################### # stores class StoreError(Exception): pass class ImmutableStore: """Store that holds files indexed by unique names. Files can be added, but not modified once they are in. Typically the hash is used as the name, or something else known to be unique, such as a UUID. >>> st = ImmutableScratchStore() >>> st.add(StringIO('hello'), 'aa') >>> 'aa' in st True >>> 'foo' in st False You are not allowed to add an id that is already present. Entries can be retrieved as files, which may then be read. >>> st.add(StringIO('goodbye'), '123123') >>> st['123123'].read() 'goodbye' :todo: Atomic add by writing to a temporary file and renaming. :todo: Perhaps automatically transform to/from XML in a method? Would just need to tell the constructor what class to use... :todo: Even within a simple disk store like this, we could gzip the files. But since many are less than one disk block, that might not help a lot. """ def __init__(self, basedir): """ImmutableStore constructor.""" self._basedir = basedir def _path(self, id): return os.path.join(self._basedir, id) def __repr__(self): return "%s(%r)" % (self.__class__.__name__, self._basedir) def add(self, f, fileid, compressed=True): """Add contents of a file into the store. :param f: An open file, or file-like object.""" # FIXME: Only works on smallish files # TODO: Can be optimized by copying at the same time as # computing the sum. mutter("add store entry %r" % (fileid)) if isinstance(f, types.StringTypes): content = f else: content = f.read() p = self._path(fileid) if os.access(p, os.F_OK) or os.access(p + '.gz', os.F_OK): bailout("store %r already contains id %r" % (self._basedir, fileid)) if compressed: f = gzip.GzipFile(p + '.gz', 'wb') os.chmod(p + '.gz', 0444) else: f = file(p, 'wb') os.chmod(p, 0444) f.write(content) f.close() def __contains__(self, fileid): """""" p = self._path(fileid) return (os.access(p, os.R_OK) or os.access(p + '.gz', os.R_OK)) # TODO: Guard against the same thing being stored twice, compressed and uncompresse def __iter__(self): for f in os.listdir(self._basedir): if f[-3:] == '.gz': # TODO: case-insensitive? yield f[:-3] else: yield f def __len__(self): return len(os.listdir(self._basedir)) def __getitem__(self, fileid): """Returns a file reading from a particular entry.""" p = self._path(fileid) try: return gzip.GzipFile(p + '.gz', 'rb') except IOError, e: if e.errno == errno.ENOENT: return file(p, 'rb') else: raise e def total_size(self): """Return (count, bytes) This is the (compressed) size stored on disk, not the size of the content.""" total = 0 count = 0 for fid in self: count += 1 p = self._path(fid) try: total += os.stat(p)[ST_SIZE] except OSError: total += os.stat(p + '.gz')[ST_SIZE] return count, total class ImmutableScratchStore(ImmutableStore): """Self-destructing test subclass of ImmutableStore. The Store only exists for the lifetime of the Python object. Obviously you should not put anything precious in it. """ def __init__(self): ImmutableStore.__init__(self, tempfile.mkdtemp()) def __del__(self): for f in os.listdir(self._basedir): fpath = os.path.join(self._basedir, f) # needed on windows, and maybe some other filesystems os.chmod(fpath, 0600) os.remove(fpath) os.rmdir(self._basedir) mutter("%r destroyed" % self) M 644 inline bzrlib/trace.py data 3706 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os, time, socket, stat, codecs import bzrlib ###################################################################### # messages and logging ## TODO: If --verbose is given then write to both stderr and ## _tracefile; perhaps replace _tracefile with a tee thing. global _tracefile, _starttime _tracefile = None # used to have % (os.environ['USER'], time.time(), os.getpid()), 'w') _starttime = None # If false, notes also go to stdout; should replace this with --silent # at some point. silent = False # TODO: Somehow tie this to the --verbose option? verbose = False def warning(msg): b = 'bzr: warning: ' + msg + '\n' sys.stderr.write(b) _tracefile.write(b) #_tracefile.flush() def mutter(msg): _tracefile.write(msg) _tracefile.write('\n') # _tracefile.flush() if verbose: sys.stderr.write('- ' + msg + '\n') def note(msg): b = '* ' + str(msg) + '\n' if not silent: sys.stderr.write(b) _tracefile.write(b) # _tracefile.flush() def log_error(msg): sys.stderr.write(msg) _tracefile.write(msg) # _tracefile.flush() def create_tracefile(argv): # TODO: Also show contents of /etc/lsb-release, if it can be parsed. # Perhaps that should eventually go into the platform library? # TODO: If the file doesn't exist, add a note describing it. # Messages are always written to here, so that we have some # information if something goes wrong. In a future version this # file will be removed on successful completion. global _starttime, _tracefile _starttime = os.times()[4] # XXX: Is HOME always set on Windows? trace_fname = os.path.join(os.environ['HOME'], '.bzr.log') _tracefile = codecs.open(trace_fname, 'at', 'utf8') t = _tracefile if os.fstat(t.fileno())[stat.ST_SIZE] == 0: t.write("\nthis is a debug log for diagnosing/reporting problems in bzr\n") t.write("you can delete or truncate this file, or include sections in\n") t.write("bug reports to bazaar-ng@lists.canonical.com\n\n") # TODO: If we failed to create the file, perhaps give a warning # but don't abort; send things to /dev/null instead? t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % bzrlib.osutils.format_date(time.time())) t.write(' version: %s\n' % bzrlib.__version__) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.getfqdn())) t.write(' arguments: %r\n' % argv) t.write(' working dir: %s\n' % os.getcwdu()) t.write(' platform: %s\n' % sys.platform) t.write(' python: %s\n' % (sys.version_info,)) import atexit atexit.register(_close_trace) def _close_trace(): times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum, %.3f elapsed" % (times[:4] + ((times[4] - _starttime),))) M 644 inline bzrlib/tree.py data 14241 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tree classes, representing directory at point in time. """ from sets import Set import os.path, os, fnmatch from osutils import pumpfile, compare_files, filesize, quotefn, sha_file, \ joinpath, splitpath, appendpath, isdir, isfile, file_kind, fingerprint_file import errno from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE from inventory import Inventory from trace import mutter, note from errors import bailout import branch import bzrlib class Tree: """Abstract file tree. There are several subclasses: * `WorkingTree` exists as files on disk editable by the user. * `RevisionTree` is a tree as recorded at some point in the past. * `EmptyTree` Trees contain an `Inventory` object, and also know how to retrieve file texts mentioned in the inventory, either from a working directory or from a store. It is possible for trees to contain files that are not described in their inventory or vice versa; for this use `filenames()`. Trees can be compared, etc, regardless of whether they are working trees or versioned trees. """ def has_filename(self, filename): """True if the tree has given filename.""" raise NotImplementedError() def has_id(self, file_id): return self.inventory.has_id(file_id) def id_set(self): """Return set of all ids in this tree.""" return self.inventory.id_set() def id2path(self, file_id): return self.inventory.id2path(file_id) def _get_inventory(self): return self._inventory inventory = property(_get_inventory, doc="Inventory of this Tree") def _check_retrieved(self, ie, f): fp = fingerprint_file(f) f.seek(0) if ie.text_size != None: if ie.text_size != fp['size']: bailout("mismatched size for file %r in %r" % (ie.file_id, self._store), ["inventory expects %d bytes" % ie.text_size, "file is actually %d bytes" % fp['size'], "store is probably damaged/corrupt"]) if ie.text_sha1 != fp['sha1']: bailout("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store), ["inventory expects %s" % ie.text_sha1, "file is actually %s" % fp['sha1'], "store is probably damaged/corrupt"]) def print_file(self, fileid): """Print file with id `fileid` to stdout.""" import sys pumpfile(self.get_file(fileid), sys.stdout) def export(self, dest): """Export this tree to a new directory. `dest` should not exist, and will be created holding the contents of this tree. :todo: To handle subdirectories we need to create the directories first. :note: If the export fails, the destination directory will be left in a half-assed state. """ os.mkdir(dest) mutter('export version %r' % self) inv = self.inventory for dp, ie in inv.iter_entries(): kind = ie.kind fullpath = appendpath(dest, dp) if kind == 'directory': os.mkdir(fullpath) elif kind == 'file': pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb')) else: bailout("don't know how to export {%s} of kind %r" % (fid, kind)) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) class WorkingTree(Tree): """Working copy tree. The inventory is held in the `Branch` working-inventory, and the files are in a directory on disk. It is possible for a `WorkingTree` to have a filename which is not listed in the Inventory and vice versa. """ def __init__(self, basedir, inv): self._inventory = inv self.basedir = basedir self.path2id = inv.path2id def __repr__(self): return "<%s of %s>" % (self.__class__.__name__, self.basedir) def abspath(self, filename): return os.path.join(self.basedir, filename) def has_filename(self, filename): return os.path.exists(self.abspath(filename)) def get_file(self, file_id): return self.get_file_byname(self.id2path(file_id)) def get_file_byname(self, filename): return file(self.abspath(filename), 'rb') def _get_store_filename(self, file_id): ## XXX: badly named; this isn't in the store at all return self.abspath(self.id2path(file_id)) def has_id(self, file_id): # files that have been deleted are excluded if not self.inventory.has_id(file_id): return False return os.access(self.abspath(self.inventory.id2path(file_id)), os.F_OK) def get_file_size(self, file_id): return os.stat(self._get_store_filename(file_id))[ST_SIZE] def get_file_sha1(self, file_id): f = self.get_file(file_id) return sha_file(f) def file_class(self, filename): if self.path2id(filename): return 'V' elif self.is_ignored(filename): return 'I' else: return '?' def list_files(self): """Recursively list all files as (path, class, kind, id). Lists, but does not descend into unversioned directories. This does not include files that have been deleted in this tree. Skips the control directory. """ inv = self.inventory def descend(from_dir_relpath, from_dir_id, dp): ls = os.listdir(dp) ls.sort() for f in ls: ## TODO: If we find a subdirectory with its own .bzr ## directory, then that is a separate tree and we ## should exclude it. if bzrlib.BZRDIR == f: continue # path within tree fp = appendpath(from_dir_relpath, f) # absolute path fap = appendpath(dp, f) f_ie = inv.get_child(from_dir_id, f) if f_ie: c = 'V' elif self.is_ignored(fp): c = 'I' else: c = '?' fk = file_kind(fap) if f_ie: if f_ie.kind != fk: bailout("file %r entered as kind %r id %r, now of kind %r" % (fap, f_ie.kind, f_ie.file_id, fk)) yield fp, c, fk, (f_ie and f_ie.file_id) if fk != 'directory': continue if c != 'V': # don't descend unversioned directories continue for ff in descend(fp, f_ie.file_id, fap): yield ff for f in descend('', inv.root.file_id, self.basedir): yield f def unknowns(self): for subp in self.extras(): if not self.is_ignored(subp): yield subp def extras(self): """Yield all unknown files in this WorkingTree. If there are any unknown directories then only the directory is returned, not all its children. But if there are unknown files under a versioned subdirectory, they are returned. Currently returned depth-first, sorted by name within directories. """ ## TODO: Work from given directory downwards for path, dir_entry in self.inventory.directories(): mutter("search for unknowns in %r" % path) dirabs = self.abspath(path) if not isdir(dirabs): # e.g. directory deleted continue fl = [] for subf in os.listdir(dirabs): if (subf != '.bzr' and (subf not in dir_entry.children)): fl.append(subf) fl.sort() for subf in fl: subp = appendpath(path, subf) yield subp def ignored_files(self): """Yield list of PATH, IGNORE_PATTERN""" for subp in self.extras(): pat = self.is_ignored(subp) if pat != None: yield subp, pat def get_ignore_list(self): """Return list of ignore patterns. Cached in the Tree object after the first call. """ if hasattr(self, '_ignorelist'): return self._ignorelist l = bzrlib.DEFAULT_IGNORE[:] if self.has_filename(bzrlib.IGNORE_FILENAME): f = self.get_file_byname(bzrlib.IGNORE_FILENAME) l.extend([line.rstrip("\n\r") for line in f.readlines()]) self._ignorelist = l return l def is_ignored(self, filename): """Check whether the filename matches an ignore pattern. Patterns containing '/' need to match the whole path; others match against only the last component. If the file is ignored, returns the pattern which caused it to be ignored, otherwise None. So this can simply be used as a boolean if desired.""" ## TODO: Use '**' to match directories, and other extended globbing stuff from cvs/rsync. for pat in self.get_ignore_list(): if '/' in pat: # as a special case, you can put ./ at the start of a pattern; # this is good to match in the top-level only; if pat[:2] == './': newpat = pat[2:] else: newpat = pat if fnmatch.fnmatchcase(filename, newpat): return pat else: if fnmatch.fnmatchcase(splitpath(filename)[-1], pat): return pat return None class RevisionTree(Tree): """Tree viewing a previous revision. File text can be retrieved from the text store. :todo: Some kind of `__repr__` method, but a good one probably means knowing the branch and revision number, or at least passing a description to the constructor. """ def __init__(self, store, inv): self._store = store self._inventory = inv def get_file(self, file_id): ie = self._inventory[file_id] f = self._store[ie.text_id] mutter(" get fileid{%s} from %r" % (file_id, self)) self._check_retrieved(ie, f) return f def get_file_size(self, file_id): return self._inventory[file_id].text_size def get_file_sha1(self, file_id): ie = self._inventory[file_id] return ie.text_sha1 def has_filename(self, filename): return bool(self.inventory.path2id(filename)) def list_files(self): # The only files returned by this are those from the version for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id class EmptyTree(Tree): def __init__(self): self._inventory = Inventory() def has_filename(self, filename): return False def list_files(self): if False: # just to make it a generator yield None ###################################################################### # diff # TODO: Merge these two functions into a single one that can operate # on either a whole tree or a set of files. # TODO: Return the diff in order by filename, not by category or in # random order. Can probably be done by lock-stepping through the # filenames from both trees. def file_status(filename, old_tree, new_tree): """Return single-letter status, old and new names for a file. The complexity here is in deciding how to represent renames; many complex cases are possible. """ old_inv = old_tree.inventory new_inv = new_tree.inventory new_id = new_inv.path2id(filename) old_id = old_inv.path2id(filename) if not new_id and not old_id: # easy: doesn't exist in either; not versioned at all if new_tree.is_ignored(filename): return 'I', None, None else: return '?', None, None elif new_id: # There is now a file of this name, great. pass else: # There is no longer a file of this name, but we can describe # what happened to the file that used to have # this name. There are two possibilities: either it was # deleted entirely, or renamed. assert old_id if new_inv.has_id(old_id): return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id) else: return 'D', old_inv.id2path(old_id), None # if the file_id is new in this revision, it is added if new_id and not old_inv.has_id(new_id): return 'A' # if there used to be a file of this name, but that ID has now # disappeared, it is deleted if old_id and not new_inv.has_id(old_id): return 'D' return 'wtf?' def find_renames(old_inv, new_inv): for file_id in old_inv: if file_id not in new_inv: continue old_name = old_inv.id2path(file_id) new_name = new_inv.id2path(file_id) if old_name != new_name: yield (old_name, new_name) commit refs/heads/master mark :185 committer 1112841618 +1000 data 17 more notes on svk from :184 M 644 inline doc/svk.txt data 1259 svk === The strategic strength is that it can trivially and reliably interoperate with upstream projects using Subversion. This tends to satisfy people who need disconnected operation, and so to allow projects to feel safe about switching to Subversion. On the other hand it may be a bit flaky in implementation -- when I tried it (dec 04), it crashed in confusing ways several times. And certainly Subversion's reputation for reliability is mixed -- some people think it's very solid, but I've seen many db crashes at HP. Being written in Perl on top of Svn bindings may not inspire confidence. robertc says he's worked with the libsvn bindings and they're a mess. Relatively little documentation. In general a feeling of a very tall stack. There is some fluff about defining multiple repositories, which seems like an argument for history-in-branch. Keeps track of merge arrows to do smart merges. They follow Perforce in not having any control files in the tree -- nice in some ways but you must use the right tool to move or delete a working area. (In fact the whole thing seems to be inspired a bit by Perforce?) I think keeping just one dotfile at the top level may be a fair compromise. (If this is unfair or inaccurate mail me dammit.) commit refs/heads/master mark :186 committer 1112844010 +1000 data 26 doc: directly import diffs from :185 M 644 inline doc/random.txt data 9095 I think Ruby's point is right: we need to think about how a tool *feels* as you're using it. Making regular commits gives a nice rhythm to to working; in some ways it's nicer to just commit single files with C-x v v than to build complex changesets. (See gmane.c.v-c.arch.devel post 19 Nov, Tom Lord.) * Would like to generate an activity report, to e.g. mail to your boss or post to your blog. "What did I change today, across all these specified branches?" * It is possibly nice that tla by default forbids you from committing if emacs autosave or lock files exist -- I find it confusing to commit somethin other than what is shown in the editor window because there are unsaved changes. However, grumbling about unknown files is annoying, and requiring people to edit regexps in the id-tagging-method file to fix it is totally unreasonable. Perhaps there should be a preference to abort on unknown files, or perhaps it should be possible to specify forbidden files. Perhaps this is related to a mechanism to detect conflicted files: should refuse to commit if there are any .rej files lying around. *Those who lose history are doomed to recreate it.* -- broked (on #gnu.arch.users) *A universal convention supplies all of maintainability, clarity, consistency, and a foundation for good programming habits too. What it doesn't do is insist that you follow it against your will. That's Python!* -- Tim Peters on comp.lang.python, 2001-06-16 (Bazaar provides mechanism and convention, but it is up to you whether you wish to follow or enforce that convention.) ---- jblack asks for A way to subtract merges, so that you can see the work you've done to a branch since conception. ---- :: now that is a neat idea: advertise branches over zeroconf should make lca fun :-) ---- http://thedailywtf.com/ShowPost.aspx?PostID=24281 Source control is necessary and useful, but in a team of one (or even two) people the setup overhead isn't always worth it--especially if you're going to join source control in a month, and you don't want to have to migrate everything out of your existing (in my case, skunkworks) system before you can use it. At least that was my experience--I putzed with CVS a bit and knew other source control systems pretty well, but in the day-to-day it wasn't worth the bother (granted, I was a bit offended at having to wait to use the mainline source control, but that's another matter). I think Bazaar-NG will have such low setup overhead (just ``init``, ``add``) that it can be easily used for even tiny projects. The ability to merge previously-unrelated trees means they can fold their project in later. ---- From tridge: * cope without $EMAIL better * notes at start of .bzr.log: * you can delete this * or include it in bug reports * should you be able to remove things from the default ignore list? * headers at start of diff, giving some comments, perhaps dates * is diff against /dev/null really OK? I think so. * separate remove/delete commands? * detect files which were removed and now in 'missing' state * should we actually compare files for 'status', or check mtime and size; reading every file in the samba source tree can take a long time. without this, doing a status on a large tree can be very slow. but relying on mtime/size is a bit dangerous. people really do work on trees which take a large chunk of memory and which will not stay in memory * status up-to-date files: not 'U', and don't list without --all * if status does compare file text, then it should be quick when checking just a single file * wrapper for svn that every time run logs - command - all inputs - time it took - sufficient to replay everything - record all files * status crashes if a file is missing * option for -p1 level on diff, etc. perhaps * commit without message should start $EDITOR * don't duplicate all files on commit * start importing tridge-junkcode * perhaps need xdelta storage sooner rather than later, to handle very large file ---- The first operation most people do with a new version-control system is *not* making their own project, but rather getting a checkout of an existing project, building it, and possibly submitting a patch. So those operations should be *extremely* easy. ---- * Way to check that a branch is fully merged, and no longer needed: should mean all its changes have been integrated upstream, no uncommitted changes or rejects or unknown files. * Filter revisions by containing a particular word (as for log). Perhaps have key-value fields that might be used for e.g. line-of-development or bug nr? * List difference in the revisions on one branch vs another. * Perhaps use a partially-readable but still hopefully unique ID for revisions/inventories? * Preview what will happen in a merge before it is applied * When a changeset deletes a file, should have the option to just make it unknown/ignored. Perhaps this is best handled by an interactive merge. If the file is unchanged locally and deleted remotely, it will by default be deleted (but the user has the option to reject the delete, or to make it just unversioned, or to save a copy.) If it is modified locall then the user still needs to choose between those options but there is no default (or perhaps the default is to reject the delete.) * interactive commit, prompting whether each hunk should be sent (as for darcs) * Write up something about detection of unmodified files * Preview a merge so as to get some idea what will happen: * What revisions will be merged (log entries, etc) * What files will be affected? * Are those simple updates, or have they been updated locally as well. * Any renames or metadata clashes? * Show diffs or conflict markers. * Do the merge, but write into a second directory. * "Show me all changesets that touch this file" Can be done by walking back through all revisions, and filtering out those where the file-id either gets a new name or a new text. * Way to commit backdated revisions or pretend to be something by someone else, for the benefit of import tools; in general allow everything taken from the current environment to be overridden. * Cope well when trying to checkout or update over a flaky connection. Passive HTTP possibly helps with this: we can fetch all the file texts first, then the inventory, and can even retry interrupted connections. * Use readline for reading log messages, and store a history of previous commit messages! * Warn when adding huge files(?) - more than say 10MB? On the other hand, why not just cope? * Perhaps allow people to specify a revision-id, much as people have unique but human-assigned names for patches at the moment? ---- 20050218090900.GA2071@opteron.random Subject: Re: [darcs-users] Re: [BK] upgrade will be needed From: Andrea Arcangeli Newsgroups: gmane.linux.kernel Date: Fri, 18 Feb 2005 10:09:00 +0100 On Thu, Feb 17, 2005 at 06:24:53PM -0800, Tupshin Harper wrote: > small to medium sized ones). Last I checked, Arch was still too slow in > some areas, though that might have changed in recent months. Also, many IMHO someone needs to rewrite ARCH using the RCS or SCCS format for the backend and a single file for the changesets and with sane parameters conventions miming SVN. The internal algorithms of arch seems the most advanced possible. It's just the interface and the fs backend that's so bad and doesn't compress in the backups either. SVN bsddb doesn't compress either by default, but at least the new fsfs compresses pretty well, not as good as CVS, but not as badly as bsddb and arch either. I may be completely wrong, so take the above just as a humble suggestion. darcs scares me a bit because it's in haskell, I don't believe very much in functional languages for compute intensive stuff, ram utilization skyrockets sometime (I wouldn't like to need >1G of ram to manage the tree). Other languages like python or perl are much slower than C/C++ too but at least ram utilization can be normally dominated to sane levels with them and they can be greatly optimized easily with C/C++ extensions of the performance critical parts. ----- * Fix up diffs for files without a trailing newline ----- * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. commit refs/heads/master mark :187 committer 1112844900 +1000 data 57 fix inverted sense introduced in previous pychecker fixup from :186 M 644 inline bzrlib/info.py data 3300 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import time import bzrlib from osutils import format_date def show_info(b): # TODO: Maybe show space used by working tree, versioned files, # unknown files, text store. print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl != None: return pl else: return 's' count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %8d %s' % (count_status[fs], name) print ' %8d versioned subdirector%s' % (count_version_dirs, plural(count_version_dirs, 'y', 'ies')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %8d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %8d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %8d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) print print 'text store:' c, t = b.text_store.total_size() print ' %8d file texts' % c print ' %8d kB' % (t/1024) print print 'revision store:' c, t = b.revision_store.total_size() print ' %8d revisions' % c print ' %8d kB' % (t/1024) print print 'inventory store:' c, t = b.inventory_store.total_size() print ' %8d inventories' % c print ' %8d kB' % (t/1024) commit refs/heads/master mark :188 committer 1112852825 +1000 data 63 - experimental remote-branch support - fix up newinventory code from :187 M 644 inline bzrlib/remotebranch.py data 1934 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ## XXX: This is pretty slow on high-latency connections because it ## doesn't keep the HTTP connection alive. If you have a smart local ## proxy it may be much better. Eventually I want to switch to ## urlgrabber which should use HTTP much more efficiently. import urllib2, gzip, zlib from errors import BzrError from revision import Revision from cStringIO import StringIO # h = HTTPConnection('localhost:8000') # h = HTTPConnection('bazaar-ng.org') prefix = 'http://bazaar-ng.org/bzr/main' def get_url(path): try: url = prefix + path return urllib2.urlopen(url) except urllib2.URLError, e: raise BzrError("remote fetch failed: %r: %s" % (url, e)) print 'read history' history = get_url('/.bzr/revision-history').read().split('\n') for i, rev_id in enumerate(history): print 'read revision %d' % i comp_f = get_url('/.bzr/revision-store/%s.gz' % rev_id) comp_data = comp_f.read() # python gzip needs a seekable file (!!) but the HTTP response # isn't, so we need to buffer it uncomp_f = gzip.GzipFile(fileobj=StringIO(comp_data)) rev = Revision.read_xml(uncomp_f) print rev.message print '----' M 644 inline bzrlib/newinventory.py data 4057 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from cElementTree import Element, ElementTree, SubElement def write_inventory(inv, f): el = Element('inventory', {'version': '2'}) root = Element('root_directory', {'id': inv.root.file_id}) el.append(root) def descend(parent_el, ie): kind = ie.kind el = Element(kind, {'name': ie.name, 'id': ie.file_id,}) if kind == 'file': if ie.text_id: el.set('text_id', ie.text_id) if ie.text_sha1: el.set('text_sha1', ie.text_sha1) if ie.text_size != None: el.set('text_size', ('%d' % ie.text_size)) elif kind != 'directory': bailout('unknown InventoryEntry kind %r' % kind) el.tail = '\n' parent_el.append(el) if kind == 'directory': l = ie.children.items() l.sort() for child_name, child_ie in l: descend(el, child_ie) # walk down through inventory, adding all directories l = inv._root.children.items() l.sort() for entry_name, ie in l: descend(root, ie) ElementTree(el).write(f, 'utf-8') f.write('\n') # This writes out an inventory without building an XML tree first, # just to see if it's faster. Not currently used. def write_slacker_inventory(inv, f): def descend(ie): kind = ie.kind f.write('<%s name="%s" id="%s" ' % (kind, ie.name, ie.file_id)) if kind == 'file': if ie.text_id: f.write('text_id="%s" ' % ie.text_id) if ie.text_sha1: f.write('text_sha1="%s" ' % ie.text_sha1) if ie.text_size != None: f.write('text_size="%d" ' % ie.text_size) f.write('/>\n') elif kind == 'directory': f.write('>\n') l = ie.children.items() l.sort() for child_name, child_ie in l: descend(child_ie) f.write('\n') else: bailout('unknown InventoryEntry kind %r' % kind) f.write('\n') f.write('\n') l = inv._root.children.items() l.sort() for entry_name, ie in l: descend(ie) f.write('\n') f.write('\n') def read_new_inventory(f): from inventory import Inventory, InventoryEntry def descend(parent_ie, el): kind = el.tag name = el.get('name') file_id = el.get('id') ie = InventoryEntry(file_id, name, el.tag) parent_ie.children[name] = ie inv._byid[file_id] = ie if kind == 'directory': for child_el in el: descend(ie, child_el) elif kind == 'file': assert len(el) == 0 ie.text_id = el.get('text_id') v = el.get('text_size') ie.text_size = v and int(v) ie.text_sha1 = el.get('text_sha1') else: bailout("unknown inventory entry %r" % kind) inv_el = ElementTree().parse(f) assert inv_el.tag == 'inventory' root_el = inv_el[0] assert root_el.tag == 'root_directory' inv = Inventory() for el in root_el: descend(inv._root, el) commit refs/heads/master mark :189 committer 1112853415 +1000 data 22 improved new-inventory from :188 M 644 inline bzrlib/newinventory.py data 4164 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from cElementTree import Element, ElementTree, SubElement def write_inventory(inv, f): el = Element('inventory', {'version': '2'}) el.text = '\n' root = Element('root_directory', {'id': inv.root.file_id}) root.tail = root.text = '\n' el.append(root) def descend(parent_el, ie): kind = ie.kind el = Element(kind, {'name': ie.name, 'id': ie.file_id,}) if kind == 'file': if ie.text_id: el.set('text_id', ie.text_id) if ie.text_sha1: el.set('text_sha1', ie.text_sha1) if ie.text_size != None: el.set('text_size', ('%d' % ie.text_size)) elif kind != 'directory': bailout('unknown InventoryEntry kind %r' % kind) el.tail = '\n' parent_el.append(el) if kind == 'directory': el.text = '\n' # break before having children l = ie.children.items() l.sort() for child_name, child_ie in l: descend(el, child_ie) # walk down through inventory, adding all directories l = inv.root.children.items() l.sort() for entry_name, ie in l: descend(root, ie) ElementTree(el).write(f, 'utf-8') f.write('\n') # This writes out an inventory without building an XML tree first, # just to see if it's faster. Not currently used. def write_slacker_inventory(inv, f): def descend(ie): kind = ie.kind f.write('<%s name="%s" id="%s" ' % (kind, ie.name, ie.file_id)) if kind == 'file': if ie.text_id: f.write('text_id="%s" ' % ie.text_id) if ie.text_sha1: f.write('text_sha1="%s" ' % ie.text_sha1) if ie.text_size != None: f.write('text_size="%d" ' % ie.text_size) f.write('/>\n') elif kind == 'directory': f.write('>\n') l = ie.children.items() l.sort() for child_name, child_ie in l: descend(child_ie) f.write('\n') else: bailout('unknown InventoryEntry kind %r' % kind) f.write('\n') f.write('\n') l = inv.root.children.items() l.sort() for entry_name, ie in l: descend(ie) f.write('\n') f.write('\n') def read_new_inventory(f): from inventory import Inventory, InventoryEntry def descend(parent_ie, el): kind = el.tag name = el.get('name') file_id = el.get('id') ie = InventoryEntry(file_id, name, el.tag) parent_ie.children[name] = ie inv._byid[file_id] = ie if kind == 'directory': for child_el in el: descend(ie, child_el) elif kind == 'file': assert len(el) == 0 ie.text_id = el.get('text_id') v = el.get('text_size') ie.text_size = v and int(v) ie.text_sha1 = el.get('text_sha1') else: bailout("unknown inventory entry %r" % kind) inv_el = ElementTree().parse(f) assert inv_el.tag == 'inventory' root_el = inv_el[0] assert root_el.tag == 'root_directory' inv = Inventory() for el in root_el: descend(inv.root, el) commit refs/heads/master mark :190 committer 1112853453 +1000 data 42 64 bits of randomness in file/revision ids from :189 M 644 inline bzrlib/branch.py data 32695 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. :todo: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. :todo: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. :todo: Keep the on-disk branch locked while the object exists. :todo: mkdir() method. """ def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. :param base: Base directory for the branch. :param init: If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. :param find_root: If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch""" return file(self.controlfilename(file_or_path), mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'wb').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'rb').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() inv = Inventory.read_xml(self.controlfile('inventory', 'r')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'w') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. This puts the files in the Added state, so that they will be recorded by the next commit. :todo: Perhaps have an option to add the ids even if the files do not (yet) exist. :todo: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. :todo: Option to specify file id. :todo: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on :todo: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True :todo: Do something useful with directories. :todo: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. :param timestamp: if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) mutter("append to revision-history") f = self.controlfile('revision-history', 'at') f.write(rev_id + '\n') f.close() if verbose: note("commited r%d" % self.revno()) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. :todo: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] else: return None def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original'): """Write out human-readable log of commits to this branch :param utc: If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l revno += 1 precursor = p def rename_one(self, from_rel, to_rel): tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(self, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo :todo: Get state for single files. :todo: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = self.basis_tree() new = self.working_tree() for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("wierd file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" try: shutil.rmtree(self.base) except OSError: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :191 committer 1112854562 +1000 data 26 more XML performance tests from :190 M 644 inline bzrlib/commands.py data 28747 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, time, types, shutil, tempfile, traceback, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] --revision REV Show changes since REV, rather than predecessor. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Diff selected files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = cmdfn.__doc__ if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': [], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': raise BzrError("arg form %r not implemented yet" % ap) elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot prof = hotshot.Profile('.bzr.profile') ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load('.bzr.profile') #stats.strip_dirs() stats.sort_stats('time') stats.print_stats(20) return ret else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') traceback.print_exc(None, bzrlib.trace._tracefile) log_error('(see $HOME/.bzr.log for debug information)\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error('(see $HOME/.bzr.log for debug information)\n') traceback.print_exc(None, bzrlib.trace._tracefile) ## traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') M 644 inline bzrlib/newinventory.py data 4463 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from cElementTree import Element, ElementTree, SubElement def write_inventory(inv, f): el = Element('inventory', {'version': '2'}) el.text = '\n' root = Element('root_directory', {'id': inv.root.file_id}) root.tail = root.text = '\n' el.append(root) def descend(parent_el, ie): kind = ie.kind el = Element(kind, {'name': ie.name, 'id': ie.file_id,}) if kind == 'file': if ie.text_id: el.set('text_id', ie.text_id) if ie.text_sha1: el.set('text_sha1', ie.text_sha1) if ie.text_size != None: el.set('text_size', ('%d' % ie.text_size)) elif kind != 'directory': bailout('unknown InventoryEntry kind %r' % kind) el.tail = '\n' parent_el.append(el) if kind == 'directory': el.text = '\n' # break before having children l = ie.children.items() l.sort() for child_name, child_ie in l: descend(el, child_ie) # walk down through inventory, adding all directories l = inv.root.children.items() l.sort() for entry_name, ie in l: descend(root, ie) ElementTree(el).write(f, 'utf-8') f.write('\n') def escape_attr(text): return text.replace("&", "&") \ .replace("'", "'") \ .replace('"', """) \ .replace("<", "<") \ .replace(">", ">") # This writes out an inventory without building an XML tree first, # just to see if it's faster. Not currently used. def write_slacker_inventory(inv, f): def descend(ie): kind = ie.kind f.write('<%s name="%s" id="%s" ' % (kind, escape_attr(ie.name), escape_attr(ie.file_id))) if kind == 'file': if ie.text_id: f.write('text_id="%s" ' % ie.text_id) if ie.text_sha1: f.write('text_sha1="%s" ' % ie.text_sha1) if ie.text_size != None: f.write('text_size="%d" ' % ie.text_size) f.write('/>\n') elif kind == 'directory': f.write('>\n') l = ie.children.items() l.sort() for child_name, child_ie in l: descend(child_ie) f.write('\n') else: bailout('unknown InventoryEntry kind %r' % kind) f.write('\n') f.write('\n' % escape_attr(inv.root.file_id)) l = inv.root.children.items() l.sort() for entry_name, ie in l: descend(ie) f.write('\n') f.write('\n') def read_new_inventory(f): from inventory import Inventory, InventoryEntry def descend(parent_ie, el): kind = el.tag name = el.get('name') file_id = el.get('id') ie = InventoryEntry(file_id, name, el.tag) parent_ie.children[name] = ie inv._byid[file_id] = ie if kind == 'directory': for child_el in el: descend(ie, child_el) elif kind == 'file': assert len(el) == 0 ie.text_id = el.get('text_id') v = el.get('text_size') ie.text_size = v and int(v) ie.text_sha1 = el.get('text_sha1') else: bailout("unknown inventory entry %r" % kind) inv_el = ElementTree().parse(f) assert inv_el.tag == 'inventory' root_el = inv_el[0] assert root_el.tag == 'root_directory' inv = Inventory() for el in root_el: descend(inv.root, el) commit refs/heads/master mark :192 committer 1112857733 +1000 data 56 - exercise the network more towards doing a remote clone from :191 M 644 inline bzrlib/remotebranch.py data 2980 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ## XXX: This is pretty slow on high-latency connections because it ## doesn't keep the HTTP connection alive. If you have a smart local ## proxy it may be much better. Eventually I want to switch to ## urlgrabber which should use HTTP much more efficiently. import urllib2, gzip, zlib from sets import Set from cStringIO import StringIO from errors import BzrError from revision import Revision from inventory import Inventory # h = HTTPConnection('localhost:8000') # h = HTTPConnection('bazaar-ng.org') # prefix = 'http://localhost:8000' prefix = 'http://bazaar-ng.org/bzr/main/' def get_url(path, compressed=False): try: url = prefix + path if compressed: url += '.gz' url_f = urllib2.urlopen(url) if not compressed: return url_f else: return gzip.GzipFile(fileobj=StringIO(url_f.read())) except urllib2.URLError, e: raise BzrError("remote fetch failed: %r: %s" % (url, e)) got_invs = Set() got_texts = Set() print 'read history' history = get_url('/.bzr/revision-history').readlines() num_revs = len(history) for i, rev_id in enumerate(history): rev_id = rev_id.rstrip() print 'read revision %d/%d' % (i, num_revs) # python gzip needs a seekable file (!!) but the HTTP response # isn't, so we need to buffer it rev_f = get_url('/.bzr/revision-store/%s' % rev_id, compressed=True) rev = Revision.read_xml(rev_f) print rev.message inv_id = rev.inventory_id if inv_id not in got_invs: print 'get inventory %s' % inv_id inv_f = get_url('/.bzr/inventory-store/%s' % inv_id, compressed=True) inv = Inventory.read_xml(inv_f) print '%4d inventory entries' % len(inv) for path, ie in inv.iter_entries(): text_id = ie.text_id if text_id == None: continue if text_id in got_texts: continue print ' fetch %s text {%s}' % (path, text_id) text_f = get_url('/.bzr/text-store/%s' % text_id, compressed=True) got_texts.add(text_id) got_invs.add(inv_id) print '----' commit refs/heads/master mark :193 committer 1112938722 +1000 data 30 more experiments with http get from :192 M 644 inline bzrlib/remotebranch.py data 3193 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ## XXX: This is pretty slow on high-latency connections because it ## doesn't keep the HTTP connection alive. If you have a smart local ## proxy it may be much better. Eventually I want to switch to ## urlgrabber which should use HTTP much more efficiently. import urllib2, gzip, zlib from sets import Set from cStringIO import StringIO from errors import BzrError from revision import Revision from inventory import Inventory # h = HTTPConnection('localhost:8000') # h = HTTPConnection('bazaar-ng.org') # velocitynet.com.au transparently proxies connections and thereby # breaks keep-alive -- sucks! import urlgrabber.keepalive urlgrabber.keepalive.DEBUG = 2 import urlgrabber prefix = 'http://localhost:8000' # prefix = 'http://bazaar-ng.org/bzr/main/' def get_url(path, compressed=False): try: url = prefix + path if compressed: url += '.gz' url_f = urlgrabber.urlopen(url, keepalive=1, close_connection=0) if not compressed: return url_f else: return gzip.GzipFile(fileobj=StringIO(url_f.read())) except urllib2.URLError, e: raise BzrError("remote fetch failed: %r: %s" % (url, e)) got_invs = Set() got_texts = Set() print 'read history' history = get_url('/.bzr/revision-history').readlines() num_revs = len(history) for i, rev_id in enumerate(history): rev_id = rev_id.rstrip() print 'read revision %d/%d' % (i, num_revs) # python gzip needs a seekable file (!!) but the HTTP response # isn't, so we need to buffer it rev_f = get_url('/.bzr/revision-store/%s' % rev_id, compressed=True) rev = Revision.read_xml(rev_f) print rev.message inv_id = rev.inventory_id if inv_id not in got_invs: print 'get inventory %s' % inv_id inv_f = get_url('/.bzr/inventory-store/%s' % inv_id, compressed=True) inv = Inventory.read_xml(inv_f) print '%4d inventory entries' % len(inv) for path, ie in inv.iter_entries(): text_id = ie.text_id if text_id == None: continue if text_id in got_texts: continue print ' fetch %s text {%s}' % (path, text_id) text_f = get_url('/.bzr/text-store/%s' % text_id, compressed=True) got_texts.add(text_id) got_invs.add(inv_id) print '----' commit refs/heads/master mark :194 committer 1112938743 +1000 data 37 add rsync exclude patterns for upload from :193 M 644 inline .rsyncexclude data 100 *.pyc *.pyo *~ # arch can bite me {arch} .arch-ids ,,* ++* /doc/*.html *.tmp bzr-test.log [#]*# .#* commit refs/heads/master mark :195 committer 1112938786 +1000 data 34 - import lovely urlgrabber library from :194 M 644 inline urlgrabber/__init__.py data 2259 # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Library General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # Copyright 2002-2004 Michael D. Stenner, Ryan Tomayko # $Id: __init__.py,v 1.14 2005/03/08 22:54:25 mstenner Exp $ """A high-level cross-protocol url-grabber. Using urlgrabber, data can be fetched in three basic ways: urlgrab(url) copy the file to the local filesystem urlopen(url) open the remote file and return a file object (like urllib2.urlopen) urlread(url) return the contents of the file as a string When using these functions (or methods), urlgrabber supports the following features: * identical behavior for http://, ftp://, and file:// urls * http keepalive - faster downloads of many files by using only a single connection * byte ranges - fetch only a portion of the file * reget - for a urlgrab, resume a partial download * progress meters - the ability to report download progress automatically, even when using urlopen! * throttling - restrict bandwidth usage * retries - automatically retry a download if it fails. The number of retries and failure types are configurable. * authenticated server access for http and ftp * proxy support - support for authenticated http and ftp proxies * mirror groups - treat a list of mirrors as a single source, automatically switching mirrors if there is a failure. """ __version__ = '2.9.6' __date__ = '2005/03/08' __author__ = 'Michael D. Stenner , ' \ 'Ryan Tomayko ' __url__ = 'http://linux.duke.edu/projects/urlgrabber/' from grabber import urlgrab, urlopen, urlread M 644 inline urlgrabber/byterange.py data 16806 # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the # Free Software Foundation, Inc., # 59 Temple Place, Suite 330, # Boston, MA 02111-1307 USA # This file is part of urlgrabber, a high-level cross-protocol url-grabber # Copyright 2002-2004 Michael D. Stenner, Ryan Tomayko # $Id: byterange.py,v 1.9 2005/02/14 21:55:07 mstenner Exp $ import os import stat import urllib import urllib2 import rfc822 try: from cStringIO import StringIO except ImportError, msg: from StringIO import StringIO class RangeError(IOError): """Error raised when an unsatisfiable range is requested.""" pass class HTTPRangeHandler(urllib2.BaseHandler): """Handler that enables HTTP Range headers. This was extremely simple. The Range header is a HTTP feature to begin with so all this class does is tell urllib2 that the "206 Partial Content" reponse from the HTTP server is what we expected. Example: import urllib2 import byterange range_handler = range.HTTPRangeHandler() opener = urllib2.build_opener(range_handler) # install it urllib2.install_opener(opener) # create Request and set Range header req = urllib2.Request('http://www.python.org/') req.header['Range'] = 'bytes=30-50' f = urllib2.urlopen(req) """ def http_error_206(self, req, fp, code, msg, hdrs): # 206 Partial Content Response r = urllib.addinfourl(fp, hdrs, req.get_full_url()) r.code = code r.msg = msg return r def http_error_416(self, req, fp, code, msg, hdrs): # HTTP's Range Not Satisfiable error raise RangeError('Requested Range Not Satisfiable') class RangeableFileObject: """File object wrapper to enable raw range handling. This was implemented primarilary for handling range specifications for file:// urls. This object effectively makes a file object look like it consists only of a range of bytes in the stream. Examples: # expose 10 bytes, starting at byte position 20, from # /etc/aliases. >>> fo = RangeableFileObject(file('/etc/passwd', 'r'), (20,30)) # seek seeks within the range (to position 23 in this case) >>> fo.seek(3) # tell tells where your at _within the range_ (position 3 in # this case) >>> fo.tell() # read EOFs if an attempt is made to read past the last # byte in the range. the following will return only 7 bytes. >>> fo.read(30) """ def __init__(self, fo, rangetup): """Create a RangeableFileObject. fo -- a file like object. only the read() method need be supported but supporting an optimized seek() is preferable. rangetup -- a (firstbyte,lastbyte) tuple specifying the range to work over. The file object provided is assumed to be at byte offset 0. """ self.fo = fo (self.firstbyte, self.lastbyte) = range_tuple_normalize(rangetup) self.realpos = 0 self._do_seek(self.firstbyte) def __getattr__(self, name): """This effectively allows us to wrap at the instance level. Any attribute not found in _this_ object will be searched for in self.fo. This includes methods.""" if hasattr(self.fo, name): return getattr(self.fo, name) raise AttributeError, name def tell(self): """Return the position within the range. This is different from fo.seek in that position 0 is the first byte position of the range tuple. For example, if this object was created with a range tuple of (500,899), tell() will return 0 when at byte position 500 of the file. """ return (self.realpos - self.firstbyte) def seek(self,offset,whence=0): """Seek within the byte range. Positioning is identical to that described under tell(). """ assert whence in (0, 1, 2) if whence == 0: # absolute seek realoffset = self.firstbyte + offset elif whence == 1: # relative seek realoffset = self.realpos + offset elif whence == 2: # absolute from end of file # XXX: are we raising the right Error here? raise IOError('seek from end of file not supported.') # do not allow seek past lastbyte in range if self.lastbyte and (realoffset >= self.lastbyte): realoffset = self.lastbyte self._do_seek(realoffset - self.realpos) def read(self, size=-1): """Read within the range. This method will limit the size read based on the range. """ size = self._calc_read_size(size) rslt = self.fo.read(size) self.realpos += len(rslt) return rslt def readline(self, size=-1): """Read lines within the range. This method will limit the size read based on the range. """ size = self._calc_read_size(size) rslt = self.fo.readline(size) self.realpos += len(rslt) return rslt def _calc_read_size(self, size): """Handles calculating the amount of data to read based on the range. """ if self.lastbyte: if size > -1: if ((self.realpos + size) >= self.lastbyte): size = (self.lastbyte - self.realpos) else: size = (self.lastbyte - self.realpos) return size def _do_seek(self,offset): """Seek based on whether wrapped object supports seek(). offset is relative to the current position (self.realpos). """ assert offset >= 0 if not hasattr(self.fo, 'seek'): self._poor_mans_seek(offset) else: self.fo.seek(self.realpos + offset) self.realpos+= offset def _poor_mans_seek(self,offset): """Seek by calling the wrapped file objects read() method. This is used for file like objects that do not have native seek support. The wrapped objects read() method is called to manually seek to the desired position. offset -- read this number of bytes from the wrapped file object. raise RangeError if we encounter EOF before reaching the specified offset. """ pos = 0 bufsize = 1024 while pos < offset: if (pos + bufsize) > offset: bufsize = offset - pos buf = self.fo.read(bufsize) if len(buf) != bufsize: raise RangeError('Requested Range Not Satisfiable') pos+= bufsize class FileRangeHandler(urllib2.FileHandler): """FileHandler subclass that adds Range support. This class handles Range headers exactly like an HTTP server would. """ def open_local_file(self, req): import mimetypes import mimetools host = req.get_host() file = req.get_selector() localfile = urllib.url2pathname(file) stats = os.stat(localfile) size = stats[stat.ST_SIZE] modified = rfc822.formatdate(stats[stat.ST_MTIME]) mtype = mimetypes.guess_type(file)[0] if host: host, port = urllib.splitport(host) if port or socket.gethostbyname(host) not in self.get_names(): raise URLError('file not on local host') fo = open(localfile,'rb') brange = req.headers.get('Range',None) brange = range_header_to_tuple(brange) assert brange != () if brange: (fb,lb) = brange if lb == '': lb = size if fb < 0 or fb > size or lb > size: raise RangeError('Requested Range Not Satisfiable') size = (lb - fb) fo = RangeableFileObject(fo, (fb,lb)) headers = mimetools.Message(StringIO( 'Content-Type: %s\nContent-Length: %d\nLast-modified: %s\n' % (mtype or 'text/plain', size, modified))) return urllib.addinfourl(fo, headers, 'file:'+file) # FTP Range Support # Unfortunately, a large amount of base FTP code had to be copied # from urllib and urllib2 in order to insert the FTP REST command. # Code modifications for range support have been commented as # follows: # -- range support modifications start/end here from urllib import splitport, splituser, splitpasswd, splitattr, \ unquote, addclosehook, addinfourl import ftplib import socket import sys import ftplib import mimetypes import mimetools class FTPRangeHandler(urllib2.FTPHandler): def ftp_open(self, req): host = req.get_host() if not host: raise IOError, ('ftp error', 'no host given') host, port = splitport(host) if port is None: port = ftplib.FTP_PORT # username/password handling user, host = splituser(host) if user: user, passwd = splitpasswd(user) else: passwd = None host = unquote(host) user = unquote(user or '') passwd = unquote(passwd or '') try: host = socket.gethostbyname(host) except socket.error, msg: raise URLError(msg) path, attrs = splitattr(req.get_selector()) dirs = path.split('/') dirs = map(unquote, dirs) dirs, file = dirs[:-1], dirs[-1] if dirs and not dirs[0]: dirs = dirs[1:] try: fw = self.connect_ftp(user, passwd, host, port, dirs) type = file and 'I' or 'D' for attr in attrs: attr, value = splitattr(attr) if attr.lower() == 'type' and \ value in ('a', 'A', 'i', 'I', 'd', 'D'): type = value.upper() # -- range support modifications start here rest = None range_tup = range_header_to_tuple(req.headers.get('Range',None)) assert range_tup != () if range_tup: (fb,lb) = range_tup if fb > 0: rest = fb # -- range support modifications end here fp, retrlen = fw.retrfile(file, type, rest) # -- range support modifications start here if range_tup: (fb,lb) = range_tup if lb == '': if retrlen is None or retrlen == 0: raise RangeError('Requested Range Not Satisfiable due to unobtainable file length.') lb = retrlen retrlen = lb - fb if retrlen < 0: # beginning of range is larger than file raise RangeError('Requested Range Not Satisfiable') else: retrlen = lb - fb fp = RangeableFileObject(fp, (0,retrlen)) # -- range support modifications end here headers = "" mtype = mimetypes.guess_type(req.get_full_url())[0] if mtype: headers += "Content-Type: %s\n" % mtype if retrlen is not None and retrlen >= 0: headers += "Content-Length: %d\n" % retrlen sf = StringIO(headers) headers = mimetools.Message(sf) return addinfourl(fp, headers, req.get_full_url()) except ftplib.all_errors, msg: raise IOError, ('ftp error', msg), sys.exc_info()[2] def connect_ftp(self, user, passwd, host, port, dirs): fw = ftpwrapper(user, passwd, host, port, dirs) return fw class ftpwrapper(urllib.ftpwrapper): # range support note: # this ftpwrapper code is copied directly from # urllib. The only enhancement is to add the rest # argument and pass it on to ftp.ntransfercmd def retrfile(self, file, type, rest=None): self.endtransfer() if type in ('d', 'D'): cmd = 'TYPE A'; isdir = 1 else: cmd = 'TYPE ' + type; isdir = 0 try: self.ftp.voidcmd(cmd) except ftplib.all_errors: self.init() self.ftp.voidcmd(cmd) conn = None if file and not isdir: # Use nlst to see if the file exists at all try: self.ftp.nlst(file) except ftplib.error_perm, reason: raise IOError, ('ftp error', reason), sys.exc_info()[2] # Restore the transfer mode! self.ftp.voidcmd(cmd) # Try to retrieve as a file try: cmd = 'RETR ' + file conn = self.ftp.ntransfercmd(cmd, rest) except ftplib.error_perm, reason: if str(reason)[:3] == '501': # workaround for REST not supported error fp, retrlen = self.retrfile(file, type) fp = RangeableFileObject(fp, (rest,'')) return (fp, retrlen) elif str(reason)[:3] != '550': raise IOError, ('ftp error', reason), sys.exc_info()[2] if not conn: # Set transfer mode to ASCII! self.ftp.voidcmd('TYPE A') # Try a directory listing if file: cmd = 'LIST ' + file else: cmd = 'LIST' conn = self.ftp.ntransfercmd(cmd) self.busy = 1 # Pass back both a suitably decorated object and a retrieval length return (addclosehook(conn[0].makefile('rb'), self.endtransfer), conn[1]) #################################################################### # Range Tuple Functions # XXX: These range tuple functions might go better in a class. _rangere = None def range_header_to_tuple(range_header): """Get a (firstbyte,lastbyte) tuple from a Range header value. Range headers have the form "bytes=-". This function pulls the firstbyte and lastbyte values and returns a (firstbyte,lastbyte) tuple. If lastbyte is not specified in the header value, it is returned as an empty string in the tuple. Return None if range_header is None Return () if range_header does not conform to the range spec pattern. """ global _rangere if range_header is None: return None if _rangere is None: import re _rangere = re.compile(r'^bytes=(\d{1,})-(\d*)') match = _rangere.match(range_header) if match: tup = range_tuple_normalize(match.group(1,2)) if tup and tup[1]: tup = (tup[0],tup[1]+1) return tup return () def range_tuple_to_header(range_tup): """Convert a range tuple to a Range header value. Return a string of the form "bytes=-" or None if no range is needed. """ if range_tup is None: return None range_tup = range_tuple_normalize(range_tup) if range_tup: if range_tup[1]: range_tup = (range_tup[0],range_tup[1] - 1) return 'bytes=%s-%s' % range_tup def range_tuple_normalize(range_tup): """Normalize a (first_byte,last_byte) range tuple. Return a tuple whose first element is guaranteed to be an int and whose second element will be '' (meaning: the last byte) or an int. Finally, return None if the normalized tuple == (0,'') as that is equivelant to retrieving the entire file. """ if range_tup is None: return None # handle first byte fb = range_tup[0] if fb in (None,''): fb = 0 else: fb = int(fb) # handle last byte try: lb = range_tup[1] except IndexError: lb = '' else: if lb is None: lb = '' elif lb != '': lb = int(lb) # check if range is over the entire file if (fb,lb) == (0,''): return None # check that the range is valid if lb < fb: raise RangeError('Invalid byte range: %s-%s' % (fb,lb)) return (fb,lb) M 644 inline urlgrabber/grabber.py data 44617 # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the # Free Software Foundation, Inc., # 59 Temple Place, Suite 330, # Boston, MA 02111-1307 USA # This file is part of urlgrabber, a high-level cross-protocol url-grabber # Copyright 2002-2004 Michael D. Stenner, Ryan Tomayko """A high-level cross-protocol url-grabber. GENERAL ARGUMENTS (kwargs) Where possible, the module-level default is indicated, and legal values are provided. copy_local = 0 [0|1] ignored except for file:// urls, in which case it specifies whether urlgrab should still make a copy of the file, or simply point to the existing copy. The module level default for this option is 0. close_connection = 0 [0|1] tells URLGrabber to close the connection after a file has been transfered. This is ignored unless the download happens with the http keepalive handler (keepalive=1). Otherwise, the connection is left open for further use. The module level default for this option is 0 (keepalive connections will not be closed). keepalive = 1 [0|1] specifies whether keepalive should be used for HTTP/1.1 servers that support it. The module level default for this option is 1 (keepalive is enabled). progress_obj = None a class instance that supports the following methods: po.start(filename, url, basename, length, text) # length will be None if unknown po.update(read) # read == bytes read so far po.end() text = None specifies an alternativ text item in the beginning of the progress bar line. If not given, the basename of the file is used. throttle = 1.0 a number - if it's an int, it's the bytes/second throttle limit. If it's a float, it is first multiplied by bandwidth. If throttle == 0, throttling is disabled. If None, the module-level default (which can be set on default_grabber.throttle) is used. See BANDWIDTH THROTTLING for more information. timeout = None a positive float expressing the number of seconds to wait for socket operations. If the value is None or 0.0, socket operations will block forever. Setting this option causes urlgrabber to call the settimeout method on the Socket object used for the request. See the Python documentation on settimeout for more information. http://www.python.org/doc/current/lib/socket-objects.html bandwidth = 0 the nominal max bandwidth in bytes/second. If throttle is a float and bandwidth == 0, throttling is disabled. If None, the module-level default (which can be set on default_grabber.bandwidth) is used. See BANDWIDTH THROTTLING for more information. range = None a tuple of the form (first_byte, last_byte) describing a byte range to retrieve. Either or both of the values may set to None. If first_byte is None, byte offset 0 is assumed. If last_byte is None, the last byte available is assumed. Note that the range specification is python-like in that (0,10) will yeild the first 10 bytes of the file. If set to None, no range will be used. reget = None [None|'simple'|'check_timestamp'] whether to attempt to reget a partially-downloaded file. Reget only applies to .urlgrab and (obviously) only if there is a partially downloaded file. Reget has two modes: 'simple' -- the local file will always be trusted. If there are 100 bytes in the local file, then the download will always begin 100 bytes into the requested file. 'check_timestamp' -- the timestamp of the server file will be compared to the timestamp of the local file. ONLY if the local file is newer than or the same age as the server file will reget be used. If the server file is newer, or the timestamp is not returned, the entire file will be fetched. NOTE: urlgrabber can do very little to verify that the partial file on disk is identical to the beginning of the remote file. You may want to either employ a custom "checkfunc" or simply avoid using reget in situations where corruption is a concern. user_agent = 'urlgrabber/VERSION' a string, usually of the form 'AGENT/VERSION' that is provided to HTTP servers in the User-agent header. The module level default for this option is "urlgrabber/VERSION". http_headers = None a tuple of 2-tuples, each containing a header and value. These will be used for http and https requests only. For example, you can do http_headers = (('Pragma', 'no-cache'),) ftp_headers = None this is just like http_headers, but will be used for ftp requests. proxies = None a dictionary that maps protocol schemes to proxy hosts. For example, to use a proxy server on host "foo" port 3128 for http and https URLs: proxies={ 'http' : 'http://foo:3128', 'https' : 'http://foo:3128' } note that proxy authentication information may be provided using normal URL constructs: proxies={ 'http' : 'http://user:host@foo:3128' } Lastly, if proxies is None, the default environment settings will be used. prefix = None a url prefix that will be prepended to all requested urls. For example: g = URLGrabber(prefix='http://foo.com/mirror/') g.urlgrab('some/file.txt') ## this will fetch 'http://foo.com/mirror/some/file.txt' This option exists primarily to allow identical behavior to MirrorGroup (and derived) instances. Note: a '/' will be inserted if necessary, so you cannot specify a prefix that ends with a partial file or directory name. opener = None Overrides the default urllib2.OpenerDirector provided to urllib2 when making requests. This option exists so that the urllib2 handler chain may be customized. Note that the range, reget, proxy, and keepalive features require that custom handlers be provided to urllib2 in order to function properly. If an opener option is provided, no attempt is made by urlgrabber to ensure chain integrity. You are responsible for ensuring that any extension handlers are present if said features are required. RETRY RELATED ARGUMENTS retry = None the number of times to retry the grab before bailing. If this is zero, it will retry forever. This was intentional... really, it was :). If this value is not supplied or is supplied but is None retrying does not occur. retrycodes = [-1,2,4,5,6,7] a sequence of errorcodes (values of e.errno) for which it should retry. See the doc on URLGrabError for more details on this. retrycodes defaults to [-1,2,4,5,6,7] if not specified explicitly. checkfunc = None a function to do additional checks. This defaults to None, which means no additional checking. The function should simply return on a successful check. It should raise URLGrabError on an unsuccessful check. Raising of any other exception will be considered immediate failure and no retries will occur. If it raises URLGrabError, the error code will determine the retry behavior. Negative error numbers are reserved for use by these passed in functions, so you can use many negative numbers for different types of failure. By default, -1 results in a retry, but this can be customized with retrycodes. If you simply pass in a function, it will be given exactly one argument: a CallbackObject instance with the .url attribute defined and either .filename (for urlgrab) or .data (for urlread). For urlgrab, .filename is the name of the local file. For urlread, .data is the actual string data. If you need other arguments passed to the callback (program state of some sort), you can do so like this: checkfunc=(function, ('arg1', 2), {'kwarg': 3}) if the downloaded file has filename /tmp/stuff, then this will result in this call (for urlgrab): function(obj, 'arg1', 2, kwarg=3) # obj.filename = '/tmp/stuff' # obj.url = 'http://foo.com/stuff' NOTE: both the "args" tuple and "kwargs" dict must be present if you use this syntax, but either (or both) can be empty. failure_callback = None The callback that gets called during retries when an attempt to fetch a file fails. The syntax for specifying the callback is identical to checkfunc, except for the attributes defined in the CallbackObject instance. In this case, it will have .exception and .url defined. As you might suspect, .exception is the exception that was raised. The callback is present primarily to inform the calling program of the failure, but if it raises an exception (including the one it's passed) that exception will NOT be caught and will therefore cause future retries to be aborted. BANDWIDTH THROTTLING urlgrabber supports throttling via two values: throttle and bandwidth Between the two, you can either specify and absolute throttle threshold or specify a theshold as a fraction of maximum available bandwidth. throttle is a number - if it's an int, it's the bytes/second throttle limit. If it's a float, it is first multiplied by bandwidth. If throttle == 0, throttling is disabled. If None, the module-level default (which can be set with set_throttle) is used. bandwidth is the nominal max bandwidth in bytes/second. If throttle is a float and bandwidth == 0, throttling is disabled. If None, the module-level default (which can be set with set_bandwidth) is used. THROTTLING EXAMPLES: Lets say you have a 100 Mbps connection. This is (about) 10^8 bits per second, or 12,500,000 Bytes per second. You have a number of throttling options: *) set_bandwidth(12500000); set_throttle(0.5) # throttle is a float This will limit urlgrab to use half of your available bandwidth. *) set_throttle(6250000) # throttle is an int This will also limit urlgrab to use half of your available bandwidth, regardless of what bandwidth is set to. *) set_throttle(6250000); set_throttle(1.0) # float Use half your bandwidth *) set_throttle(6250000); set_throttle(2.0) # float Use up to 12,500,000 Bytes per second (your nominal max bandwidth) *) set_throttle(6250000); set_throttle(0) # throttle = 0 Disable throttling - this is more efficient than a very large throttle setting. *) set_throttle(0); set_throttle(1.0) # throttle is float, bandwidth = 0 Disable throttling - this is the default when the module is loaded. SUGGESTED AUTHOR IMPLEMENTATION (THROTTLING) While this is flexible, it's not extremely obvious to the user. I suggest you implement a float throttle as a percent to make the distinction between absolute and relative throttling very explicit. Also, you may want to convert the units to something more convenient than bytes/second, such as kbps or kB/s, etc. """ # $Id: grabber.py,v 1.39 2005/03/03 00:54:23 mstenner Exp $ import os import os.path import urlparse import rfc822 import time import string import urllib import urllib2 from stat import * # S_* and ST_* try: exec('from ' + (__name__.split('.'))[0] + ' import __version__') except: __version__ = '???' auth_handler = urllib2.HTTPBasicAuthHandler( \ urllib2.HTTPPasswordMgrWithDefaultRealm()) DEBUG=0 try: from i18n import _ except ImportError, msg: def _(st): return st try: from httplib import HTTPException except ImportError, msg: HTTPException = None try: # This is a convenient way to make keepalive optional. # Just rename the module so it can't be imported. from keepalive import HTTPHandler except ImportError, msg: keepalive_handler = None else: keepalive_handler = HTTPHandler() try: # add in range support conditionally too from urlgrabber.byterange import HTTPRangeHandler, FileRangeHandler, \ FTPRangeHandler, range_tuple_normalize, range_tuple_to_header, \ RangeError except ImportError, msg: range_handlers = () RangeError = None have_range = 0 else: range_handlers = (HTTPRangeHandler(), FileRangeHandler(), FTPRangeHandler()) have_range = 1 # check whether socket timeout support is available (Python >= 2.3) import socket try: TimeoutError = socket.timeout have_socket_timeout = True except AttributeError: TimeoutError = None have_socket_timeout = False class URLGrabError(IOError): """ URLGrabError error codes: URLGrabber error codes (0 -- 255) 0 - everything looks good (you should never see this) 1 - malformed url 2 - local file doesn't exist 3 - request for non-file local file (dir, etc) 4 - IOError on fetch 5 - OSError on fetch 6 - no content length header when we expected one 7 - HTTPException 8 - Exceeded read limit (for urlread) 9 - Requested byte range not satisfiable. 10 - Byte range requested, but range support unavailable 11 - Illegal reget mode 12 - Socket timeout. MirrorGroup error codes (256 -- 511) 256 - No more mirrors left to try Custom (non-builtin) classes derived from MirrorGroup (512 -- 767) [ this range reserved for application-specific error codes ] Retry codes (< 0) -1 - retry the download, unknown reason Note: to test which group a code is in, you can simply do integer division by 256: e.errno / 256 Negative codes are reserved for use by functions passed in to retrygrab with checkfunc. The value -1 is built in as a generic retry code and is already included in the retrycodes list. Therefore, you can create a custom check function that simply returns -1 and the fetch will be re-tried. For more customized retries, you can use other negative number and include them in retry-codes. This is nice for outputting useful messages about what failed. You can use these error codes like so: try: urlgrab(url) except URLGrabError, e: if e.errno == 3: ... # or print e.strerror # or simply print e #### print '[Errno %i] %s' % (e.errno, e.strerror) """ pass class CallbackObject: """Container for returned callback data. This is currently a dummy class into which urlgrabber can stuff information for passing to callbacks. This way, the prototype for all callbacks is the same, regardless of the data that will be passed back. Any function that accepts a callback function as an argument SHOULD document what it will define in this object. It is possible that this class will have some greater functionality in the future. """ pass def close_all(): """close any open keepalive connections""" if keepalive_handler: keepalive_handler.close_all() def urlgrab(url, filename=None, **kwargs): """grab the file at and make a local copy at If filename is none, the basename of the url is used. urlgrab returns the filename of the local file, which may be different from the passed-in filename if the copy_local kwarg == 0. See module documentation for a description of possible kwargs. """ return default_grabber.urlgrab(url, filename, **kwargs) def urlopen(url, **kwargs): """open the url and return a file object If a progress object or throttle specifications exist, then a special file object will be returned that supports them. The file object can be treated like any other file object. See module documentation for a description of possible kwargs. """ return default_grabber.urlopen(url, **kwargs) def urlread(url, limit=None, **kwargs): """read the url into a string, up to 'limit' bytes If the limit is exceeded, an exception will be thrown. Note that urlread is NOT intended to be used as a way of saying "I want the first N bytes" but rather 'read the whole file into memory, but don't use too much' See module documentation for a description of possible kwargs. """ return default_grabber.urlread(url, limit, **kwargs) class URLGrabberOptions: """Class to ease kwargs handling.""" def __init__(self, delegate=None, **kwargs): """Initialize URLGrabberOptions object. Set default values for all options and then update options specified in kwargs. """ self.delegate = delegate if delegate is None: self._set_defaults() self._set_attributes(**kwargs) def __getattr__(self, name): if self.delegate and hasattr(self.delegate, name): return getattr(self.delegate, name) raise AttributeError, name def raw_throttle(self): """Calculate raw throttle value from throttle and bandwidth values. """ if self.throttle <= 0: return 0 elif type(self.throttle) == type(0): return float(self.throttle) else: # throttle is a float return self.bandwidth * self.throttle def derive(self, **kwargs): """Create a derived URLGrabberOptions instance. This method creates a new instance and overrides the options specified in kwargs. """ return URLGrabberOptions(delegate=self, **kwargs) def _set_attributes(self, **kwargs): """Update object attributes with those provided in kwargs.""" self.__dict__.update(kwargs) if have_range and kwargs.has_key('range'): # normalize the supplied range value self.range = range_tuple_normalize(self.range) if not self.reget in [None, 'simple', 'check_timestamp']: raise URLGrabError(11, _('Illegal reget mode: %s') \ % (self.reget, )) def _set_defaults(self): """Set all options to their default values. When adding new options, make sure a default is provided here. """ self.progress_obj = None self.throttle = 1.0 self.bandwidth = 0 self.retry = None self.retrycodes = [-1,2,4,5,6,7] self.checkfunc = None self.copy_local = 0 self.close_connection = 0 self.range = None self.user_agent = 'urlgrabber/%s' % __version__ self.keepalive = 1 self.proxies = None self.reget = None self.failure_callback = None self.prefix = None self.opener = None self.cache_openers = True self.timeout = None self.text = None self.http_headers = None self.ftp_headers = None class URLGrabber: """Provides easy opening of URLs with a variety of options. All options are specified as kwargs. Options may be specified when the class is created and may be overridden on a per request basis. New objects inherit default values from default_grabber. """ def __init__(self, **kwargs): self.opts = URLGrabberOptions(**kwargs) def _retry(self, opts, func, *args): tries = 0 while 1: tries = tries + 1 try: return apply(func, (opts,) + args, {}) except URLGrabError, e: if DEBUG: print 'EXCEPTION: %s' % e if (opts.retry is None) \ or (tries == opts.retry) \ or (e.errno not in opts.retrycodes): raise if opts.failure_callback: cb_func, cb_args, cb_kwargs = \ self._make_callback(opts.failure_callback) # this is a little icky - for now, the first element # of args is the url. we might consider a way to tidy # that up, though obj = CallbackObject() obj.exception = e obj.url = args[0] cb_func(obj, *cb_args, **cb_kwargs) def urlopen(self, url, **kwargs): """open the url and return a file object If a progress object or throttle value specified when this object was created, then a special file object will be returned that supports them. The file object can be treated like any other file object. """ opts = self.opts.derive(**kwargs) (url,parts) = self._parse_url(url) def retryfunc(opts, url): return URLGrabberFileObject(url, filename=None, opts=opts) return self._retry(opts, retryfunc, url) def urlgrab(self, url, filename=None, **kwargs): """grab the file at and make a local copy at If filename is none, the basename of the url is used. urlgrab returns the filename of the local file, which may be different from the passed-in filename if copy_local == 0. """ opts = self.opts.derive(**kwargs) (url, parts) = self._parse_url(url) (scheme, host, path, parm, query, frag) = parts if filename is None: if scheme in [ 'http', 'https' ]: filename = os.path.basename( urllib.unquote(path) ) else: filename = os.path.basename( path ) if scheme == 'file' and not opts.copy_local: # just return the name of the local file - don't make a # copy currently if not os.path.exists(path): raise URLGrabError(2, _('Local file does not exist: %s') % (path, )) elif not os.path.isfile(path): raise URLGrabError(3, _('Not a normal file: %s') % (path, )) elif not opts.range: return path def retryfunc(opts, url, filename): fo = URLGrabberFileObject(url, filename, opts) try: fo._do_grab() if not opts.checkfunc is None: cb_func, cb_args, cb_kwargs = \ self._make_callback(opts.checkfunc) obj = CallbackObject() obj.filename = filename obj.url = url apply(cb_func, (obj, )+cb_args, cb_kwargs) finally: fo.close() return filename return self._retry(opts, retryfunc, url, filename) def urlread(self, url, limit=None, **kwargs): """read the url into a string, up to 'limit' bytes If the limit is exceeded, an exception will be thrown. Note that urlread is NOT intended to be used as a way of saying "I want the first N bytes" but rather 'read the whole file into memory, but don't use too much' """ opts = self.opts.derive(**kwargs) (url, parts) = self._parse_url(url) if limit is not None: limit = limit + 1 def retryfunc(opts, url, limit): fo = URLGrabberFileObject(url, filename=None, opts=opts) s = '' try: # this is an unfortunate thing. Some file-like objects # have a default "limit" of None, while the built-in (real) # file objects have -1. They each break the other, so for # now, we just force the default if necessary. if limit is None: s = fo.read() else: s = fo.read(limit) if not opts.checkfunc is None: cb_func, cb_args, cb_kwargs = \ self._make_callback(opts.checkfunc) obj = CallbackObject() obj.data = s obj.url = url apply(cb_func, (obj, )+cb_args, cb_kwargs) finally: fo.close() return s s = self._retry(opts, retryfunc, url, limit) if limit and len(s) > limit: raise URLGrabError(8, _('Exceeded limit (%i): %s') % (limit, url)) return s def _parse_url(self,url): """break up the url into its component parts This function disassembles a url and 1) "normalizes" it, tidying it up a bit 2) does any authentication stuff it needs to do it returns the (cleaned) url and a tuple of component parts """ if self.opts.prefix: p = self.opts.prefix if p[-1] == '/' or url[0] == '/': url = p + url else: url = p + '/' + url (scheme, host, path, parm, query, frag) = \ urlparse.urlparse(url) if not scheme: if not url[0] == '/': url = os.path.abspath(url) url = 'file:' + url (scheme, host, path, parm, query, frag) = \ urlparse.urlparse(url) path = os.path.normpath(path) if scheme in ['http', 'https']: path = urllib.quote(path) if '@' in host and auth_handler and scheme in ['http', 'https']: try: user_pass, host = host.split('@', 1) if ':' in user_pass: user, password = user_pass.split(':', 1) except ValueError, e: raise URLGrabError(1, _('Bad URL: %s') % url) if DEBUG: print 'adding HTTP auth: %s, %s' % (user, password) auth_handler.add_password(None, host, user, password) parts = (scheme, host, path, parm, query, frag) url = urlparse.urlunparse(parts) return url, parts def _make_callback(self, callback_obj): if callable(callback_obj): return callback_obj, (), {} else: return callback_obj # create the default URLGrabber used by urlXXX functions. # NOTE: actual defaults are set in URLGrabberOptions default_grabber = URLGrabber() class URLGrabberFileObject: """This is a file-object wrapper that supports progress objects and throttling. This exists to solve the following problem: lets say you want to drop-in replace a normal open with urlopen. You want to use a progress meter and/or throttling, but how do you do that without rewriting your code? Answer: urlopen will return a wrapped file object that does the progress meter and-or throttling internally. """ def __init__(self, url, filename, opts): self.url = url self.filename = filename self.opts = opts self.fo = None self._rbuf = '' self._rbufsize = 1024*8 self._ttime = time.time() self._tsize = 0 self._amount_read = 0 self._opener = None self._do_open() def __getattr__(self, name): """This effectively allows us to wrap at the instance level. Any attribute not found in _this_ object will be searched for in self.fo. This includes methods.""" if hasattr(self.fo, name): return getattr(self.fo, name) raise AttributeError, name def _get_opener(self): """Build a urllib2 OpenerDirector based on request options.""" if self.opts.opener: return self.opts.opener elif self._opener is None: handlers = [] need_keepalive_handler = (keepalive_handler and self.opts.keepalive) need_range_handler = (range_handlers and \ (self.opts.range or self.opts.reget)) # if you specify a ProxyHandler when creating the opener # it _must_ come before all other handlers in the list or urllib2 # chokes. if self.opts.proxies: handlers.append( CachedProxyHandler(self.opts.proxies) ) # ------------------------------------------------------- # OK, these next few lines are a serious kludge to get # around what I think is a bug in python 2.2's # urllib2. The basic idea is that default handlers # get applied first. If you override one (like a # proxy handler), then the default gets pulled, but # the replacement goes on the end. In the case of # proxies, this means the normal handler picks it up # first and the proxy isn't used. Now, this probably # only happened with ftp or non-keepalive http, so not # many folks saw it. The simple approach to fixing it # is just to make sure you override the other # conflicting defaults as well. I would LOVE to see # these go way or be dealt with more elegantly. The # problem isn't there after 2.2. -MDS 2005/02/24 if not need_keepalive_handler: handlers.append( urllib2.HTTPHandler() ) if not need_range_handler: handlers.append( urllib2.FTPHandler() ) # ------------------------------------------------------- if need_keepalive_handler: handlers.append( keepalive_handler ) if need_range_handler: handlers.extend( range_handlers ) handlers.append( auth_handler ) if self.opts.cache_openers: self._opener = CachedOpenerDirector(*handlers) else: self._opener = urllib2.build_opener(*handlers) # OK, I don't like to do this, but otherwise, we end up with # TWO user-agent headers. self._opener.addheaders = [] return self._opener def _do_open(self): opener = self._get_opener() req = urllib2.Request(self.url) # build request object self._add_headers(req) # add misc headers that we need self._build_range(req) # take care of reget and byterange stuff fo, hdr = self._make_request(req, opener) if self.reget_time and self.opts.reget == 'check_timestamp': # do this if we have a local file with known timestamp AND # we're in check_timestamp reget mode. fetch_again = 0 try: modified_tuple = hdr.getdate_tz('last-modified') modified_stamp = rfc822.mktime_tz(modified_tuple) if modified_stamp > self.reget_time: fetch_again = 1 except (TypeError,): fetch_again = 1 if fetch_again: # the server version is newer than the (incomplete) local # version, so we should abandon the version we're getting # and fetch the whole thing again. fo.close() self.opts.reget = None del req.headers['Range'] self._build_range(req) fo, hdr = self._make_request(req, opener) (scheme, host, path, parm, query, frag) = urlparse.urlparse(self.url) if not (self.opts.progress_obj or self.opts.raw_throttle() \ or self.opts.timeout): # if we're not using the progress_obj, throttling, or timeout # we can get a performance boost by going directly to # the underlying fileobject for reads. self.read = fo.read if hasattr(fo, 'readline'): self.readline = fo.readline elif self.opts.progress_obj: try: length = int(hdr['Content-Length']) except: length = None self.opts.progress_obj.start(str(self.filename), self.url, os.path.basename(path), length, text=self.opts.text) self.opts.progress_obj.update(0) (self.fo, self.hdr) = (fo, hdr) def _add_headers(self, req): if self.opts.user_agent: req.add_header('User-agent', self.opts.user_agent) try: req_type = req.get_type() except ValueError: req_type = None if self.opts.http_headers and req_type in ('http', 'https'): for h, v in self.opts.http_headers: req.add_header(h, v) if self.opts.ftp_headers and req_type == 'ftp': for h, v in self.opts.ftp_headers: req.add_header(h, v) def _build_range(self, req): self.reget_time = None self.append = 0 reget_length = 0 rt = None if have_range and self.opts.reget and type(self.filename) == type(''): # we have reget turned on and we're dumping to a file try: s = os.stat(self.filename) except OSError: pass else: self.reget_time = s[ST_MTIME] reget_length = s[ST_SIZE] rt = reget_length, '' self.append = 1 if self.opts.range: if not have_range: raise URLGrabError(10, _('Byte range requested but range '\ 'support unavailable')) rt = self.opts.range if rt[0]: rt = (rt[0] + reget_length, rt[1]) if rt: header = range_tuple_to_header(rt) if header: req.add_header('Range', header) def _make_request(self, req, opener): try: if have_socket_timeout and self.opts.timeout: old_to = socket.getdefaulttimeout() socket.setdefaulttimeout(self.opts.timeout) try: fo = opener.open(req) finally: socket.setdefaulttimeout(old_to) else: fo = opener.open(req) hdr = fo.info() except ValueError, e: raise URLGrabError(1, _('Bad URL: %s') % (e, )) except RangeError, e: raise URLGrabError(9, _('%s') % (e, )) except IOError, e: if hasattr(e, 'reason') and have_socket_timeout and \ isinstance(e.reason, TimeoutError): raise URLGrabError(12, _('Timeout: %s') % (e, )) else: raise URLGrabError(4, _('IOError: %s') % (e, )) except OSError, e: raise URLGrabError(5, _('OSError: %s') % (e, )) except HTTPException, e: raise URLGrabError(7, _('HTTP Error (%s): %s') % \ (e.__class__.__name__, e)) else: return (fo, hdr) def _do_grab(self): """dump the file to self.filename.""" if self.append: new_fo = open(self.filename, 'ab') else: new_fo = open(self.filename, 'wb') bs = 1024*8 size = 0 block = self.read(bs) size = size + len(block) while block: new_fo.write(block) block = self.read(bs) size = size + len(block) new_fo.close() try: modified_tuple = self.hdr.getdate_tz('last-modified') modified_stamp = rfc822.mktime_tz(modified_tuple) os.utime(self.filename, (modified_stamp, modified_stamp)) except (TypeError,), e: pass return size def _fill_buffer(self, amt=None): """fill the buffer to contain at least 'amt' bytes by reading from the underlying file object. If amt is None, then it will read until it gets nothing more. It updates the progress meter and throttles after every self._rbufsize bytes.""" # the _rbuf test is only in this first 'if' for speed. It's not # logically necessary if self._rbuf and not amt is None: L = len(self._rbuf) if amt > L: amt = amt - L else: return # if we've made it here, then we don't have enough in the buffer # and we need to read more. buf = [self._rbuf] bufsize = len(self._rbuf) while amt is None or amt: # first, delay if necessary for throttling reasons if self.opts.raw_throttle(): diff = self._tsize/self.opts.raw_throttle() - \ (time.time() - self._ttime) if diff > 0: time.sleep(diff) self._ttime = time.time() # now read some data, up to self._rbufsize if amt is None: readamount = self._rbufsize else: readamount = min(amt, self._rbufsize) try: new = self.fo.read(readamount) except socket.error, e: raise URLGrabError(4, _('Socket Error: %s') % (e, )) except TimeoutError, e: raise URLGrabError(12, _('Timeout: %s') % (e, )) newsize = len(new) if not newsize: break # no more to read if amt: amt = amt - newsize buf.append(new) bufsize = bufsize + newsize self._tsize = newsize self._amount_read = self._amount_read + newsize if self.opts.progress_obj: self.opts.progress_obj.update(self._amount_read) self._rbuf = string.join(buf, '') return def read(self, amt=None): self._fill_buffer(amt) if amt is None: s, self._rbuf = self._rbuf, '' else: s, self._rbuf = self._rbuf[:amt], self._rbuf[amt:] return s def readline(self, limit=-1): i = string.find(self._rbuf, '\n') while i < 0 and not (0 < limit <= len(self._rbuf)): L = len(self._rbuf) self._fill_buffer(L + self._rbufsize) if not len(self._rbuf) > L: break i = string.find(self._rbuf, '\n', L) if i < 0: i = len(self._rbuf) else: i = i+1 if 0 <= limit < len(self._rbuf): i = limit s, self._rbuf = self._rbuf[:i], self._rbuf[i:] return s def close(self): if self.opts.progress_obj: self.opts.progress_obj.end(self._amount_read) self.fo.close() if self.opts.close_connection: try: self.fo.close_connection() except: pass _handler_cache = [] def CachedOpenerDirector(*handlers): for (cached_handlers, opener) in _handler_cache: if cached_handlers == handlers: for handler in opener.handlers: handler.add_parent(opener) return opener opener = urllib2.build_opener(*handlers) _handler_cache.append( (handlers, opener) ) return opener _proxy_cache = [] def CachedProxyHandler(proxies): for (pdict, handler) in _proxy_cache: if pdict == proxies: break else: handler = urllib2.ProxyHandler(proxies) _proxy_cache.append( (proxies, handler) ) return handler ##################################################################### # DEPRECATED FUNCTIONS def set_throttle(new_throttle): """Deprecated. Use: default_grabber.throttle = new_throttle""" default_grabber.throttle = new_throttle def set_bandwidth(new_bandwidth): """Deprecated. Use: default_grabber.bandwidth = new_bandwidth""" default_grabber.bandwidth = new_bandwidth def set_progress_obj(new_progress_obj): """Deprecated. Use: default_grabber.progress_obj = new_progress_obj""" default_grabber.progress_obj = new_progress_obj def set_user_agent(new_user_agent): """Deprecated. Use: default_grabber.user_agent = new_user_agent""" default_grabber.user_agent = new_user_agent def retrygrab(url, filename=None, copy_local=0, close_connection=0, progress_obj=None, throttle=None, bandwidth=None, numtries=3, retrycodes=[-1,2,4,5,6,7], checkfunc=None): """Deprecated. Use: urlgrab() with the retry arg instead""" kwargs = {'copy_local' : copy_local, 'close_connection' : close_connection, 'progress_obj' : progress_obj, 'throttle' : throttle, 'bandwidth' : bandwidth, 'retry' : numtries, 'retrycodes' : retrycodes, 'checkfunc' : checkfunc } return urlgrab(url, filename, **kwargs) ##################################################################### # TESTING def _main_test(): import sys try: url, filename = sys.argv[1:3] except ValueError: print 'usage:', sys.argv[0], \ ' [copy_local=0|1] [close_connection=0|1]' sys.exit() kwargs = {} for a in sys.argv[3:]: k, v = string.split(a, '=', 1) kwargs[k] = int(v) set_throttle(1.0) set_bandwidth(32 * 1024) print "throttle: %s, throttle bandwidth: %s B/s" % (default_grabber.throttle, default_grabber.bandwidth) try: from progress import text_progress_meter except ImportError, e: pass else: kwargs['progress_obj'] = text_progress_meter() try: name = apply(urlgrab, (url, filename), kwargs) except URLGrabError, e: print e else: print 'LOCAL FILE:', name def _retry_test(): import sys try: url, filename = sys.argv[1:3] except ValueError: print 'usage:', sys.argv[0], \ ' [copy_local=0|1] [close_connection=0|1]' sys.exit() kwargs = {} for a in sys.argv[3:]: k, v = string.split(a, '=', 1) kwargs[k] = int(v) try: from progress import text_progress_meter except ImportError, e: pass else: kwargs['progress_obj'] = text_progress_meter() global DEBUG #DEBUG = 1 def cfunc(filename, hello, there='foo'): print hello, there import random rnum = random.random() if rnum < .5: print 'forcing retry' raise URLGrabError(-1, 'forcing retry') if rnum < .75: print 'forcing failure' raise URLGrabError(-2, 'forcing immediate failure') print 'success' return close_all() kwargs['checkfunc'] = (cfunc, ('hello',), {'there':'there'}) try: name = apply(retrygrab, (url, filename), kwargs) except URLGrabError, e: print e else: print 'LOCAL FILE:', name def _file_object_test(filename=None): import random, cStringIO, sys if filename is None: filename = __file__ print 'using file "%s" for comparisons' % filename fo = open(filename) s_input = fo.read() fo.close() for testfunc in [_test_file_object_smallread, _test_file_object_readall, _test_file_object_readline, _test_file_object_readlines]: fo_input = cStringIO.StringIO(s_input) fo_output = cStringIO.StringIO() wrapper = URLGrabberFileObject(fo_input, None, 0) print 'testing %-30s ' % testfunc.__name__, testfunc(wrapper, fo_output) s_output = fo_output.getvalue() if s_output == s_input: print 'passed' else: print 'FAILED' def _test_file_object_smallread(wrapper, fo_output): while 1: s = wrapper.read(23) fo_output.write(s) if not s: return def _test_file_object_readall(wrapper, fo_output): s = wrapper.read() fo_output.write(s) def _test_file_object_readline(wrapper, fo_output): while 1: s = wrapper.readline() fo_output.write(s) if not s: return def _test_file_object_readlines(wrapper, fo_output): li = wrapper.readlines() fo_output.write(string.join(li, '')) if __name__ == '__main__': _main_test() _retry_test() _file_object_test('test') M 644 inline urlgrabber/keepalive.py data 20244 # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the # Free Software Foundation, Inc., # 59 Temple Place, Suite 330, # Boston, MA 02111-1307 USA # This file is part of urlgrabber, a high-level cross-protocol url-grabber # Copyright 2002-2004 Michael D. Stenner, Ryan Tomayko """An HTTP handler for urllib2 that supports HTTP 1.1 and keepalive. >>> import urllib2 >>> from keepalive import HTTPHandler >>> keepalive_handler = HTTPHandler() >>> opener = urllib2.build_opener(keepalive_handler) >>> urllib2.install_opener(opener) >>> >>> fo = urllib2.urlopen('http://www.python.org') If a connection to a given host is requested, and all of the existing connections are still in use, another connection will be opened. If the handler tries to use an existing connection but it fails in some way, it will be closed and removed from the pool. To remove the handler, simply re-run build_opener with no arguments, and install that opener. You can explicitly close connections by using the close_connection() method of the returned file-like object (described below) or you can use the handler methods: close_connection(host) close_all() open_connections() NOTE: using the close_connection and close_all methods of the handler should be done with care when using multiple threads. * there is nothing that prevents another thread from creating new connections immediately after connections are closed * no checks are done to prevent in-use connections from being closed >>> keepalive_handler.close_all() EXTRA ATTRIBUTES AND METHODS Upon a status of 200, the object returned has a few additional attributes and methods, which should not be used if you want to remain consistent with the normal urllib2-returned objects: close_connection() - close the connection to the host readlines() - you know, readlines() status - the return status (ie 404) reason - english translation of status (ie 'File not found') If you want the best of both worlds, use this inside an AttributeError-catching try: >>> try: status = fo.status >>> except AttributeError: status = None Unfortunately, these are ONLY there if status == 200, so it's not easy to distinguish between non-200 responses. The reason is that urllib2 tries to do clever things with error codes 301, 302, 401, and 407, and it wraps the object upon return. For python versions earlier than 2.4, you can avoid this fancy error handling by setting the module-level global HANDLE_ERRORS to zero. You see, prior to 2.4, it's the HTTP Handler's job to determine what to handle specially, and what to just pass up. HANDLE_ERRORS == 0 means "pass everything up". In python 2.4, however, this job no longer belongs to the HTTP Handler and is now done by a NEW handler, HTTPErrorProcessor. Here's the bottom line: python version < 2.4 HANDLE_ERRORS == 1 (default) pass up 200, treat the rest as errors HANDLE_ERRORS == 0 pass everything up, error processing is left to the calling code python version >= 2.4 HANDLE_ERRORS == 1 pass up 200, treat the rest as errors HANDLE_ERRORS == 0 (default) pass everything up, let the other handlers (specifically, HTTPErrorProcessor) decide what to do In practice, setting the variable either way makes little difference in python 2.4, so for the most consistent behavior across versions, you probably just want to use the defaults, which will give you exceptions on errors. """ # $Id: keepalive.py,v 1.9 2005/02/14 21:55:07 mstenner Exp $ import urllib2 import httplib import socket import thread DEBUG = 0 def DBPRINT(*args): print ' '.join(args) import sys _python_version = map(int, sys.version.split()[0].split('.')) if _python_version < [2, 4]: HANDLE_ERRORS = 1 else: HANDLE_ERRORS = 0 class ConnectionManager: """ The connection manager must be able to: * keep track of all existing """ def __init__(self): self._lock = thread.allocate_lock() self._hostmap = {} # map hosts to a list of connections self._connmap = {} # map connections to host self._readymap = {} # map connection to ready state def add(self, host, connection, ready): self._lock.acquire() try: if not self._hostmap.has_key(host): self._hostmap[host] = [] self._hostmap[host].append(connection) self._connmap[connection] = host self._readymap[connection] = ready finally: self._lock.release() def remove(self, connection): self._lock.acquire() try: try: host = self._connmap[connection] except KeyError: pass else: del self._connmap[connection] del self._readymap[connection] self._hostmap[host].remove(connection) if not self._hostmap[host]: del self._hostmap[host] finally: self._lock.release() def set_ready(self, connection, ready): try: self._readymap[connection] = ready except KeyError: pass def get_ready_conn(self, host): conn = None self._lock.acquire() try: if self._hostmap.has_key(host): for c in self._hostmap[host]: if self._readymap[c]: self._readymap[c] = 0 conn = c break finally: self._lock.release() return conn def get_all(self, host=None): if host: return list(self._hostmap.get(host, [])) else: return dict(self._hostmap) class HTTPHandler(urllib2.HTTPHandler): def __init__(self): self._cm = ConnectionManager() #### Connection Management def open_connections(self): """return a list of connected hosts and the number of connections to each. [('foo.com:80', 2), ('bar.org', 1)]""" return [(host, len(li)) for (host, li) in self._cm.get_all().items()] def close_connection(self, host): """close connection(s) to host is the host:port spec, as in 'www.cnn.com:8080' as passed in. no error occurs if there is no connection to that host.""" for h in self._cm.get_all(host): self._cm.remove(h) h.close() def close_all(self): """close all open connections""" for host, conns in self._cm.get_all().items(): for h in conns: self._cm.remove(h) h.close() def _request_closed(self, request, host, connection): """tells us that this request is now closed and the the connection is ready for another request""" self._cm.set_ready(connection, 1) def _remove_connection(self, host, connection, close=0): if close: connection.close() self._cm.remove(connection) #### Transaction Execution def http_open(self, req): return self.do_open(HTTPConnection, req) def do_open(self, http_class, req): host = req.get_host() if not host: raise urllib2.URLError('no host given') try: h = self._cm.get_ready_conn(host) while h: r = self._reuse_connection(h, req, host) # if this response is non-None, then it worked and we're # done. Break out, skipping the else block. if r: break # connection is bad - possibly closed by server # discard it and ask for the next free connection h.close() self._cm.remove(h) h = self._cm.get_ready_conn(host) else: # no (working) free connections were found. Create a new one. h = http_class(host) if DEBUG: DBPRINT("creating new connection to %s (%d)" % \ (host, id(h))) self._cm.add(host, h, 0) self._start_transaction(h, req) r = h.getresponse() except (socket.error, httplib.HTTPException), err: raise urllib2.URLError(err) # if not a persistent connection, don't try to reuse it if r.will_close: self._cm.remove(h) if DEBUG: DBPRINT("STATUS: %s, %s" % (r.status, r.reason)) r._handler = self r._host = host r._url = req.get_full_url() r._connection = h r.code = r.status if r.status == 200 or not HANDLE_ERRORS: return r else: return self.parent.error('http', req, r, r.status, r.reason, r.msg) def _reuse_connection(self, h, req, host): """start the transaction with a re-used connection return a response object (r) upon success or None on failure. This DOES not close or remove bad connections in cases where it returns. However, if an unexpected exception occurs, it will close and remove the connection before re-raising. """ try: self._start_transaction(h, req) r = h.getresponse() # note: just because we got something back doesn't mean it # worked. We'll check the version below, too. except (socket.error, httplib.HTTPException): r = None except: # adding this block just in case we've missed # something we will still raise the exception, but # lets try and close the connection and remove it # first. We previously got into a nasty loop # where an exception was uncaught, and so the # connection stayed open. On the next try, the # same exception was raised, etc. The tradeoff is # that it's now possible this call will raise # a DIFFERENT exception if DEBUG: DBPRINT("unexpected exception - " \ "closing connection to %s (%d)" % (host, id(h))) self._cm.remove(h) h.close() raise if r is None or r.version == 9: # httplib falls back to assuming HTTP 0.9 if it gets a # bad header back. This is most likely to happen if # the socket has been closed by the server since we # last used the connection. if DEBUG: DBPRINT("failed to re-use connection to %s (%d)" \ % (host, id(h))) r = None else: if DEBUG: DBPRINT("re-using connection to %s (%d)" % (host, id(h))) return r def _start_transaction(self, h, req): try: if req.has_data(): data = req.get_data() h.putrequest('POST', req.get_selector()) if not req.headers.has_key('Content-type'): h.putheader('Content-type', 'application/x-www-form-urlencoded') if not req.headers.has_key('Content-length'): h.putheader('Content-length', '%d' % len(data)) else: h.putrequest('GET', req.get_selector()) except (socket.error, httplib.HTTPException), err: raise urllib2.URLError(err) for args in self.parent.addheaders: h.putheader(*args) for k, v in req.headers.items(): h.putheader(k, v) h.endheaders() if req.has_data(): h.send(data) class HTTPResponse(httplib.HTTPResponse): # we need to subclass HTTPResponse in order to # 1) add readline() and readlines() methods # 2) add close_connection() methods # 3) add info() and geturl() methods # in order to add readline(), read must be modified to deal with a # buffer. example: readline must read a buffer and then spit back # one line at a time. The only real alternative is to read one # BYTE at a time (ick). Once something has been read, it can't be # put back (ok, maybe it can, but that's even uglier than this), # so if you THEN do a normal read, you must first take stuff from # the buffer. # the read method wraps the original to accomodate buffering, # although read() never adds to the buffer. # Both readline and readlines have been stolen with almost no # modification from socket.py def __init__(self, sock, debuglevel=0, strict=0, method=None): if method: # the httplib in python 2.3 uses the method arg httplib.HTTPResponse.__init__(self, sock, debuglevel, method) else: # 2.2 doesn't httplib.HTTPResponse.__init__(self, sock, debuglevel) self.fileno = sock.fileno self.code = None self._rbuf = '' self._rbufsize = 8096 self._handler = None # inserted by the handler later self._host = None # (same) self._url = None # (same) self._connection = None # (same) _raw_read = httplib.HTTPResponse.read def close(self): if self.fp: self.fp.close() self.fp = None if self._handler: self._handler._request_closed(self, self._host, self._connection) def close_connection(self): self._handler._remove_connection(self._host, self._connection, close=1) self.close() def info(self): return self.msg def geturl(self): return self._url def read(self, amt=None): # the _rbuf test is only in this first if for speed. It's not # logically necessary if self._rbuf and not amt is None: L = len(self._rbuf) if amt > L: amt -= L else: s = self._rbuf[:amt] self._rbuf = self._rbuf[amt:] return s s = self._rbuf + self._raw_read(amt) self._rbuf = '' return s def readline(self, limit=-1): data = "" i = self._rbuf.find('\n') while i < 0 and not (0 < limit <= len(self._rbuf)): new = self._raw_read(self._rbufsize) if not new: break i = new.find('\n') if i >= 0: i = i + len(self._rbuf) self._rbuf = self._rbuf + new if i < 0: i = len(self._rbuf) else: i = i+1 if 0 <= limit < len(self._rbuf): i = limit data, self._rbuf = self._rbuf[:i], self._rbuf[i:] return data def readlines(self, sizehint = 0): total = 0 list = [] while 1: line = self.readline() if not line: break list.append(line) total += len(line) if sizehint and total >= sizehint: break return list class HTTPConnection(httplib.HTTPConnection): # use the modified response class response_class = HTTPResponse ######################################################################### ##### TEST FUNCTIONS ######################################################################### def error_handler(url): global HANDLE_ERRORS orig = HANDLE_ERRORS keepalive_handler = HTTPHandler() opener = urllib2.build_opener(keepalive_handler) urllib2.install_opener(opener) pos = {0: 'off', 1: 'on'} for i in (0, 1): print " fancy error handling %s (HANDLE_ERRORS = %i)" % (pos[i], i) HANDLE_ERRORS = i try: fo = urllib2.urlopen(url) foo = fo.read() fo.close() try: status, reason = fo.status, fo.reason except AttributeError: status, reason = None, None except IOError, e: print " EXCEPTION: %s" % e raise else: print " status = %s, reason = %s" % (status, reason) HANDLE_ERRORS = orig hosts = keepalive_handler.open_connections() print "open connections:", hosts keepalive_handler.close_all() def continuity(url): import md5 format = '%25s: %s' # first fetch the file with the normal http handler opener = urllib2.build_opener() urllib2.install_opener(opener) fo = urllib2.urlopen(url) foo = fo.read() fo.close() m = md5.new(foo) print format % ('normal urllib', m.hexdigest()) # now install the keepalive handler and try again opener = urllib2.build_opener(HTTPHandler()) urllib2.install_opener(opener) fo = urllib2.urlopen(url) foo = fo.read() fo.close() m = md5.new(foo) print format % ('keepalive read', m.hexdigest()) fo = urllib2.urlopen(url) foo = '' while 1: f = fo.readline() if f: foo = foo + f else: break fo.close() m = md5.new(foo) print format % ('keepalive readline', m.hexdigest()) def comp(N, url): print ' making %i connections to:\n %s' % (N, url) sys.stdout.write(' first using the normal urllib handlers') # first use normal opener opener = urllib2.build_opener() urllib2.install_opener(opener) t1 = fetch(N, url) print ' TIME: %.3f s' % t1 sys.stdout.write(' now using the keepalive handler ') # now install the keepalive handler and try again opener = urllib2.build_opener(HTTPHandler()) urllib2.install_opener(opener) t2 = fetch(N, url) print ' TIME: %.3f s' % t2 print ' improvement factor: %.2f' % (t1/t2, ) def fetch(N, url, delay=0): lens = [] starttime = time.time() for i in range(N): if delay and i > 0: time.sleep(delay) fo = urllib2.urlopen(url) foo = fo.read() fo.close() lens.append(len(foo)) diff = time.time() - starttime j = 0 for i in lens[1:]: j = j + 1 if not i == lens[0]: print "WARNING: inconsistent length on read %i: %i" % (j, i) return diff def test_timeout(url): global DEBUG, DBPRINT dbp = DBPRINT def DBPRINT(*args): print ' ' + ' '.join(args) DEBUG=1 print " fetching the file to establish a connection" fo = urllib2.urlopen(url) data1 = fo.read() fo.close() i = 20 print " waiting %i seconds for the server to close the connection" % i while i > 0: sys.stdout.write('\r %2i' % i) sys.stdout.flush() time.sleep(1) i -= 1 sys.stderr.write('\r') print " fetching the file a second time" fo = urllib2.urlopen(url) data2 = fo.read() fo.close() if data1 == data2: print ' data are identical' else: print ' ERROR: DATA DIFFER' DEBUG=0 DBPRINT = dbp def test(url, N=10): print "checking error hander (do this on a non-200)" try: error_handler(url) except IOError, e: print "exiting - exception will prevent further tests" sys.exit() print print "performing continuity test (making sure stuff isn't corrupted)" continuity(url) print print "performing speed comparison" comp(N, url) print print "performing dropped-connection check" test_timeout(url) if __name__ == '__main__': import time import sys try: N = int(sys.argv[1]) url = sys.argv[2] except: print "%s " % sys.argv[0] else: test(url, N) M 644 inline urlgrabber/mirror.py data 18117 # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the # Free Software Foundation, Inc., # 59 Temple Place, Suite 330, # Boston, MA 02111-1307 USA # This file is part of urlgrabber, a high-level cross-protocol url-grabber # Copyright 2002-2004 Michael D. Stenner, Ryan Tomayko """Module for downloading files from a pool of mirrors DESCRIPTION This module provides support for downloading files from a pool of mirrors with configurable failover policies. To a large extent, the failover policy is chosen by using different classes derived from the main class, MirrorGroup. Instances of MirrorGroup (and cousins) act very much like URLGrabber instances in that they have urlread, urlgrab, and urlopen methods. They can therefore, be used in very similar ways. from urlgrabber.grabber import URLGrabber from urlgrabber.mirror import MirrorGroup gr = URLGrabber() mg = MirrorGroup(gr, ['http://foo.com/some/directory/', 'http://bar.org/maybe/somewhere/else/', 'ftp://baz.net/some/other/place/entirely/'] mg.urlgrab('relative/path.zip') The assumption is that all mirrors are identical AFTER the base urls specified, so that any mirror can be used to fetch any file. FAILOVER The failover mechanism is designed to be customized by subclassing from MirrorGroup to change the details of the behavior. In general, the classes maintain a master mirror list and a "current mirror" index. When a download is initiated, a copy of this list and index is created for that download only. The specific failover policy depends on the class used, and so is documented in the class documentation. Note that ANY behavior of the class can be overridden, so any failover policy at all is possible (although you may need to change the interface in extreme cases). CUSTOMIZATION Most customization of a MirrorGroup object is done at instantiation time (or via subclassing). There are four major types of customization: 1) Pass in a custom urlgrabber - The passed in urlgrabber will be used (by default... see #2) for the grabs, so options to it apply for the url-fetching 2) Custom mirror list - Mirror lists can simply be a list of stings mirrors (as shown in the example above) but each can also be a dict, allowing for more options. For example, the first mirror in the list above could also have been: {'mirror': 'http://foo.com/some/directory/', 'grabber': , 'kwargs': { }} All mirrors are converted to this format internally. If 'grabber' is omitted, the default grabber will be used. If kwargs are omitted, then (duh) they will not be used. 3) Pass keyword arguments when instantiating the mirror group. See, for example, the failure_callback argument. 4) Finally, any kwargs passed in for the specific file (to the urlgrab method, for example) will be folded in. The options passed into the grabber's urlXXX methods will override any options specified in a custom mirror dict. """ # $Id: mirror.py,v 1.12 2004/09/07 21:19:54 mstenner Exp $ import random import thread # needed for locking to make this threadsafe from grabber import URLGrabError, CallbackObject DEBUG=0 def DBPRINT(*args): print ' '.join(args) try: from i18n import _ except ImportError, msg: def _(st): return st class GrabRequest: """This is a dummy class used to hold information about the specific request. For example, a single file. By maintaining this information separately, we can accomplish two things: 1) make it a little easier to be threadsafe 2) have request-specific parameters """ pass class MirrorGroup: """Base Mirror class Instances of this class are built with a grabber object and a list of mirrors. Then all calls to urlXXX should be passed relative urls. The requested file will be searched for on the first mirror. If the grabber raises an exception (possibly after some retries) then that mirror will be removed from the list, and the next will be attempted. If all mirrors are exhausted, then an exception will be raised. MirrorGroup has the following failover policy: * downloads begin with the first mirror * by default (see default_action below) a failure (after retries) causes it to increment the local AND master indices. Also, the current mirror is removed from the local list (but NOT the master list - the mirror can potentially be used for other files) * if the local list is ever exhausted, a URLGrabError will be raised (errno=256, no more mirrors) OPTIONS In addition to the required arguments "grabber" and "mirrors", MirrorGroup also takes the following optional arguments: default_action A dict that describes the actions to be taken upon failure (after retries). default_action can contain any of the following keys (shown here with their default values): default_action = {'increment': 1, 'increment_master': 1, 'remove': 1, 'remove_master': 0, 'fail': 0} In this context, 'increment' means "use the next mirror" and 'remove' means "never use this mirror again". The two 'master' values refer to the instance-level mirror list (used for all files), whereas the non-master values refer to the current download only. The 'fail' option will cause immediate failure by re-raising the exception and no further attempts to get the current download. This dict can be set at instantiation time, mg = MirrorGroup(grabber, mirrors, default_action={'fail':1}) at method-execution time (only applies to current fetch), filename = mg.urlgrab(url, default_action={'increment': 0}) or by returning an action dict from the failure_callback return {'fail':0} in increasing precedence. If all three of these were done, the net result would be: {'increment': 0, # set in method 'increment_master': 1, # class default 'remove': 1, # class default 'remove_master': 0, # class default 'fail': 0} # set at instantiation, reset # from callback failure_callback this is a callback that will be called when a mirror "fails", meaning the grabber raises some URLGrabError. If this is a tuple, it is interpreted to be of the form (cb, args, kwargs) where cb is the actual callable object (function, method, etc). Otherwise, it is assumed to be the callable object itself. The callback will be passed a grabber.CallbackObject instance along with args and kwargs (if present). The following attributes are defined withing the instance: obj.exception = < exception that was raised > obj.mirror = < the mirror that was tried > obj.relative_url = < url relative to the mirror > obj.url = < full url that failed > # .url is just the combination of .mirror # and .relative_url The failure callback can return an action dict, as described above. Like default_action, the failure_callback can be set at instantiation time or when the urlXXX method is called. In the latter case, it applies only for that fetch. The callback can re-raise the exception quite easily. For example, this is a perfectly adequate callback function: def callback(obj): raise obj.exception WARNING: do not save the exception object (or the CallbackObject instance). As they contain stack frame references, they can lead to circular references. Notes: * The behavior can be customized by deriving and overriding the 'CONFIGURATION METHODS' * The 'grabber' instance is kept as a reference, not copied. Therefore, the grabber instance can be modified externally and changes will take effect immediately. """ # notes on thread-safety: # A GrabRequest should never be shared by multiple threads because # it's never saved inside the MG object and never returned outside it. # therefore, it should be safe to access/modify grabrequest data # without a lock. However, accessing the mirrors and _next attributes # of the MG itself must be done when locked to prevent (for example) # removal of the wrong mirror. ############################################################## # CONFIGURATION METHODS - intended to be overridden to # customize behavior def __init__(self, grabber, mirrors, **kwargs): """Initialize the MirrorGroup object. REQUIRED ARGUMENTS grabber - URLGrabber instance mirrors - a list of mirrors OPTIONAL ARGUMENTS failure_callback - callback to be used when a mirror fails default_action - dict of failure actions See the module-level and class level documentation for more details. """ # OVERRIDE IDEAS: # shuffle the list to randomize order self.grabber = grabber self.mirrors = self._parse_mirrors(mirrors) self._next = 0 self._lock = thread.allocate_lock() self.default_action = None self._process_kwargs(kwargs) # if these values are found in **kwargs passed to one of the urlXXX # methods, they will be stripped before getting passed on to the # grabber options = ['default_action', 'failure_callback'] def _process_kwargs(self, kwargs): self.failure_callback = kwargs.get('failure_callback') self.default_action = kwargs.get('default_action') def _parse_mirrors(self, mirrors): parsed_mirrors = [] for m in mirrors: if type(m) == type(''): m = {'mirror': m} parsed_mirrors.append(m) return parsed_mirrors def _load_gr(self, gr): # OVERRIDE IDEAS: # shuffle gr list self._lock.acquire() gr.mirrors = list(self.mirrors) gr._next = self._next self._lock.release() def _get_mirror(self, gr): # OVERRIDE IDEAS: # return a random mirror so that multiple mirrors get used # even without failures. if not gr.mirrors: raise URLGrabError(256, _('No more mirrors to try.')) return gr.mirrors[gr._next] def _failure(self, gr, cb_obj): # OVERRIDE IDEAS: # inspect the error - remove=1 for 404, remove=2 for connection # refused, etc. (this can also be done via # the callback) cb = gr.kw.get('failure_callback') or self.failure_callback if cb: if type(cb) == type( () ): cb, args, kwargs = cb else: args, kwargs = (), {} action = cb(cb_obj, *args, **kwargs) or {} else: action = {} # XXXX - decide - there are two ways to do this # the first is action-overriding as a whole - use the entire action # or fall back on module level defaults #action = action or gr.kw.get('default_action') or self.default_action # the other is to fall through for each element in the action dict a = dict(self.default_action or {}) a.update(gr.kw.get('default_action', {})) a.update(action) action = a self.increment_mirror(gr, action) if action and action.get('fail', 0): raise def increment_mirror(self, gr, action={}): """Tell the mirror object increment the mirror index This increments the mirror index, which amounts to telling the mirror object to use a different mirror (for this and future downloads). This is a SEMI-public method. It will be called internally, and you may never need to call it. However, it is provided (and is made public) so that the calling program can increment the mirror choice for methods like urlopen. For example, with urlopen, there's no good way for the mirror group to know that an error occurs mid-download (it's already returned and given you the file object). remove --- can have several values 0 do not remove the mirror from the list 1 remove the mirror for this download only 2 remove the mirror permanently beware of remove=0 as it can lead to infinite loops """ badmirror = gr.mirrors[gr._next] self._lock.acquire() try: ind = self.mirrors.index(badmirror) except ValueError: pass else: if action.get('remove_master', 0): del self.mirrors[ind] elif self._next == ind and action.get('increment_master', 1): self._next += 1 if self._next >= len(self.mirrors): self._next = 0 self._lock.release() if action.get('remove', 1): del gr.mirrors[gr._next] elif action.get('increment', 1): gr._next += 1 if gr._next >= len(gr.mirrors): gr._next = 0 if DEBUG: grm = [m['mirror'] for m in gr.mirrors] DBPRINT('GR mirrors: [%s] %i' % (' '.join(grm), gr._next)) selfm = [m['mirror'] for m in self.mirrors] DBPRINT('MAIN mirrors: [%s] %i' % (' '.join(selfm), self._next)) ##################################################################### # NON-CONFIGURATION METHODS # these methods are designed to be largely workhorse methods that # are not intended to be overridden. That doesn't mean you can't; # if you want to, feel free, but most things can be done by # by overriding the configuration methods :) def _join_url(self, base_url, rel_url): if base_url.endswith('/') or rel_url.startswith('/'): return base_url + rel_url else: return base_url + '/' + rel_url def _mirror_try(self, func, url, kw): gr = GrabRequest() gr.func = func gr.url = url gr.kw = dict(kw) self._load_gr(gr) for k in self.options: try: del kw[k] except KeyError: pass while 1: mirrorchoice = self._get_mirror(gr) fullurl = self._join_url(mirrorchoice['mirror'], gr.url) kwargs = dict(mirrorchoice.get('kwargs', {})) kwargs.update(kw) grabber = mirrorchoice.get('grabber') or self.grabber func_ref = getattr(grabber, func) if DEBUG: DBPRINT('MIRROR: trying %s -> %s' % (url, fullurl)) try: return func_ref( *(fullurl,), **kwargs ) except URLGrabError, e: if DEBUG: DBPRINT('MIRROR: failed') obj = CallbackObject() obj.exception = e obj.mirror = mirrorchoice['mirror'] obj.relative_url = gr.url obj.url = fullurl self._failure(gr, obj) def urlgrab(self, url, filename=None, **kwargs): kw = dict(kwargs) kw['filename'] = filename func = 'urlgrab' return self._mirror_try(func, url, kw) def urlopen(self, url, **kwargs): kw = dict(kwargs) func = 'urlopen' return self._mirror_try(func, url, kw) def urlread(self, url, limit=None, **kwargs): kw = dict(kwargs) kw['limit'] = limit func = 'urlread' return self._mirror_try(func, url, kw) class MGRandomStart(MirrorGroup): """A mirror group that starts at a random mirror in the list. This behavior of this class is identical to MirrorGroup, except that it starts at a random location in the mirror list. """ def __init__(self, grabber, mirrors, **kwargs): """Initialize the object The arguments for intialization are the same as for MirrorGroup """ MirrorGroup.__init__(self, grabber, mirrors, **kwargs) self._next = random.randrange(len(mirrors)) class MGRandomOrder(MirrorGroup): """A mirror group that uses mirrors in a random order. This behavior of this class is identical to MirrorGroup, except that it uses the mirrors in a random order. Note that the order is set at initialization time and fixed thereafter. That is, it does not pick a random mirror after each failure. """ def __init__(self, grabber, mirrors, **kwargs): """Initialize the object The arguments for intialization are the same as for MirrorGroup """ MirrorGroup.__init__(self, grabber, mirrors, **kwargs) random.shuffle(self.mirrors) if __name__ == '__main__': pass M 644 inline urlgrabber/progress.py data 18099 # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the # Free Software Foundation, Inc., # 59 Temple Place, Suite 330, # Boston, MA 02111-1307 USA # This file is part of urlgrabber, a high-level cross-protocol url-grabber # Copyright 2002-2004 Michael D. Stenner, Ryan Tomayko # $Id: progress.py,v 1.5 2005/01/14 18:21:41 rtomayko Exp $ import sys import time import math import thread class BaseMeter: def __init__(self): self.update_period = 0.3 # seconds self.filename = None self.url = None self.basename = None self.text = None self.size = None self.start_time = None self.last_amount_read = 0 self.last_update_time = None self.re = RateEstimator() def start(self, filename=None, url=None, basename=None, size=None, now=None, text=None): self.filename = filename self.url = url self.basename = basename self.text = text #size = None ######### TESTING self.size = size if not size is None: self.fsize = format_number(size) + 'B' if now is None: now = time.time() self.start_time = now self.re.start(size, now) self.last_amount_read = 0 self.last_update_time = now self._do_start(now) def _do_start(self, now=None): pass def update(self, amount_read, now=None): # for a real gui, you probably want to override and put a call # to your mainloop iteration function here if now is None: now = time.time() if (now >= self.last_update_time + self.update_period) or \ not self.last_update_time: self.re.update(amount_read, now) self.last_amount_read = amount_read self.last_update_time = now self._do_update(amount_read, now) def _do_update(self, amount_read, now=None): pass def end(self, amount_read, now=None): if now is None: now = time.time() self.re.update(amount_read, now) self.last_amount_read = amount_read self.last_update_time = now self._do_end(amount_read, now) def _do_end(self, amount_read, now=None): pass class TextMeter(BaseMeter): def __init__(self, fo=sys.stderr): BaseMeter.__init__(self) self.fo = fo def _do_update(self, amount_read, now=None): etime = self.re.elapsed_time() fetime = format_time(etime) fread = format_number(amount_read) #self.size = None if self.text is not None: text = self.text else: text = self.basename if self.size is None: out = '\r%-60.60s %5sB %s ' % \ (text, fread, fetime) else: rtime = self.re.remaining_time() frtime = format_time(rtime) frac = self.re.fraction_read() bar = '='*int(25 * frac) out = '\r%-25.25s %3i%% |%-25.25s| %5sB %8s ETA ' % \ (text, frac*100, bar, fread, frtime) self.fo.write(out) self.fo.flush() def _do_end(self, amount_read, now=None): total_time = format_time(self.re.elapsed_time()) total_size = format_number(amount_read) if self.text is not None: text = self.text else: text = self.basename if self.size is None: out = '\r%-60.60s %5sB %s ' % \ (text, total_size, total_time) else: bar = '='*25 out = '\r%-25.25s %3i%% |%-25.25s| %5sB %8s ' % \ (text, 100, bar, total_size, total_time) self.fo.write(out + '\n') self.fo.flush() text_progress_meter = TextMeter class MultiFileHelper(BaseMeter): def __init__(self, master): BaseMeter.__init__(self) self.master = master def _do_start(self, now): self.master.start_meter(self, now) def _do_update(self, amount_read, now): # elapsed time since last update self.master.update_meter(self, now) def _do_end(self, amount_read, now): self.ftotal_time = format_time(now - self.start_time) self.ftotal_size = format_number(self.last_amount_read) self.master.end_meter(self, now) def failure(self, message, now=None): self.master.failure_meter(self, message, now) def message(self, message): self.master.message_meter(self, message) class MultiFileMeter: helperclass = MultiFileHelper def __init__(self): self.meters = [] self.in_progress_meters = [] self._lock = thread.allocate_lock() self.update_period = 0.3 # seconds self.numfiles = None self.finished_files = 0 self.failed_files = 0 self.open_files = 0 self.total_size = None self.failed_size = 0 self.start_time = None self.finished_file_size = 0 self.last_update_time = None self.re = RateEstimator() def start(self, numfiles=None, total_size=None, now=None): if now is None: now = time.time() self.numfiles = numfiles self.finished_files = 0 self.failed_files = 0 self.open_files = 0 self.total_size = total_size self.failed_size = 0 self.start_time = now self.finished_file_size = 0 self.last_update_time = now self.re.start(total_size, now) self._do_start(now) def _do_start(self, now): pass def end(self, now=None): if now is None: now = time.time() self._do_end(now) def _do_end(self, now): pass def lock(self): self._lock.acquire() def unlock(self): self._lock.release() ########################################################### # child meter creation and destruction def newMeter(self): newmeter = self.helperclass(self) self.meters.append(newmeter) return newmeter def removeMeter(self, meter): self.meters.remove(meter) ########################################################### # child functions - these should only be called by helpers def start_meter(self, meter, now): if not meter in self.meters: raise ValueError('attempt to use orphaned meter') self._lock.acquire() try: if not meter in self.in_progress_meters: self.in_progress_meters.append(meter) self.open_files += 1 finally: self._lock.release() self._do_start_meter(meter, now) def _do_start_meter(self, meter, now): pass def update_meter(self, meter, now): if not meter in self.meters: raise ValueError('attempt to use orphaned meter') if (now >= self.last_update_time + self.update_period) or \ not self.last_update_time: self.re.update(self._amount_read(), now) self.last_update_time = now self._do_update_meter(meter, now) def _do_update_meter(self, meter, now): pass def end_meter(self, meter, now): if not meter in self.meters: raise ValueError('attempt to use orphaned meter') self._lock.acquire() try: try: self.in_progress_meters.remove(meter) except ValueError: pass self.open_files -= 1 self.finished_files += 1 self.finished_file_size += meter.last_amount_read finally: self._lock.release() self._do_end_meter(meter, now) def _do_end_meter(self, meter, now): pass def failure_meter(self, meter, message, now): if not meter in self.meters: raise ValueError('attempt to use orphaned meter') self._lock.acquire() try: try: self.in_progress_meters.remove(meter) except ValueError: pass self.open_files -= 1 self.failed_files += 1 if meter.size and self.failed_size is not None: self.failed_size += meter.size else: self.failed_size = None finally: self._lock.release() self._do_failure_meter(meter, message, now) def _do_failure_meter(self, meter, message, now): pass def message_meter(self, meter, message): pass ######################################################## # internal functions def _amount_read(self): tot = self.finished_file_size for m in self.in_progress_meters: tot += m.last_amount_read return tot class TextMultiFileMeter(MultiFileMeter): def __init__(self, fo=sys.stderr): self.fo = fo MultiFileMeter.__init__(self) # files: ###/### ###% data: ######/###### ###% time: ##:##:##/##:##:## def _do_update_meter(self, meter, now): self._lock.acquire() try: format = "files: %3i/%-3i %3i%% data: %6.6s/%-6.6s %3i%% " \ "time: %8.8s/%8.8s" df = self.finished_files tf = self.numfiles or 1 pf = 100 * float(df)/tf + 0.49 dd = self.re.last_amount_read td = self.total_size pd = 100 * (self.re.fraction_read() or 0) + 0.49 dt = self.re.elapsed_time() rt = self.re.remaining_time() if rt is None: tt = None else: tt = dt + rt fdd = format_number(dd) + 'B' ftd = format_number(td) + 'B' fdt = format_time(dt, 1) ftt = format_time(tt, 1) out = '%-79.79s' % (format % (df, tf, pf, fdd, ftd, pd, fdt, ftt)) self.fo.write('\r' + out) self.fo.flush() finally: self._lock.release() def _do_end_meter(self, meter, now): self._lock.acquire() try: format = "%-30.30s %6.6s %8.8s %9.9s" fn = meter.basename size = meter.last_amount_read fsize = format_number(size) + 'B' et = meter.re.elapsed_time() fet = format_time(et, 1) frate = format_number(size / et) + 'B/s' out = '%-79.79s' % (format % (fn, fsize, fet, frate)) self.fo.write('\r' + out + '\n') finally: self._lock.release() self._do_update_meter(meter, now) def _do_failure_meter(self, meter, message, now): self._lock.acquire() try: format = "%-30.30s %6.6s %s" fn = meter.basename if type(message) in (type(''), type(u'')): message = message.splitlines() if not message: message = [''] out = '%-79s' % (format % (fn, 'FAILED', message[0] or '')) self.fo.write('\r' + out + '\n') for m in message[1:]: self.fo.write(' ' + m + '\n') self._lock.release() finally: self._do_update_meter(meter, now) def message_meter(self, meter, message): self._lock.acquire() try: pass finally: self._lock.release() def _do_end(self, now): self._do_update_meter(None, now) self._lock.acquire() try: self.fo.write('\n') self.fo.flush() finally: self._lock.release() ###################################################################### # support classes and functions class RateEstimator: def __init__(self, timescale=5.0): self.timescale = timescale def start(self, total=None, now=None): if now is None: now = time.time() self.total = total self.start_time = now self.last_update_time = now self.last_amount_read = 0 self.ave_rate = None def update(self, amount_read, now=None): if now is None: now = time.time() if amount_read == 0: # if we just started this file, all bets are off self.last_update_time = now self.last_amount_read = 0 self.ave_rate = None return #print 'times', now, self.last_update_time time_diff = now - self.last_update_time read_diff = amount_read - self.last_amount_read self.last_update_time = now self.last_amount_read = amount_read self.ave_rate = self._temporal_rolling_ave(\ time_diff, read_diff, self.ave_rate, self.timescale) #print 'results', time_diff, read_diff, self.ave_rate ##################################################################### # result methods def average_rate(self): "get the average transfer rate (in bytes/second)" return self.ave_rate def elapsed_time(self): "the time between the start of the transfer and the most recent update" return self.last_update_time - self.start_time def remaining_time(self): "estimated time remaining" if not self.ave_rate or not self.total: return None return (self.total - self.last_amount_read) / self.ave_rate def fraction_read(self): """the fraction of the data that has been read (can be None for unknown transfer size)""" if self.total is None: return None elif self.total == 0: return 1.0 else: return float(self.last_amount_read)/self.total ######################################################################### # support methods def _temporal_rolling_ave(self, time_diff, read_diff, last_ave, timescale): """a temporal rolling average performs smooth averaging even when updates come at irregular intervals. This is performed by scaling the "epsilon" according to the time since the last update. Specifically, epsilon = time_diff / timescale As a general rule, the average will take on a completely new value after 'timescale' seconds.""" epsilon = time_diff / timescale if epsilon > 1: epsilon = 1.0 return self._rolling_ave(time_diff, read_diff, last_ave, epsilon) def _rolling_ave(self, time_diff, read_diff, last_ave, epsilon): """perform a "rolling average" iteration a rolling average "folds" new data into an existing average with some weight, epsilon. epsilon must be between 0.0 and 1.0 (inclusive) a value of 0.0 means only the old value (initial value) counts, and a value of 1.0 means only the newest value is considered.""" try: recent_rate = read_diff / time_diff except ZeroDivisionError: recent_rate = None if last_ave is None: return recent_rate elif recent_rate is None: return last_ave # at this point, both last_ave and recent_rate are numbers return epsilon * recent_rate + (1 - epsilon) * last_ave def _round_remaining_time(self, rt, start_time=15.0): """round the remaining time, depending on its size If rt is between n*start_time and (n+1)*start_time round downward to the nearest multiple of n (for any counting number n). If rt < start_time, round down to the nearest 1. For example (for start_time = 15.0): 2.7 -> 2.0 25.2 -> 25.0 26.4 -> 26.0 35.3 -> 34.0 63.6 -> 60.0 """ if rt < 0: return 0.0 shift = int(math.log(rt/start_time)/math.log(2)) rt = int(rt) if shift <= 0: return rt return float(int(rt) >> shift << shift) def format_time(seconds, use_hours=0): if seconds is None or seconds < 0: if use_hours: return '--:--:--' else: return '--:--' else: seconds = int(seconds) minutes = seconds / 60 seconds = seconds % 60 if use_hours: hours = minutes / 60 minutes = minutes % 60 return '%02i:%02i:%02i' % (hours, minutes, seconds) else: return '%02i:%02i' % (minutes, seconds) def format_number(number, SI=0, space=' '): """Turn numbers into human-readable metric-like numbers""" symbols = ['', # (none) 'k', # kilo 'M', # mega 'G', # giga 'T', # tera 'P', # peta 'E', # exa 'Z', # zetta 'Y'] # yotta if SI: step = 1000.0 else: step = 1024.0 thresh = 999 depth = 0 # we want numbers between while number > thresh: depth = depth + 1 number = number / step # just in case someone needs more than 1000 yottabytes! diff = depth - len(symbols) + 1 if diff > 0: depth = depth - diff number = number * thresh**depth if type(number) == type(1) or type(number) == type(1L): format = '%i%s%s' elif number < 9.95: # must use 9.95 for proper sizing. For example, 9.99 will be # rounded to 10.0 with the .1f format string (which is too long) format = '%.1f%s%s' else: format = '%.0f%s%s' return(format % (float(number or 0), space, symbols[depth])) commit refs/heads/master mark :196 committer 1112998449 +1000 data 18 selected-file diff from :195 M 644 inline NEWS data 2588 bzr-0.0.4 NOT RELEASED YET ENHANCEMENTS: * bzr diff optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 29353 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, time, types, shutil, tempfile, traceback, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and new_name not in file_list: continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = cmdfn.__doc__ if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot prof = hotshot.Profile('.bzr.profile') ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load('.bzr.profile') #stats.strip_dirs() stats.sort_stats('time') stats.print_stats(20) return ret else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') traceback.print_exc(None, bzrlib.trace._tracefile) log_error('(see $HOME/.bzr.log for debug information)\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error('(see $HOME/.bzr.log for debug information)\n') traceback.print_exc(None, bzrlib.trace._tracefile) ## traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :197 committer 1112998510 +1000 data 28 notes from kernel discussion from :196 M 644 inline doc/random.txt data 9491 I think Ruby's point is right: we need to think about how a tool *feels* as you're using it. Making regular commits gives a nice rhythm to to working; in some ways it's nicer to just commit single files with C-x v v than to build complex changesets. (See gmane.c.v-c.arch.devel post 19 Nov, Tom Lord.) * Would like to generate an activity report, to e.g. mail to your boss or post to your blog. "What did I change today, across all these specified branches?" * It is possibly nice that tla by default forbids you from committing if emacs autosave or lock files exist -- I find it confusing to commit somethin other than what is shown in the editor window because there are unsaved changes. However, grumbling about unknown files is annoying, and requiring people to edit regexps in the id-tagging-method file to fix it is totally unreasonable. Perhaps there should be a preference to abort on unknown files, or perhaps it should be possible to specify forbidden files. Perhaps this is related to a mechanism to detect conflicted files: should refuse to commit if there are any .rej files lying around. *Those who lose history are doomed to recreate it.* -- broked (on #gnu.arch.users) *A universal convention supplies all of maintainability, clarity, consistency, and a foundation for good programming habits too. What it doesn't do is insist that you follow it against your will. That's Python!* -- Tim Peters on comp.lang.python, 2001-06-16 (Bazaar provides mechanism and convention, but it is up to you whether you wish to follow or enforce that convention.) ---- jblack asks for A way to subtract merges, so that you can see the work you've done to a branch since conception. ---- :: now that is a neat idea: advertise branches over zeroconf should make lca fun :-) ---- http://thedailywtf.com/ShowPost.aspx?PostID=24281 Source control is necessary and useful, but in a team of one (or even two) people the setup overhead isn't always worth it--especially if you're going to join source control in a month, and you don't want to have to migrate everything out of your existing (in my case, skunkworks) system before you can use it. At least that was my experience--I putzed with CVS a bit and knew other source control systems pretty well, but in the day-to-day it wasn't worth the bother (granted, I was a bit offended at having to wait to use the mainline source control, but that's another matter). I think Bazaar-NG will have such low setup overhead (just ``init``, ``add``) that it can be easily used for even tiny projects. The ability to merge previously-unrelated trees means they can fold their project in later. ---- From tridge: * cope without $EMAIL better * notes at start of .bzr.log: * you can delete this * or include it in bug reports * should you be able to remove things from the default ignore list? * headers at start of diff, giving some comments, perhaps dates * is diff against /dev/null really OK? I think so. * separate remove/delete commands? * detect files which were removed and now in 'missing' state * should we actually compare files for 'status', or check mtime and size; reading every file in the samba source tree can take a long time. without this, doing a status on a large tree can be very slow. but relying on mtime/size is a bit dangerous. people really do work on trees which take a large chunk of memory and which will not stay in memory * status up-to-date files: not 'U', and don't list without --all * if status does compare file text, then it should be quick when checking just a single file * wrapper for svn that every time run logs - command - all inputs - time it took - sufficient to replay everything - record all files * status crashes if a file is missing * option for -p1 level on diff, etc. perhaps * commit without message should start $EDITOR * don't duplicate all files on commit * start importing tridge-junkcode * perhaps need xdelta storage sooner rather than later, to handle very large file ---- The first operation most people do with a new version-control system is *not* making their own project, but rather getting a checkout of an existing project, building it, and possibly submitting a patch. So those operations should be *extremely* easy. ---- * Way to check that a branch is fully merged, and no longer needed: should mean all its changes have been integrated upstream, no uncommitted changes or rejects or unknown files. * Filter revisions by containing a particular word (as for log). Perhaps have key-value fields that might be used for e.g. line-of-development or bug nr? * List difference in the revisions on one branch vs another. * Perhaps use a partially-readable but still hopefully unique ID for revisions/inventories? * Preview what will happen in a merge before it is applied * When a changeset deletes a file, should have the option to just make it unknown/ignored. Perhaps this is best handled by an interactive merge. If the file is unchanged locally and deleted remotely, it will by default be deleted (but the user has the option to reject the delete, or to make it just unversioned, or to save a copy.) If it is modified locall then the user still needs to choose between those options but there is no default (or perhaps the default is to reject the delete.) * interactive commit, prompting whether each hunk should be sent (as for darcs) * Write up something about detection of unmodified files * Preview a merge so as to get some idea what will happen: * What revisions will be merged (log entries, etc) * What files will be affected? * Are those simple updates, or have they been updated locally as well. * Any renames or metadata clashes? * Show diffs or conflict markers. * Do the merge, but write into a second directory. * "Show me all changesets that touch this file" Can be done by walking back through all revisions, and filtering out those where the file-id either gets a new name or a new text. * Way to commit backdated revisions or pretend to be something by someone else, for the benefit of import tools; in general allow everything taken from the current environment to be overridden. * Cope well when trying to checkout or update over a flaky connection. Passive HTTP possibly helps with this: we can fetch all the file texts first, then the inventory, and can even retry interrupted connections. * Use readline for reading log messages, and store a history of previous commit messages! * Warn when adding huge files(?) - more than say 10MB? On the other hand, why not just cope? * Perhaps allow people to specify a revision-id, much as people have unique but human-assigned names for patches at the moment? ---- 20050218090900.GA2071@opteron.random Subject: Re: [darcs-users] Re: [BK] upgrade will be needed From: Andrea Arcangeli Newsgroups: gmane.linux.kernel Date: Fri, 18 Feb 2005 10:09:00 +0100 On Thu, Feb 17, 2005 at 06:24:53PM -0800, Tupshin Harper wrote: > small to medium sized ones). Last I checked, Arch was still too slow in > some areas, though that might have changed in recent months. Also, many IMHO someone needs to rewrite ARCH using the RCS or SCCS format for the backend and a single file for the changesets and with sane parameters conventions miming SVN. The internal algorithms of arch seems the most advanced possible. It's just the interface and the fs backend that's so bad and doesn't compress in the backups either. SVN bsddb doesn't compress either by default, but at least the new fsfs compresses pretty well, not as good as CVS, but not as badly as bsddb and arch either. I may be completely wrong, so take the above just as a humble suggestion. darcs scares me a bit because it's in haskell, I don't believe very much in functional languages for compute intensive stuff, ram utilization skyrockets sometime (I wouldn't like to need >1G of ram to manage the tree). Other languages like python or perl are much slower than C/C++ too but at least ram utilization can be normally dominated to sane levels with them and they can be greatly optimized easily with C/C++ extensions of the performance critical parts. ----- * Fix up diffs for files without a trailing newline ----- * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. ---- When sending patches by email (optionall) send each one as a separate mail, with a sequence number [%03d/%03d] at the start of the Subject. See mail from linus 2005-04-07. http://linux.yyz.us/patch-format.html http://www.zip.com.au/~akpm/linux/patches/stuff/tpp.txt ---- dwmw2 (2005-04-07) reorder patches by cherry-picking them from a main development tree before sending them on. commit refs/heads/master mark :198 committer 1113014944 +1000 data 62 - experimental compressed Revfile support not integrated yet from :197 M 644 inline bzrlib/mdiff.py data 1857 # (C) 2005 Matt Mackall # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import difflib, sys, struct def linesplit(a): al, ap = [], [] last = 0 n = a.find("\n") + 1 while n > 0: ap.append(last) al.append(a[last:n]) last = n n = a.find("\n", n) + 1 return (al, ap) def diff(a, b): (al, ap) = linesplit(a) (bl, bp) = linesplit(b) d = difflib.SequenceMatcher(None, al, bl) ops = [] for o, m, n, s, t in d.get_opcodes(): if o == 'equal': continue ops.append((ap[m], ap[n], "".join(bl[s:t]))) return ops def tobinary(ops): b = "" for f in ops: b += struct.pack(">lll", f[0], f[1], len(f[2])) + f[2] return b def bdiff(a, b): return tobinary(diff(a, b)) def patch(t, ops): last = 0 r = [] for p1, p2, sub in ops: r.append(t[last:p1]) r.append(sub) last = p2 r.append(t[last:]) return "".join(r) def frombinary(b): ops = [] while b: p = b[:12] m, n, l = struct.unpack(">lll", p) ops.append((m, n, b[12:12 + l])) b = b[12 + l:] return ops def bpatch(t, b): return patch(t, frombinary(b)) M 644 inline bzrlib/revfile.py data 6909 #! /usr/bin/env python # (C) 2005 Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. """ import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) class RevfileError(Exception): pass class Revfile: def __init__(self, basename): self.basename = basename self.idxfile = open(basename + '.irev', 'r+b') self.datafile = open(basename + '.drev', 'r+b') if self.last_idx() == -1: print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def last_idx(self): """Return last index already present, or -1 if none.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l == 0: return -1 if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) return (l / _RECORDSIZE) - 1 def revision(self, rev): base = self.index[rev][0] start = self.index[base][1] end = self.index[rev][1] + self.index[rev][2] f = open(self.datafile()) f.seek(start) data = f.read(end - start) last = self.index[base][2] text = zlib.decompress(data[:last]) for r in range(base + 1, rev + 1): s = self.index[r][2] b = zlib.decompress(data[last:last + s]) text = mdiff.bpatch(text, b) last = last + s return text def add_full_text(self, t): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" idx = self.last_idx() + 1 self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * idx data_offset = self.datafile.tell() assert isinstance(t, str) # not unicode or anything wierd self.datafile.write(t) self.datafile.flush() entry = sha.new(t).digest() entry += struct.pack(">llll12x", 0, 0, data_offset, len(t)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def __len__(self): return int(self.last_idx()) def __getitem__(self, idx): self.idxfile.seek((idx + 1) * _RECORDSIZE) rec = self.idxfile.read(_RECORDSIZE) if len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sllll12x", rec) def addrevision(self, text, changeset): t = self.tip() n = t + 1 if not n % factor: data = zlib.compress(text) base = n else: prev = self.revision(t) data = zlib.compress(mdiff.bdiff(prev, text)) base = self.index[t][0] offset = 0 if t >= 0: offset = self.index[t][1] + self.index[t][2] self.index.append((base, offset, len(data), changeset)) entry = struct.pack(">llll", base, offset, len(data), changeset) open(self.indexfile(), "a").write(entry) open(self.datafile(), "a").write(data) def dump(self): print '%-8s %-40s %-8s %-8s %-8s %-8s' \ % tuple('idx sha1 base flags offset len'.split()) print '-'*8, '-'*40, ('-'*8 + ' ')*4 for i in range(len(self)): rec = self[i] print "#%-7d %40s #%-7d %08x %8d %8d " \ % (i, hexlify(rec[0]), rec[1], rec[2], rec[3], rec[4]) def main(argv): r = Revfile("testrev") if len(argv) < 2: sys.stderr.write("usage: revfile dump\n" " revfile add\n") sys.exit(1) if argv[1] == 'add': new_idx = r.add_full_text(sys.stdin.read()) print 'added idx %d' % new_idx elif argv[1] == 'dump': r.dump() else: sys.stderr.write("unknown command %r\n" % argv[1]) sys.exit(1) if __name__ == '__main__': import sys main(sys.argv) commit refs/heads/master mark :199 committer 1113015338 +1000 data 55 - use -1 for no_base in revfile - better revfile dumper from :198 M 644 inline bzrlib/revfile.py data 7212 #! /usr/bin/env python # (C) 2005 Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. """ import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_BASE = 0xFFFFFFFFL class RevfileError(Exception): pass class Revfile: def __init__(self, basename): self.basename = basename self.idxfile = open(basename + '.irev', 'r+b') self.datafile = open(basename + '.drev', 'r+b') if self.last_idx() == -1: print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def last_idx(self): """Return last index already present, or -1 if none.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l == 0: return -1 if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) return (l / _RECORDSIZE) - 1 def revision(self, rev): base = self.index[rev][0] start = self.index[base][1] end = self.index[rev][1] + self.index[rev][2] f = open(self.datafile()) f.seek(start) data = f.read(end - start) last = self.index[base][2] text = zlib.decompress(data[:last]) for r in range(base + 1, rev + 1): s = self.index[r][2] b = zlib.decompress(data[last:last + s]) text = mdiff.bpatch(text, b) last = last + s return text def add_full_text(self, t): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" idx = self.last_idx() + 1 self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * idx data_offset = self.datafile.tell() assert isinstance(t, str) # not unicode or anything wierd self.datafile.write(t) self.datafile.flush() entry = sha.new(t).digest() entry += struct.pack(">IIII12x", 0xFFFFFFFFL, 0, data_offset, len(t)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def __len__(self): return int(self.last_idx()) def __getitem__(self, idx): self.idxfile.seek((idx + 1) * _RECORDSIZE) rec = self.idxfile.read(_RECORDSIZE) if len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def addrevision(self, text, changeset): t = self.tip() n = t + 1 if not n % factor: data = zlib.compress(text) base = n else: prev = self.revision(t) data = zlib.compress(mdiff.bdiff(prev, text)) base = self.index[t][0] offset = 0 if t >= 0: offset = self.index[t][1] + self.index[t][2] self.index.append((base, offset, len(data), changeset)) entry = struct.pack(">llll", base, offset, len(data), changeset) open(self.indexfile(), "a").write(entry) open(self.datafile(), "a").write(data) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i in range(len(self)): rec = self[i] f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_BASE: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def main(argv): r = Revfile("testrev") if len(argv) < 2: sys.stderr.write("usage: revfile dump\n" " revfile add\n") sys.exit(1) if argv[1] == 'add': new_idx = r.add_full_text(sys.stdin.read()) print 'added idx %d' % new_idx elif argv[1] == 'dump': r.dump() else: sys.stderr.write("unknown command %r\n" % argv[1]) sys.exit(1) if __name__ == '__main__': import sys main(sys.argv) commit refs/heads/master mark :200 committer 1113015548 +1000 data 53 revfile: fix up __getitem__ to allow simple iteration from :199 M 644 inline bzrlib/revfile.py data 7333 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. """ import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_BASE = 0xFFFFFFFFL class RevfileError(Exception): pass class Revfile: def __init__(self, basename): self.basename = basename self.idxfile = open(basename + '.irev', 'r+b') self.datafile = open(basename + '.drev', 'r+b') if self.last_idx() == -1: print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def last_idx(self): """Return last index already present, or -1 if none.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l == 0: return -1 if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) return (l / _RECORDSIZE) - 1 def revision(self, rev): base = self.index[rev][0] start = self.index[base][1] end = self.index[rev][1] + self.index[rev][2] f = open(self.datafile()) f.seek(start) data = f.read(end - start) last = self.index[base][2] text = zlib.decompress(data[:last]) for r in range(base + 1, rev + 1): s = self.index[r][2] b = zlib.decompress(data[last:last + s]) text = mdiff.bpatch(text, b) last = last + s return text def add_full_text(self, t): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" idx = self.last_idx() + 1 self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * idx data_offset = self.datafile.tell() assert isinstance(t, str) # not unicode or anything wierd self.datafile.write(t) self.datafile.flush() entry = sha.new(t).digest() entry += struct.pack(">IIII12x", 0xFFFFFFFFL, 0, data_offset, len(t)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def __len__(self): return int(self.last_idx()) def __getitem__(self, idx): self.idxfile.seek((idx + 1) * _RECORDSIZE) rec = self.idxfile.read(_RECORDSIZE) if not rec: raise IndexError("no record %d in index for %r" % (idx, self.basename)) elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def addrevision(self, text, changeset): t = self.tip() n = t + 1 if not n % factor: data = zlib.compress(text) base = n else: prev = self.revision(t) data = zlib.compress(mdiff.bdiff(prev, text)) base = self.index[t][0] offset = 0 if t >= 0: offset = self.index[t][1] + self.index[t][2] self.index.append((base, offset, len(data), changeset)) entry = struct.pack(">llll", base, offset, len(data), changeset) open(self.indexfile(), "a").write(entry) open(self.datafile(), "a").write(data) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_BASE: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def main(argv): r = Revfile("testrev") if len(argv) < 2: sys.stderr.write("usage: revfile dump\n" " revfile add\n") sys.exit(1) if argv[1] == 'add': new_idx = r.add_full_text(sys.stdin.read()) print 'added idx %d' % new_idx elif argv[1] == 'dump': r.dump() else: sys.stderr.write("unknown command %r\n" % argv[1]) sys.exit(1) if __name__ == '__main__': import sys main(sys.argv) commit refs/heads/master mark :201 committer 1113017370 +1000 data 117 Revfile: - get full text from a record- fix creation of files if they don't exist- protect against half-assed storage from :200 M 644 inline bzrlib/revfile.py data 8787 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. """ # TODO: Something like pread() would make this slightly simpler and # perhaps more efficient. # TODO: Could also try to mmap things... import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_BASE = 0xFFFFFFFFL # fields in the index record I_SHA = 0 I_BASE = 1 I_FLAGS = 2 I_OFFSET = 3 I_LEN = 4 class RevfileError(Exception): pass class Revfile: def __init__(self, basename): self.basename = basename idxname = basename + '.irev' dataname = basename + '.drev' idx_exists = os.path.exists(idxname) data_exists = os.path.exists(dataname) if idx_exists != data_exists: raise RevfileError("half-assed revfile") if not idx_exists: self.idxfile = open(idxname, 'w+b') self.datafile = open(dataname, 'w+b') print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: self.idxfile = open(idxname, 'r+b') self.dataname = open(dataname, 'r+b') h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def last_idx(self): """Return last index already present, or -1 if none.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l == 0: return -1 if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) return (l / _RECORDSIZE) - 1 def revision(self, rev): base = self.index[rev][0] start = self.index[base][1] end = self.index[rev][1] + self.index[rev][2] f = open(self.datafile()) f.seek(start) data = f.read(end - start) last = self.index[base][2] text = zlib.decompress(data[:last]) for r in range(base + 1, rev + 1): s = self.index[r][2] b = zlib.decompress(data[last:last + s]) text = mdiff.bpatch(text, b) last = last + s return text def add_full_text(self, t): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" idx = self.last_idx() + 1 self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * idx data_offset = self.datafile.tell() assert isinstance(t, str) # not unicode or anything wierd self.datafile.write(t) self.datafile.flush() entry = sha.new(t).digest() entry += struct.pack(">IIII12x", 0xFFFFFFFFL, 0, data_offset, len(t)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def _get_full_text(self, idx): idxrec = self[idx] assert idxrec[I_FLAGS] == 0 assert idxrec[I_BASE] == _NO_BASE l = idxrec[I_LEN] if l == 0: return '' self.datafile.seek(idxrec[I_OFFSET]) text = self.datafile.read(l) if len(text) != l: raise RevfileError("short read %d of %d " "getting text for record %d in %r" % (len(text), l, idx, self.basename)) return text def __len__(self): return int(self.last_idx()) def __getitem__(self, idx): """Index by sequence id returns the index field""" self._seek_index(idx) return self._read_next_index() def _seek_index(self, idx): self.idxfile.seek((idx + 1) * _RECORDSIZE) def _read_next_index(self): rec = self.idxfile.read(_RECORDSIZE) if not rec: raise IndexError("end of index file") elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def addrevision(self, text, changeset): t = self.tip() n = t + 1 if not n % factor: data = zlib.compress(text) base = n else: prev = self.revision(t) data = zlib.compress(mdiff.bdiff(prev, text)) base = self.index[t][0] offset = 0 if t >= 0: offset = self.index[t][1] + self.index[t][2] self.index.append((base, offset, len(data), changeset)) entry = struct.pack(">llll", base, offset, len(data), changeset) open(self.indexfile(), "a").write(entry) open(self.datafile(), "a").write(data) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_BASE: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def main(argv): r = Revfile("testrev") if len(argv) < 2: sys.stderr.write("usage: revfile dump\n" " revfile add\n" " revfile get IDX\n") sys.exit(1) if argv[1] == 'add': new_idx = r.add_full_text(sys.stdin.read()) print 'added idx %d' % new_idx elif argv[1] == 'dump': r.dump() elif argv[1] == 'get': sys.stdout.write(r._get_full_text(int(argv[2]))) else: sys.stderr.write("unknown command %r\n" % argv[1]) sys.exit(1) if __name__ == '__main__': import sys main(sys.argv) commit refs/heads/master mark :202 committer 1113018253 +1000 data 92 Revfile: - better message when trying to get missing index - clean up last_idx stuff - todo from :201 M 644 inline bzrlib/revfile.py data 9035 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. """ # TODO: Something like pread() would make this slightly simpler and # perhaps more efficient. # TODO: Could also try to mmap things... import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_BASE = 0xFFFFFFFFL # fields in the index record I_SHA = 0 I_BASE = 1 I_FLAGS = 2 I_OFFSET = 3 I_LEN = 4 class RevfileError(Exception): pass class Revfile: def __init__(self, basename): # TODO: Option to open readonly # TODO: Lock file while open # TODO: advise of random access self.basename = basename idxname = basename + '.irev' dataname = basename + '.drev' idx_exists = os.path.exists(idxname) data_exists = os.path.exists(dataname) if idx_exists != data_exists: raise RevfileError("half-assed revfile") if not idx_exists: self.idxfile = open(idxname, 'w+b') self.datafile = open(dataname, 'w+b') print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: self.idxfile = open(idxname, 'r+b') self.datafile = open(dataname, 'r+b') h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def last_idx(self): """Return last index already present, or -1 if none.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l == 0: return -1 if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) return (l / _RECORDSIZE) - 2 def revision(self, rev): base = self.index[rev][0] start = self.index[base][1] end = self.index[rev][1] + self.index[rev][2] f = open(self.datafile()) f.seek(start) data = f.read(end - start) last = self.index[base][2] text = zlib.decompress(data[:last]) for r in range(base + 1, rev + 1): s = self.index[r][2] b = zlib.decompress(data[last:last + s]) text = mdiff.bpatch(text, b) last = last + s return text def add_full_text(self, t): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" idx = self.last_idx() + 1 self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * (idx + 1) data_offset = self.datafile.tell() assert isinstance(t, str) # not unicode or anything wierd self.datafile.write(t) self.datafile.flush() entry = sha.new(t).digest() entry += struct.pack(">IIII12x", 0xFFFFFFFFL, 0, data_offset, len(t)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def _get_full_text(self, idx): idxrec = self[idx] assert idxrec[I_FLAGS] == 0 assert idxrec[I_BASE] == _NO_BASE l = idxrec[I_LEN] if l == 0: return '' self.datafile.seek(idxrec[I_OFFSET]) text = self.datafile.read(l) if len(text) != l: raise RevfileError("short read %d of %d " "getting text for record %d in %r" % (len(text), l, idx, self.basename)) return text def __len__(self): return int(self.last_idx()) + 1 def __getitem__(self, idx): """Index by sequence id returns the index field""" self._seek_index(idx) return self._read_next_index() def _seek_index(self, idx): self.idxfile.seek((idx + 1) * _RECORDSIZE) def _read_next_index(self): rec = self.idxfile.read(_RECORDSIZE) if not rec: raise IndexError("end of index file") elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def addrevision(self, text, changeset): t = self.tip() n = t + 1 if not n % factor: data = zlib.compress(text) base = n else: prev = self.revision(t) data = zlib.compress(mdiff.bdiff(prev, text)) base = self.index[t][0] offset = 0 if t >= 0: offset = self.index[t][1] + self.index[t][2] self.index.append((base, offset, len(data), changeset)) entry = struct.pack(">llll", base, offset, len(data), changeset) open(self.indexfile(), "a").write(entry) open(self.datafile(), "a").write(data) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_BASE: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def main(argv): r = Revfile("testrev") if len(argv) < 2: sys.stderr.write("usage: revfile dump\n" " revfile add\n" " revfile get IDX\n") sys.exit(1) if argv[1] == 'add': new_idx = r.add_full_text(sys.stdin.read()) print 'added idx %d' % new_idx elif argv[1] == 'dump': r.dump() elif argv[1] == 'get': try: sys.stdout.write(r._get_full_text(int(argv[2]))) except IndexError: sys.stderr.write("no such record\n") sys.exit(1) else: sys.stderr.write("unknown command %r\n" % argv[1]) sys.exit(1) if __name__ == '__main__': import sys main(sys.argv) commit refs/heads/master mark :203 committer 1113018618 +1000 data 83 revfile: - remove last_idx in favour of just __len__ - better command-line handling from :202 M 644 inline bzrlib/revfile.py data 9179 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. """ # TODO: Something like pread() would make this slightly simpler and # perhaps more efficient. # TODO: Could also try to mmap things... import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_BASE = 0xFFFFFFFFL # fields in the index record I_SHA = 0 I_BASE = 1 I_FLAGS = 2 I_OFFSET = 3 I_LEN = 4 class RevfileError(Exception): pass class Revfile: def __init__(self, basename): # TODO: Option to open readonly # TODO: Lock file while open # TODO: advise of random access self.basename = basename idxname = basename + '.irev' dataname = basename + '.drev' idx_exists = os.path.exists(idxname) data_exists = os.path.exists(dataname) if idx_exists != data_exists: raise RevfileError("half-assed revfile") if not idx_exists: self.idxfile = open(idxname, 'w+b') self.datafile = open(dataname, 'w+b') print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: self.idxfile = open(idxname, 'r+b') self.datafile = open(dataname, 'r+b') h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def revision(self, rev): base = self.index[rev][0] start = self.index[base][1] end = self.index[rev][1] + self.index[rev][2] f = open(self.datafile()) f.seek(start) data = f.read(end - start) last = self.index[base][2] text = zlib.decompress(data[:last]) for r in range(base + 1, rev + 1): s = self.index[r][2] b = zlib.decompress(data[last:last + s]) text = mdiff.bpatch(text, b) last = last + s return text def add_full_text(self, t): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" idx = len(self) self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * (idx + 1) data_offset = self.datafile.tell() assert isinstance(t, str) # not unicode or anything wierd self.datafile.write(t) self.datafile.flush() entry = sha.new(t).digest() entry += struct.pack(">IIII12x", 0xFFFFFFFFL, 0, data_offset, len(t)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def _get_full_text(self, idx): idxrec = self[idx] assert idxrec[I_FLAGS] == 0 assert idxrec[I_BASE] == _NO_BASE l = idxrec[I_LEN] if l == 0: return '' self.datafile.seek(idxrec[I_OFFSET]) text = self.datafile.read(l) if len(text) != l: raise RevfileError("short read %d of %d " "getting text for record %d in %r" % (len(text), l, idx, self.basename)) return text def __len__(self): """Return number of revisions.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) if l < _RECORDSIZE: raise RevfileError("no header present in index of %r" % (self.basename)) return int(l / _RECORDSIZE) - 1 def __getitem__(self, idx): """Index by sequence id returns the index field""" self._seek_index(idx) return self._read_next_index() def _seek_index(self, idx): self.idxfile.seek((idx + 1) * _RECORDSIZE) def _read_next_index(self): rec = self.idxfile.read(_RECORDSIZE) if not rec: raise IndexError("end of index file") elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def addrevision(self, text, changeset): t = self.tip() n = t + 1 if not n % factor: data = zlib.compress(text) base = n else: prev = self.revision(t) data = zlib.compress(mdiff.bdiff(prev, text)) base = self.index[t][0] offset = 0 if t >= 0: offset = self.index[t][1] + self.index[t][2] self.index.append((base, offset, len(data), changeset)) entry = struct.pack(">llll", base, offset, len(data), changeset) open(self.indexfile(), "a").write(entry) open(self.datafile(), "a").write(data) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_BASE: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def main(argv): r = Revfile("testrev") try: cmd = argv[1] except IndexError: sys.stderr.write("usage: revfile dump\n" " revfile add\n" " revfile get IDX\n") return 1 if cmd == 'add': new_idx = r.add_full_text(sys.stdin.read()) print 'added idx %d' % new_idx elif cmd == 'dump': r.dump() elif cmd == 'get': try: idx = int(argv[2]) except IndexError: sys.stderr.write("usage: revfile get IDX\n") return 1 if idx < 0 or idx >= len(r): sys.stderr.write("invalid index %r\n" % idx) return 1 sys.stdout.write(r._get_full_text(idx)) else: sys.stderr.write("unknown command %r\n" % cmd) return 1 if __name__ == '__main__': import sys sys.exit(main(sys.argv) or 0) commit refs/heads/master mark :204 committer 1113019285 +1000 data 74 Revfile:- new find-sha command and implementation- new _check_index helper from :203 M 644 inline bzrlib/revfile.py data 10397 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. """ # TODO: Something like pread() would make this slightly simpler and # perhaps more efficient. # TODO: Could also try to mmap things... import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify, unhexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_RECORD = 0xFFFFFFFFL # fields in the index record I_SHA = 0 I_BASE = 1 I_FLAGS = 2 I_OFFSET = 3 I_LEN = 4 class RevfileError(Exception): pass class Revfile: def __init__(self, basename): # TODO: Option to open readonly # TODO: Lock file while open # TODO: advise of random access self.basename = basename idxname = basename + '.irev' dataname = basename + '.drev' idx_exists = os.path.exists(idxname) data_exists = os.path.exists(dataname) if idx_exists != data_exists: raise RevfileError("half-assed revfile") if not idx_exists: self.idxfile = open(idxname, 'w+b') self.datafile = open(dataname, 'w+b') print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: self.idxfile = open(idxname, 'r+b') self.datafile = open(dataname, 'r+b') h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def revision(self, rev): base = self.index[rev][0] start = self.index[base][1] end = self.index[rev][1] + self.index[rev][2] f = open(self.datafile()) f.seek(start) data = f.read(end - start) last = self.index[base][2] text = zlib.decompress(data[:last]) for r in range(base + 1, rev + 1): s = self.index[r][2] b = zlib.decompress(data[last:last + s]) text = mdiff.bpatch(text, b) last = last + s return text def _add_full_text(self, t): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" idx = len(self) self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * (idx + 1) data_offset = self.datafile.tell() assert isinstance(t, str) # not unicode or anything wierd self.datafile.write(t) self.datafile.flush() entry = sha.new(t).digest() entry += struct.pack(">IIII12x", 0xFFFFFFFFL, 0, data_offset, len(t)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def _check_index(self, idx): if idx < 0 or idx > len(self): raise RevfileError("invalid index %r" % idx) def find_sha(self, s): assert isinstance(s, str) assert len(s) == 20 for idx, idxrec in enumerate(self): if idxrec[I_SHA] == s: return idx else: return _NO_RECORD def _add_diff(self, text, base): """Add a text stored relative to a previous text.""" self._check_index(base) text_sha = sha.new(text).digest() def addrevision(self, text, changeset): t = self.tip() n = t + 1 if not n % factor: data = zlib.compress(text) base = n else: prev = self.revision(t) data = zlib.compress(mdiff.bdiff(prev, text)) base = self.index[t][0] offset = 0 if t >= 0: offset = self.index[t][1] + self.index[t][2] self.index.append((base, offset, len(data), changeset)) entry = struct.pack(">llll", base, offset, len(data), changeset) open(self.indexfile(), "a").write(entry) open(self.datafile(), "a").write(data) def _get_full_text(self, idx): idxrec = self[idx] assert idxrec[I_FLAGS] == 0 assert idxrec[I_BASE] == _NO_RECORD l = idxrec[I_LEN] if l == 0: return '' self.datafile.seek(idxrec[I_OFFSET]) text = self.datafile.read(l) if len(text) != l: raise RevfileError("short read %d of %d " "getting text for record %d in %r" % (len(text), l, idx, self.basename)) if sha.new(text).digest() != idxrec[I_SHA]: raise RevfileError("corrupt SHA-1 digest on record %d" % idx) return text def __len__(self): """Return number of revisions.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) if l < _RECORDSIZE: raise RevfileError("no header present in index of %r" % (self.basename)) return int(l / _RECORDSIZE) - 1 def __getitem__(self, idx): """Index by sequence id returns the index field""" ## TODO: Can avoid seek if we just moved there... self._seek_index(idx) return self._read_next_index() def _seek_index(self, idx): self.idxfile.seek((idx + 1) * _RECORDSIZE) def _read_next_index(self): rec = self.idxfile.read(_RECORDSIZE) if not rec: raise IndexError("end of index file") elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_RECORD: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def main(argv): r = Revfile("testrev") try: cmd = argv[1] except IndexError: sys.stderr.write("usage: revfile dump\n" " revfile add\n" " revfile get IDX\n" " revfile find-sha HEX\n") return 1 if cmd == 'add': new_idx = r._add_full_text(sys.stdin.read()) print 'added idx %d' % new_idx elif cmd == 'dump': r.dump() elif cmd == 'get': try: idx = int(argv[2]) except IndexError: sys.stderr.write("usage: revfile get IDX\n") return 1 if idx < 0 or idx >= len(r): sys.stderr.write("invalid index %r\n" % idx) return 1 sys.stdout.write(r._get_full_text(idx)) elif cmd == 'find-sha': try: s = unhexlify(argv[2]) except IndexError: sys.stderr.write("usage: revfile find-sha HEX\n") return 1 idx = r.find_sha(s) if idx == _NO_RECORD: sys.stderr.write("no such record\n") return 1 else: print idx else: sys.stderr.write("unknown command %r\n" % cmd) return 1 if __name__ == '__main__': import sys sys.exit(main(sys.argv) or 0) commit refs/heads/master mark :205 committer 1113021498 +1000 data 67 Revfile:- store and retrieve deltas!mdiff:- work on bytes not lines from :204 M 644 inline bzrlib/mdiff.py data 2231 # (C) 2005 Matt Mackall # (C) 2005 Canonical Ltd # based on code by Matt Mackall, hacked by Martin Pool # mm's code works line-by-line; this just works on byte strings. # Possibly slower; possibly gives better results for code not # regularly separated by newlines and anyhow a bit simpler. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # TODO: maybe work on files not strings? import difflib, sys, struct from cStringIO import StringIO def diff(a, b): d = difflib.SequenceMatcher(None, a, b) for o, m, n, s, t in d.get_opcodes(): if o == 'equal': continue # a[m:n] should be replaced by b[s:t] if s == t: yield m, n, '' else: yield m, n, b[s:t] def tobinary(ops): b = StringIO() for f in ops: b.write(struct.pack(">III", f[0], f[1], len(f[2]))) b.write(f[2]) return b.getvalue() def bdiff(a, b): return tobinary(diff(a, b)) def patch(t, ops): last = 0 b = StringIO() for m, n, r in ops: b.write(t[last:m]) if r: b.write(r) last = n b.write(t[last:]) return b.getvalue() def frombinary(b): bin = StringIO(b) while True: p = bin.read(12) if not p: break m, n, l = struct.unpack(">III", p) if l == 0: r = '' else: r = bin.read(l) if len(r) != l: raise Exception("truncated patch data") yield m, n, r def bpatch(t, b): return patch(t, frombinary(b)) M 644 inline bzrlib/revfile.py data 11808 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. """ # TODO: Something like pread() would make this slightly simpler and # perhaps more efficient. # TODO: Could also try to mmap things... import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify, unhexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_RECORD = 0xFFFFFFFFL # fields in the index record I_SHA = 0 I_BASE = 1 I_FLAGS = 2 I_OFFSET = 3 I_LEN = 4 class RevfileError(Exception): pass class Revfile: def __init__(self, basename): # TODO: Option to open readonly # TODO: Lock file while open # TODO: advise of random access self.basename = basename idxname = basename + '.irev' dataname = basename + '.drev' idx_exists = os.path.exists(idxname) data_exists = os.path.exists(dataname) if idx_exists != data_exists: raise RevfileError("half-assed revfile") if not idx_exists: self.idxfile = open(idxname, 'w+b') self.datafile = open(dataname, 'w+b') print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: self.idxfile = open(idxname, 'r+b') self.datafile = open(dataname, 'r+b') h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def revision(self, rev): base = self.index[rev][0] start = self.index[base][1] end = self.index[rev][1] + self.index[rev][2] f = open(self.datafile()) f.seek(start) data = f.read(end - start) last = self.index[base][2] text = zlib.decompress(data[:last]) for r in range(base + 1, rev + 1): s = self.index[r][2] b = zlib.decompress(data[last:last + s]) text = mdiff.bpatch(text, b) last = last + s return text def _check_index(self, idx): if idx < 0 or idx > len(self): raise RevfileError("invalid index %r" % idx) def find_sha(self, s): assert isinstance(s, str) assert len(s) == 20 for idx, idxrec in enumerate(self): if idxrec[I_SHA] == s: return idx else: return _NO_RECORD def _add_common(self, text_sha, data, flags, base): """Add pre-processed data, can be either full text or delta.""" idx = len(self) self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * (idx + 1) data_offset = self.datafile.tell() assert isinstance(data, str) # not unicode or anything wierd self.datafile.write(data) self.datafile.flush() assert isinstance(text_sha, str) entry = text_sha entry += struct.pack(">IIII12x", base, flags, data_offset, len(data)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def _add_full_text(self, text): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" return self._add_common(sha.new(text).digest(), text, 0, _NO_RECORD) def _add_delta(self, text, base): """Add a text stored relative to a previous text.""" self._check_index(base) text_sha = sha.new(text).digest() base_text = self.get(base) data = mdiff.bdiff(base_text, text) return self._add_common(text_sha, data, 0, base) def add(self, text, base=None): # TODO: check it's not already present? assert 0 def addrevision(self, text, changeset): t = self.tip() n = t + 1 if not n % factor: data = zlib.compress(text) base = n else: prev = self.revision(t) data = zlib.compress(mdiff.bdiff(prev, text)) base = self.index[t][0] offset = 0 if t >= 0: offset = self.index[t][1] + self.index[t][2] self.index.append((base, offset, len(data), changeset)) entry = struct.pack(">llll", base, offset, len(data), changeset) open(self.indexfile(), "a").write(entry) open(self.datafile(), "a").write(data) def get(self, idx): idxrec = self[idx] base = idxrec[I_BASE] if base == _NO_RECORD: text = self._get_full_text(idx, idxrec) else: text = self._get_patched(idx, idxrec) if sha.new(text).digest() != idxrec[I_SHA]: raise RevfileError("corrupt SHA-1 digest on record %d" % idx) return text def _get_raw(self, idx, idxrec): l = idxrec[I_LEN] if l == 0: return '' self.datafile.seek(idxrec[I_OFFSET]) data = self.datafile.read(l) if len(data) != l: raise RevfileError("short read %d of %d " "getting text for record %d in %r" % (len(data), l, idx, self.basename)) return data def _get_full_text(self, idx, idxrec): assert idxrec[I_FLAGS] == 0 assert idxrec[I_BASE] == _NO_RECORD text = self._get_raw(idx, idxrec) return text def _get_patched(self, idx, idxrec): assert idxrec[I_FLAGS] == 0 base = idxrec[I_BASE] assert base >= 0 assert base < idx # no loops! base_text = self.get(base) patch = self._get_raw(idx, idxrec) text = mdiff.bpatch(base_text, patch) return text def __len__(self): """Return number of revisions.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) if l < _RECORDSIZE: raise RevfileError("no header present in index of %r" % (self.basename)) return int(l / _RECORDSIZE) - 1 def __getitem__(self, idx): """Index by sequence id returns the index field""" ## TODO: Can avoid seek if we just moved there... self._seek_index(idx) return self._read_next_index() def _seek_index(self, idx): if idx < 0: raise RevfileError("invalid index %r" % idx) self.idxfile.seek((idx + 1) * _RECORDSIZE) def _read_next_index(self): rec = self.idxfile.read(_RECORDSIZE) if not rec: raise IndexError("end of index file") elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_RECORD: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def main(argv): r = Revfile("testrev") try: cmd = argv[1] except IndexError: sys.stderr.write("usage: revfile dump\n" " revfile add\n" " revfile add-delta BASE\n" " revfile get IDX\n" " revfile find-sha HEX\n") return 1 if cmd == 'add': new_idx = r._add_full_text(sys.stdin.read()) print 'added idx %d' % new_idx elif cmd == 'add-delta': new_idx = r._add_delta(sys.stdin.read(), int(argv[2])) print 'added idx %d' % new_idx elif cmd == 'dump': r.dump() elif cmd == 'get': try: idx = int(argv[2]) except IndexError: sys.stderr.write("usage: revfile get IDX\n") return 1 if idx < 0 or idx >= len(r): sys.stderr.write("invalid index %r\n" % idx) return 1 sys.stdout.write(r.get(idx)) elif cmd == 'find-sha': try: s = unhexlify(argv[2]) except IndexError: sys.stderr.write("usage: revfile find-sha HEX\n") return 1 idx = r.find_sha(s) if idx == _NO_RECORD: sys.stderr.write("no such record\n") return 1 else: print idx else: sys.stderr.write("unknown command %r\n" % cmd) return 1 if __name__ == '__main__': import sys sys.exit(main(sys.argv) or 0) commit refs/heads/master mark :206 committer 1113022713 +1000 data 22 new Revfile.add() dwim from :205 M 644 inline bzrlib/revfile.py data 12073 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. """ # TODO: Something like pread() would make this slightly simpler and # perhaps more efficient. # TODO: Could also try to mmap things... import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify, unhexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_RECORD = 0xFFFFFFFFL # fields in the index record I_SHA = 0 I_BASE = 1 I_FLAGS = 2 I_OFFSET = 3 I_LEN = 4 class RevfileError(Exception): pass class Revfile: def __init__(self, basename): # TODO: Option to open readonly # TODO: Lock file while open # TODO: advise of random access self.basename = basename idxname = basename + '.irev' dataname = basename + '.drev' self.idxpos = 0L idx_exists = os.path.exists(idxname) data_exists = os.path.exists(dataname) if idx_exists != data_exists: raise RevfileError("half-assed revfile") if not idx_exists: self.idxfile = open(idxname, 'w+b') self.datafile = open(dataname, 'w+b') print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: self.idxfile = open(idxname, 'r+b') self.datafile = open(dataname, 'r+b') h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def revision(self, rev): base = self.index[rev][0] start = self.index[base][1] end = self.index[rev][1] + self.index[rev][2] f = open(self.datafile()) f.seek(start) data = f.read(end - start) last = self.index[base][2] text = zlib.decompress(data[:last]) for r in range(base + 1, rev + 1): s = self.index[r][2] b = zlib.decompress(data[last:last + s]) text = mdiff.bpatch(text, b) last = last + s return text def _check_index(self, idx): if idx < 0 or idx > len(self): raise RevfileError("invalid index %r" % idx) def find_sha(self, s): assert isinstance(s, str) assert len(s) == 20 for idx, idxrec in enumerate(self): if idxrec[I_SHA] == s: return idx else: return _NO_RECORD def _add_common(self, text_sha, data, flags, base): """Add pre-processed data, can be either full text or delta.""" idx = len(self) self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * (idx + 1) data_offset = self.datafile.tell() assert isinstance(data, str) # not unicode or anything wierd self.datafile.write(data) self.datafile.flush() assert isinstance(text_sha, str) entry = text_sha entry += struct.pack(">IIII12x", base, flags, data_offset, len(data)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def _add_full_text(self, text, text_sha): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" return self._add_common(text_sha, text, 0, _NO_RECORD) def _add_delta(self, text, text_sha, base): """Add a text stored relative to a previous text.""" self._check_index(base) base_text = self.get(base) data = mdiff.bdiff(base_text, text) return self._add_common(text_sha, data, 0, base) def add(self, text, base=_NO_RECORD): text_sha = sha.new(text).digest() idx = self.find_sha(text_sha) if idx != _NO_RECORD: return idx # already present if base == _NO_RECORD: return self._add_full_text(text, text_sha) else: return self._add_delta(self, text, text_sha, base) def addrevision(self, text, changeset): t = self.tip() n = t + 1 if not n % factor: data = zlib.compress(text) base = n else: prev = self.revision(t) data = zlib.compress(mdiff.bdiff(prev, text)) base = self.index[t][0] offset = 0 if t >= 0: offset = self.index[t][1] + self.index[t][2] self.index.append((base, offset, len(data), changeset)) entry = struct.pack(">llll", base, offset, len(data), changeset) open(self.indexfile(), "a").write(entry) open(self.datafile(), "a").write(data) def get(self, idx): idxrec = self[idx] base = idxrec[I_BASE] if base == _NO_RECORD: text = self._get_full_text(idx, idxrec) else: text = self._get_patched(idx, idxrec) if sha.new(text).digest() != idxrec[I_SHA]: raise RevfileError("corrupt SHA-1 digest on record %d" % idx) return text def _get_raw(self, idx, idxrec): l = idxrec[I_LEN] if l == 0: return '' self.datafile.seek(idxrec[I_OFFSET]) data = self.datafile.read(l) if len(data) != l: raise RevfileError("short read %d of %d " "getting text for record %d in %r" % (len(data), l, idx, self.basename)) return data def _get_full_text(self, idx, idxrec): assert idxrec[I_FLAGS] == 0 assert idxrec[I_BASE] == _NO_RECORD text = self._get_raw(idx, idxrec) return text def _get_patched(self, idx, idxrec): assert idxrec[I_FLAGS] == 0 base = idxrec[I_BASE] assert base >= 0 assert base < idx # no loops! base_text = self.get(base) patch = self._get_raw(idx, idxrec) text = mdiff.bpatch(base_text, patch) return text def __len__(self): """Return number of revisions.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) if l < _RECORDSIZE: raise RevfileError("no header present in index of %r" % (self.basename)) return int(l / _RECORDSIZE) - 1 def __getitem__(self, idx): """Index by sequence id returns the index field""" ## TODO: Can avoid seek if we just moved there... self._seek_index(idx) return self._read_next_index() def _seek_index(self, idx): if idx < 0: raise RevfileError("invalid index %r" % idx) self.idxfile.seek((idx + 1) * _RECORDSIZE) def _read_next_index(self): rec = self.idxfile.read(_RECORDSIZE) if not rec: raise IndexError("end of index file") elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_RECORD: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def main(argv): r = Revfile("testrev") try: cmd = argv[1] except IndexError: sys.stderr.write("usage: revfile dump\n" " revfile add\n" " revfile add-delta BASE\n" " revfile get IDX\n" " revfile find-sha HEX\n") return 1 if cmd == 'add': new_idx = r.add(sys.stdin.read()) print 'added idx %d' % new_idx elif cmd == 'add-delta': new_idx = r._add_delta(sys.stdin.read(), int(argv[2])) print 'added idx %d' % new_idx elif cmd == 'dump': r.dump() elif cmd == 'get': try: idx = int(argv[2]) except IndexError: sys.stderr.write("usage: revfile get IDX\n") return 1 if idx < 0 or idx >= len(r): sys.stderr.write("invalid index %r\n" % idx) return 1 sys.stdout.write(r.get(idx)) elif cmd == 'find-sha': try: s = unhexlify(argv[2]) except IndexError: sys.stderr.write("usage: revfile find-sha HEX\n") return 1 idx = r.find_sha(s) if idx == _NO_RECORD: sys.stderr.write("no such record\n") return 1 else: print idx else: sys.stderr.write("unknown command %r\n" % cmd) return 1 if __name__ == '__main__': import sys sys.exit(main(sys.argv) or 0) commit refs/heads/master mark :207 committer 1113023196 +1000 data 70 Revfile: compress data going into datafile if that would be worthwhile from :206 M 644 inline bzrlib/revfile.py data 12429 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. """ # TODO: Something like pread() would make this slightly simpler and # perhaps more efficient. # TODO: Could also try to mmap things... import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify, unhexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_RECORD = 0xFFFFFFFFL # fields in the index record I_SHA = 0 I_BASE = 1 I_FLAGS = 2 I_OFFSET = 3 I_LEN = 4 FL_GZIP = 1 class RevfileError(Exception): pass class Revfile: def __init__(self, basename): # TODO: Option to open readonly # TODO: Lock file while open # TODO: advise of random access self.basename = basename idxname = basename + '.irev' dataname = basename + '.drev' self.idxpos = 0L idx_exists = os.path.exists(idxname) data_exists = os.path.exists(dataname) if idx_exists != data_exists: raise RevfileError("half-assed revfile") if not idx_exists: self.idxfile = open(idxname, 'w+b') self.datafile = open(dataname, 'w+b') print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: self.idxfile = open(idxname, 'r+b') self.datafile = open(dataname, 'r+b') h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def revision(self, rev): base = self.index[rev][0] start = self.index[base][1] end = self.index[rev][1] + self.index[rev][2] f = open(self.datafile()) f.seek(start) data = f.read(end - start) last = self.index[base][2] text = zlib.decompress(data[:last]) for r in range(base + 1, rev + 1): s = self.index[r][2] b = zlib.decompress(data[last:last + s]) text = mdiff.bpatch(text, b) last = last + s return text def _check_index(self, idx): if idx < 0 or idx > len(self): raise RevfileError("invalid index %r" % idx) def find_sha(self, s): assert isinstance(s, str) assert len(s) == 20 for idx, idxrec in enumerate(self): if idxrec[I_SHA] == s: return idx else: return _NO_RECORD def _add_common(self, text_sha, data, base): """Add pre-processed data, can be either full text or delta. This does the compression if that makes sense.""" flags = 0 if len(data) > 50: # don't do compression if it's too small; it's unlikely to win # enough to be worthwhile compr_data = zlib.compress(data) if len(compr_data) < len(data): data = compr_data flags = FL_GZIP idx = len(self) self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * (idx + 1) data_offset = self.datafile.tell() assert isinstance(data, str) # not unicode or anything wierd self.datafile.write(data) self.datafile.flush() assert isinstance(text_sha, str) entry = text_sha entry += struct.pack(">IIII12x", base, flags, data_offset, len(data)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def _add_full_text(self, text, text_sha): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" return self._add_common(text_sha, text, _NO_RECORD) def _add_delta(self, text, text_sha, base): """Add a text stored relative to a previous text.""" self._check_index(base) base_text = self.get(base) data = mdiff.bdiff(base_text, text) return self._add_common(text_sha, data, base) def add(self, text, base=_NO_RECORD): text_sha = sha.new(text).digest() idx = self.find_sha(text_sha) if idx != _NO_RECORD: return idx # already present if base == _NO_RECORD: return self._add_full_text(text, text_sha) else: return self._add_delta(text, text_sha, base) def addrevision(self, text, changeset): t = self.tip() n = t + 1 if not n % factor: data = zlib.compress(text) base = n else: prev = self.revision(t) data = zlib.compress(mdiff.bdiff(prev, text)) base = self.index[t][0] offset = 0 if t >= 0: offset = self.index[t][1] + self.index[t][2] self.index.append((base, offset, len(data), changeset)) entry = struct.pack(">llll", base, offset, len(data), changeset) open(self.indexfile(), "a").write(entry) open(self.datafile(), "a").write(data) def get(self, idx): idxrec = self[idx] base = idxrec[I_BASE] if base == _NO_RECORD: text = self._get_full_text(idx, idxrec) else: text = self._get_patched(idx, idxrec) if sha.new(text).digest() != idxrec[I_SHA]: raise RevfileError("corrupt SHA-1 digest on record %d" % idx) return text def _get_raw(self, idx, idxrec): l = idxrec[I_LEN] if l == 0: return '' self.datafile.seek(idxrec[I_OFFSET]) data = self.datafile.read(l) if len(data) != l: raise RevfileError("short read %d of %d " "getting text for record %d in %r" % (len(data), l, idx, self.basename)) return data def _get_full_text(self, idx, idxrec): assert idxrec[I_FLAGS] == 0 assert idxrec[I_BASE] == _NO_RECORD text = self._get_raw(idx, idxrec) return text def _get_patched(self, idx, idxrec): assert idxrec[I_FLAGS] == 0 base = idxrec[I_BASE] assert base >= 0 assert base < idx # no loops! base_text = self.get(base) patch = self._get_raw(idx, idxrec) text = mdiff.bpatch(base_text, patch) return text def __len__(self): """Return number of revisions.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) if l < _RECORDSIZE: raise RevfileError("no header present in index of %r" % (self.basename)) return int(l / _RECORDSIZE) - 1 def __getitem__(self, idx): """Index by sequence id returns the index field""" ## TODO: Can avoid seek if we just moved there... self._seek_index(idx) return self._read_next_index() def _seek_index(self, idx): if idx < 0: raise RevfileError("invalid index %r" % idx) self.idxfile.seek((idx + 1) * _RECORDSIZE) def _read_next_index(self): rec = self.idxfile.read(_RECORDSIZE) if not rec: raise IndexError("end of index file") elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_RECORD: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def main(argv): r = Revfile("testrev") try: cmd = argv[1] except IndexError: sys.stderr.write("usage: revfile dump\n" " revfile add\n" " revfile add-delta BASE\n" " revfile get IDX\n" " revfile find-sha HEX\n") return 1 if cmd == 'add': new_idx = r.add(sys.stdin.read()) print 'added idx %d' % new_idx elif cmd == 'add-delta': new_idx = r.add(sys.stdin.read(), int(argv[2])) print 'added idx %d' % new_idx elif cmd == 'dump': r.dump() elif cmd == 'get': try: idx = int(argv[2]) except IndexError: sys.stderr.write("usage: revfile get IDX\n") return 1 if idx < 0 or idx >= len(r): sys.stderr.write("invalid index %r\n" % idx) return 1 sys.stdout.write(r.get(idx)) elif cmd == 'find-sha': try: s = unhexlify(argv[2]) except IndexError: sys.stderr.write("usage: revfile find-sha HEX\n") return 1 idx = r.find_sha(s) if idx == _NO_RECORD: sys.stderr.write("no such record\n") return 1 else: print idx else: sys.stderr.write("unknown command %r\n" % cmd) return 1 if __name__ == '__main__': import sys sys.exit(main(sys.argv) or 0) commit refs/heads/master mark :208 committer 1113023350 +1000 data 22 show compression ratio from :207 M 644 inline bzrlib/revfile.py data 12634 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. """ # TODO: Something like pread() would make this slightly simpler and # perhaps more efficient. # TODO: Could also try to mmap things... import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify, unhexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_RECORD = 0xFFFFFFFFL # fields in the index record I_SHA = 0 I_BASE = 1 I_FLAGS = 2 I_OFFSET = 3 I_LEN = 4 FL_GZIP = 1 class RevfileError(Exception): pass class Revfile: def __init__(self, basename): # TODO: Option to open readonly # TODO: Lock file while open # TODO: advise of random access self.basename = basename idxname = basename + '.irev' dataname = basename + '.drev' self.idxpos = 0L idx_exists = os.path.exists(idxname) data_exists = os.path.exists(dataname) if idx_exists != data_exists: raise RevfileError("half-assed revfile") if not idx_exists: self.idxfile = open(idxname, 'w+b') self.datafile = open(dataname, 'w+b') print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: self.idxfile = open(idxname, 'r+b') self.datafile = open(dataname, 'r+b') h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def revision(self, rev): base = self.index[rev][0] start = self.index[base][1] end = self.index[rev][1] + self.index[rev][2] f = open(self.datafile()) f.seek(start) data = f.read(end - start) last = self.index[base][2] text = zlib.decompress(data[:last]) for r in range(base + 1, rev + 1): s = self.index[r][2] b = zlib.decompress(data[last:last + s]) text = mdiff.bpatch(text, b) last = last + s return text def _check_index(self, idx): if idx < 0 or idx > len(self): raise RevfileError("invalid index %r" % idx) def find_sha(self, s): assert isinstance(s, str) assert len(s) == 20 for idx, idxrec in enumerate(self): if idxrec[I_SHA] == s: return idx else: return _NO_RECORD def _add_common(self, text_sha, data, base): """Add pre-processed data, can be either full text or delta. This does the compression if that makes sense.""" flags = 0 data_len = len(data) if data_len > 50: # don't do compression if it's too small; it's unlikely to win # enough to be worthwhile compr_data = zlib.compress(data) compr_len = len(compr_data) if compr_len < data_len: data = compr_data flags = FL_GZIP print '- compressed %d -> %d, %.1f%%' \ % (data_len, compr_len, float(compr_len)/float(data_len) * 100.0) idx = len(self) self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * (idx + 1) data_offset = self.datafile.tell() assert isinstance(data, str) # not unicode or anything wierd self.datafile.write(data) self.datafile.flush() assert isinstance(text_sha, str) entry = text_sha entry += struct.pack(">IIII12x", base, flags, data_offset, len(data)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def _add_full_text(self, text, text_sha): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" return self._add_common(text_sha, text, _NO_RECORD) def _add_delta(self, text, text_sha, base): """Add a text stored relative to a previous text.""" self._check_index(base) base_text = self.get(base) data = mdiff.bdiff(base_text, text) return self._add_common(text_sha, data, base) def add(self, text, base=_NO_RECORD): text_sha = sha.new(text).digest() idx = self.find_sha(text_sha) if idx != _NO_RECORD: return idx # already present if base == _NO_RECORD: return self._add_full_text(text, text_sha) else: return self._add_delta(text, text_sha, base) def addrevision(self, text, changeset): t = self.tip() n = t + 1 if not n % factor: data = zlib.compress(text) base = n else: prev = self.revision(t) data = zlib.compress(mdiff.bdiff(prev, text)) base = self.index[t][0] offset = 0 if t >= 0: offset = self.index[t][1] + self.index[t][2] self.index.append((base, offset, len(data), changeset)) entry = struct.pack(">llll", base, offset, len(data), changeset) open(self.indexfile(), "a").write(entry) open(self.datafile(), "a").write(data) def get(self, idx): idxrec = self[idx] base = idxrec[I_BASE] if base == _NO_RECORD: text = self._get_full_text(idx, idxrec) else: text = self._get_patched(idx, idxrec) if sha.new(text).digest() != idxrec[I_SHA]: raise RevfileError("corrupt SHA-1 digest on record %d" % idx) return text def _get_raw(self, idx, idxrec): l = idxrec[I_LEN] if l == 0: return '' self.datafile.seek(idxrec[I_OFFSET]) data = self.datafile.read(l) if len(data) != l: raise RevfileError("short read %d of %d " "getting text for record %d in %r" % (len(data), l, idx, self.basename)) return data def _get_full_text(self, idx, idxrec): assert idxrec[I_FLAGS] == 0 assert idxrec[I_BASE] == _NO_RECORD text = self._get_raw(idx, idxrec) return text def _get_patched(self, idx, idxrec): assert idxrec[I_FLAGS] == 0 base = idxrec[I_BASE] assert base >= 0 assert base < idx # no loops! base_text = self.get(base) patch = self._get_raw(idx, idxrec) text = mdiff.bpatch(base_text, patch) return text def __len__(self): """Return number of revisions.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) if l < _RECORDSIZE: raise RevfileError("no header present in index of %r" % (self.basename)) return int(l / _RECORDSIZE) - 1 def __getitem__(self, idx): """Index by sequence id returns the index field""" ## TODO: Can avoid seek if we just moved there... self._seek_index(idx) return self._read_next_index() def _seek_index(self, idx): if idx < 0: raise RevfileError("invalid index %r" % idx) self.idxfile.seek((idx + 1) * _RECORDSIZE) def _read_next_index(self): rec = self.idxfile.read(_RECORDSIZE) if not rec: raise IndexError("end of index file") elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_RECORD: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def main(argv): r = Revfile("testrev") try: cmd = argv[1] except IndexError: sys.stderr.write("usage: revfile dump\n" " revfile add\n" " revfile add-delta BASE\n" " revfile get IDX\n" " revfile find-sha HEX\n") return 1 if cmd == 'add': new_idx = r.add(sys.stdin.read()) print 'added idx %d' % new_idx elif cmd == 'add-delta': new_idx = r.add(sys.stdin.read(), int(argv[2])) print 'added idx %d' % new_idx elif cmd == 'dump': r.dump() elif cmd == 'get': try: idx = int(argv[2]) except IndexError: sys.stderr.write("usage: revfile get IDX\n") return 1 if idx < 0 or idx >= len(r): sys.stderr.write("invalid index %r\n" % idx) return 1 sys.stdout.write(r.get(idx)) elif cmd == 'find-sha': try: s = unhexlify(argv[2]) except IndexError: sys.stderr.write("usage: revfile find-sha HEX\n") return 1 idx = r.find_sha(s) if idx == _NO_RECORD: sys.stderr.write("no such record\n") return 1 else: print idx else: sys.stderr.write("unknown command %r\n" % cmd) return 1 if __name__ == '__main__': import sys sys.exit(main(sys.argv) or 0) commit refs/heads/master mark :209 committer 1113023673 +1000 data 30 Revfile: handle decompression from :208 M 644 inline bzrlib/revfile.py data 12822 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. """ # TODO: Something like pread() would make this slightly simpler and # perhaps more efficient. # TODO: Could also try to mmap things... import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify, unhexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_RECORD = 0xFFFFFFFFL # fields in the index record I_SHA = 0 I_BASE = 1 I_FLAGS = 2 I_OFFSET = 3 I_LEN = 4 FL_GZIP = 1 class RevfileError(Exception): pass class Revfile: def __init__(self, basename): # TODO: Option to open readonly # TODO: Lock file while open # TODO: advise of random access self.basename = basename idxname = basename + '.irev' dataname = basename + '.drev' self.idxpos = 0L idx_exists = os.path.exists(idxname) data_exists = os.path.exists(dataname) if idx_exists != data_exists: raise RevfileError("half-assed revfile") if not idx_exists: self.idxfile = open(idxname, 'w+b') self.datafile = open(dataname, 'w+b') print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: self.idxfile = open(idxname, 'r+b') self.datafile = open(dataname, 'r+b') h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def revision(self, rev): base = self.index[rev][0] start = self.index[base][1] end = self.index[rev][1] + self.index[rev][2] f = open(self.datafile()) f.seek(start) data = f.read(end - start) last = self.index[base][2] text = zlib.decompress(data[:last]) for r in range(base + 1, rev + 1): s = self.index[r][2] b = zlib.decompress(data[last:last + s]) text = mdiff.bpatch(text, b) last = last + s return text def _check_index(self, idx): if idx < 0 or idx > len(self): raise RevfileError("invalid index %r" % idx) def find_sha(self, s): assert isinstance(s, str) assert len(s) == 20 for idx, idxrec in enumerate(self): if idxrec[I_SHA] == s: return idx else: return _NO_RECORD def _add_common(self, text_sha, data, base): """Add pre-processed data, can be either full text or delta. This does the compression if that makes sense.""" flags = 0 data_len = len(data) if data_len > 50: # don't do compression if it's too small; it's unlikely to win # enough to be worthwhile compr_data = zlib.compress(data) compr_len = len(compr_data) if compr_len < data_len: data = compr_data flags = FL_GZIP print '- compressed %d -> %d, %.1f%%' \ % (data_len, compr_len, float(compr_len)/float(data_len) * 100.0) idx = len(self) self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * (idx + 1) data_offset = self.datafile.tell() assert isinstance(data, str) # not unicode or anything wierd self.datafile.write(data) self.datafile.flush() assert isinstance(text_sha, str) entry = text_sha entry += struct.pack(">IIII12x", base, flags, data_offset, len(data)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def _add_full_text(self, text, text_sha): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" return self._add_common(text_sha, text, _NO_RECORD) def _add_delta(self, text, text_sha, base): """Add a text stored relative to a previous text.""" self._check_index(base) base_text = self.get(base) data = mdiff.bdiff(base_text, text) return self._add_common(text_sha, data, base) def add(self, text, base=_NO_RECORD): text_sha = sha.new(text).digest() idx = self.find_sha(text_sha) if idx != _NO_RECORD: return idx # already present if base == _NO_RECORD: return self._add_full_text(text, text_sha) else: return self._add_delta(text, text_sha, base) def addrevision(self, text, changeset): t = self.tip() n = t + 1 if not n % factor: data = zlib.compress(text) base = n else: prev = self.revision(t) data = zlib.compress(mdiff.bdiff(prev, text)) base = self.index[t][0] offset = 0 if t >= 0: offset = self.index[t][1] + self.index[t][2] self.index.append((base, offset, len(data), changeset)) entry = struct.pack(">llll", base, offset, len(data), changeset) open(self.indexfile(), "a").write(entry) open(self.datafile(), "a").write(data) def get(self, idx): idxrec = self[idx] base = idxrec[I_BASE] if base == _NO_RECORD: text = self._get_full_text(idx, idxrec) else: text = self._get_patched(idx, idxrec) if sha.new(text).digest() != idxrec[I_SHA]: raise RevfileError("corrupt SHA-1 digest on record %d" % idx) return text def _get_raw(self, idx, idxrec): flags = idxrec[I_FLAGS] if flags & ~FL_GZIP: raise RevfileError("unsupported index flags %#x on index %d" % (flags, idx)) l = idxrec[I_LEN] if l == 0: return '' self.datafile.seek(idxrec[I_OFFSET]) data = self.datafile.read(l) if len(data) != l: raise RevfileError("short read %d of %d " "getting text for record %d in %r" % (len(data), l, idx, self.basename)) if flags & FL_GZIP: data = zlib.decompress(data) return data def _get_full_text(self, idx, idxrec): assert idxrec[I_BASE] == _NO_RECORD text = self._get_raw(idx, idxrec) return text def _get_patched(self, idx, idxrec): base = idxrec[I_BASE] assert base >= 0 assert base < idx # no loops! base_text = self.get(base) patch = self._get_raw(idx, idxrec) text = mdiff.bpatch(base_text, patch) return text def __len__(self): """Return number of revisions.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) if l < _RECORDSIZE: raise RevfileError("no header present in index of %r" % (self.basename)) return int(l / _RECORDSIZE) - 1 def __getitem__(self, idx): """Index by sequence id returns the index field""" ## TODO: Can avoid seek if we just moved there... self._seek_index(idx) return self._read_next_index() def _seek_index(self, idx): if idx < 0: raise RevfileError("invalid index %r" % idx) self.idxfile.seek((idx + 1) * _RECORDSIZE) def _read_next_index(self): rec = self.idxfile.read(_RECORDSIZE) if not rec: raise IndexError("end of index file") elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_RECORD: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def main(argv): r = Revfile("testrev") try: cmd = argv[1] except IndexError: sys.stderr.write("usage: revfile dump\n" " revfile add\n" " revfile add-delta BASE\n" " revfile get IDX\n" " revfile find-sha HEX\n") return 1 if cmd == 'add': new_idx = r.add(sys.stdin.read()) print 'added idx %d' % new_idx elif cmd == 'add-delta': new_idx = r.add(sys.stdin.read(), int(argv[2])) print 'added idx %d' % new_idx elif cmd == 'dump': r.dump() elif cmd == 'get': try: idx = int(argv[2]) except IndexError: sys.stderr.write("usage: revfile get IDX\n") return 1 if idx < 0 or idx >= len(r): sys.stderr.write("invalid index %r\n" % idx) return 1 sys.stdout.write(r.get(idx)) elif cmd == 'find-sha': try: s = unhexlify(argv[2]) except IndexError: sys.stderr.write("usage: revfile find-sha HEX\n") return 1 idx = r.find_sha(s) if idx == _NO_RECORD: sys.stderr.write("no such record\n") return 1 else: print idx else: sys.stderr.write("unknown command %r\n" % cmd) return 1 if __name__ == '__main__': import sys sys.exit(main(sys.argv) or 0) commit refs/heads/master mark :210 committer 1113023683 +1000 data 16 remove dead code from :209 M 644 inline bzrlib/revfile.py data 12172 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. """ # TODO: Something like pread() would make this slightly simpler and # perhaps more efficient. # TODO: Could also try to mmap things... import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify, unhexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_RECORD = 0xFFFFFFFFL # fields in the index record I_SHA = 0 I_BASE = 1 I_FLAGS = 2 I_OFFSET = 3 I_LEN = 4 FL_GZIP = 1 class RevfileError(Exception): pass class Revfile: def __init__(self, basename): # TODO: Option to open readonly # TODO: Lock file while open # TODO: advise of random access self.basename = basename idxname = basename + '.irev' dataname = basename + '.drev' self.idxpos = 0L idx_exists = os.path.exists(idxname) data_exists = os.path.exists(dataname) if idx_exists != data_exists: raise RevfileError("half-assed revfile") if not idx_exists: self.idxfile = open(idxname, 'w+b') self.datafile = open(dataname, 'w+b') print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: self.idxfile = open(idxname, 'r+b') self.datafile = open(dataname, 'r+b') h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def revision(self, rev): base = self.index[rev][0] start = self.index[base][1] end = self.index[rev][1] + self.index[rev][2] f = open(self.datafile()) f.seek(start) data = f.read(end - start) last = self.index[base][2] text = zlib.decompress(data[:last]) for r in range(base + 1, rev + 1): s = self.index[r][2] b = zlib.decompress(data[last:last + s]) text = mdiff.bpatch(text, b) last = last + s return text def _check_index(self, idx): if idx < 0 or idx > len(self): raise RevfileError("invalid index %r" % idx) def find_sha(self, s): assert isinstance(s, str) assert len(s) == 20 for idx, idxrec in enumerate(self): if idxrec[I_SHA] == s: return idx else: return _NO_RECORD def _add_common(self, text_sha, data, base): """Add pre-processed data, can be either full text or delta. This does the compression if that makes sense.""" flags = 0 data_len = len(data) if data_len > 50: # don't do compression if it's too small; it's unlikely to win # enough to be worthwhile compr_data = zlib.compress(data) compr_len = len(compr_data) if compr_len < data_len: data = compr_data flags = FL_GZIP print '- compressed %d -> %d, %.1f%%' \ % (data_len, compr_len, float(compr_len)/float(data_len) * 100.0) idx = len(self) self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * (idx + 1) data_offset = self.datafile.tell() assert isinstance(data, str) # not unicode or anything wierd self.datafile.write(data) self.datafile.flush() assert isinstance(text_sha, str) entry = text_sha entry += struct.pack(">IIII12x", base, flags, data_offset, len(data)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def _add_full_text(self, text, text_sha): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" return self._add_common(text_sha, text, _NO_RECORD) def _add_delta(self, text, text_sha, base): """Add a text stored relative to a previous text.""" self._check_index(base) base_text = self.get(base) data = mdiff.bdiff(base_text, text) return self._add_common(text_sha, data, base) def add(self, text, base=_NO_RECORD): text_sha = sha.new(text).digest() idx = self.find_sha(text_sha) if idx != _NO_RECORD: return idx # already present if base == _NO_RECORD: return self._add_full_text(text, text_sha) else: return self._add_delta(text, text_sha, base) def get(self, idx): idxrec = self[idx] base = idxrec[I_BASE] if base == _NO_RECORD: text = self._get_full_text(idx, idxrec) else: text = self._get_patched(idx, idxrec) if sha.new(text).digest() != idxrec[I_SHA]: raise RevfileError("corrupt SHA-1 digest on record %d" % idx) return text def _get_raw(self, idx, idxrec): flags = idxrec[I_FLAGS] if flags & ~FL_GZIP: raise RevfileError("unsupported index flags %#x on index %d" % (flags, idx)) l = idxrec[I_LEN] if l == 0: return '' self.datafile.seek(idxrec[I_OFFSET]) data = self.datafile.read(l) if len(data) != l: raise RevfileError("short read %d of %d " "getting text for record %d in %r" % (len(data), l, idx, self.basename)) if flags & FL_GZIP: data = zlib.decompress(data) return data def _get_full_text(self, idx, idxrec): assert idxrec[I_BASE] == _NO_RECORD text = self._get_raw(idx, idxrec) return text def _get_patched(self, idx, idxrec): base = idxrec[I_BASE] assert base >= 0 assert base < idx # no loops! base_text = self.get(base) patch = self._get_raw(idx, idxrec) text = mdiff.bpatch(base_text, patch) return text def __len__(self): """Return number of revisions.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) if l < _RECORDSIZE: raise RevfileError("no header present in index of %r" % (self.basename)) return int(l / _RECORDSIZE) - 1 def __getitem__(self, idx): """Index by sequence id returns the index field""" ## TODO: Can avoid seek if we just moved there... self._seek_index(idx) return self._read_next_index() def _seek_index(self, idx): if idx < 0: raise RevfileError("invalid index %r" % idx) self.idxfile.seek((idx + 1) * _RECORDSIZE) def _read_next_index(self): rec = self.idxfile.read(_RECORDSIZE) if not rec: raise IndexError("end of index file") elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_RECORD: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def main(argv): r = Revfile("testrev") try: cmd = argv[1] except IndexError: sys.stderr.write("usage: revfile dump\n" " revfile add\n" " revfile add-delta BASE\n" " revfile get IDX\n" " revfile find-sha HEX\n") return 1 if cmd == 'add': new_idx = r.add(sys.stdin.read()) print 'added idx %d' % new_idx elif cmd == 'add-delta': new_idx = r.add(sys.stdin.read(), int(argv[2])) print 'added idx %d' % new_idx elif cmd == 'dump': r.dump() elif cmd == 'get': try: idx = int(argv[2]) except IndexError: sys.stderr.write("usage: revfile get IDX\n") return 1 if idx < 0 or idx >= len(r): sys.stderr.write("invalid index %r\n" % idx) return 1 sys.stdout.write(r.get(idx)) elif cmd == 'find-sha': try: s = unhexlify(argv[2]) except IndexError: sys.stderr.write("usage: revfile find-sha HEX\n") return 1 idx = r.find_sha(s) if idx == _NO_RECORD: sys.stderr.write("no such record\n") return 1 else: print idx else: sys.stderr.write("unknown command %r\n" % cmd) return 1 if __name__ == '__main__': import sys sys.exit(main(sys.argv) or 0) commit refs/heads/master mark :211 committer 1113023772 +1000 data 16 remove dead code from :210 M 644 inline bzrlib/revfile.py data 12146 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. """ # TODO: Something like pread() would make this slightly simpler and # perhaps more efficient. # TODO: Could also try to mmap things... import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify, unhexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_RECORD = 0xFFFFFFFFL # fields in the index record I_SHA = 0 I_BASE = 1 I_FLAGS = 2 I_OFFSET = 3 I_LEN = 4 FL_GZIP = 1 class RevfileError(Exception): pass class Revfile: def __init__(self, basename): # TODO: Option to open readonly # TODO: Lock file while open # TODO: advise of random access self.basename = basename idxname = basename + '.irev' dataname = basename + '.drev' idx_exists = os.path.exists(idxname) data_exists = os.path.exists(dataname) if idx_exists != data_exists: raise RevfileError("half-assed revfile") if not idx_exists: self.idxfile = open(idxname, 'w+b') self.datafile = open(dataname, 'w+b') print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: self.idxfile = open(idxname, 'r+b') self.datafile = open(dataname, 'r+b') h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def revision(self, rev): base = self.index[rev][0] start = self.index[base][1] end = self.index[rev][1] + self.index[rev][2] f = open(self.datafile()) f.seek(start) data = f.read(end - start) last = self.index[base][2] text = zlib.decompress(data[:last]) for r in range(base + 1, rev + 1): s = self.index[r][2] b = zlib.decompress(data[last:last + s]) text = mdiff.bpatch(text, b) last = last + s return text def _check_index(self, idx): if idx < 0 or idx > len(self): raise RevfileError("invalid index %r" % idx) def find_sha(self, s): assert isinstance(s, str) assert len(s) == 20 for idx, idxrec in enumerate(self): if idxrec[I_SHA] == s: return idx else: return _NO_RECORD def _add_common(self, text_sha, data, base): """Add pre-processed data, can be either full text or delta. This does the compression if that makes sense.""" flags = 0 data_len = len(data) if data_len > 50: # don't do compression if it's too small; it's unlikely to win # enough to be worthwhile compr_data = zlib.compress(data) compr_len = len(compr_data) if compr_len < data_len: data = compr_data flags = FL_GZIP print '- compressed %d -> %d, %.1f%%' \ % (data_len, compr_len, float(compr_len)/float(data_len) * 100.0) idx = len(self) self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * (idx + 1) data_offset = self.datafile.tell() assert isinstance(data, str) # not unicode or anything wierd self.datafile.write(data) self.datafile.flush() assert isinstance(text_sha, str) entry = text_sha entry += struct.pack(">IIII12x", base, flags, data_offset, len(data)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def _add_full_text(self, text, text_sha): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" return self._add_common(text_sha, text, _NO_RECORD) def _add_delta(self, text, text_sha, base): """Add a text stored relative to a previous text.""" self._check_index(base) base_text = self.get(base) data = mdiff.bdiff(base_text, text) return self._add_common(text_sha, data, base) def add(self, text, base=_NO_RECORD): text_sha = sha.new(text).digest() idx = self.find_sha(text_sha) if idx != _NO_RECORD: return idx # already present if base == _NO_RECORD: return self._add_full_text(text, text_sha) else: return self._add_delta(text, text_sha, base) def get(self, idx): idxrec = self[idx] base = idxrec[I_BASE] if base == _NO_RECORD: text = self._get_full_text(idx, idxrec) else: text = self._get_patched(idx, idxrec) if sha.new(text).digest() != idxrec[I_SHA]: raise RevfileError("corrupt SHA-1 digest on record %d" % idx) return text def _get_raw(self, idx, idxrec): flags = idxrec[I_FLAGS] if flags & ~FL_GZIP: raise RevfileError("unsupported index flags %#x on index %d" % (flags, idx)) l = idxrec[I_LEN] if l == 0: return '' self.datafile.seek(idxrec[I_OFFSET]) data = self.datafile.read(l) if len(data) != l: raise RevfileError("short read %d of %d " "getting text for record %d in %r" % (len(data), l, idx, self.basename)) if flags & FL_GZIP: data = zlib.decompress(data) return data def _get_full_text(self, idx, idxrec): assert idxrec[I_BASE] == _NO_RECORD text = self._get_raw(idx, idxrec) return text def _get_patched(self, idx, idxrec): base = idxrec[I_BASE] assert base >= 0 assert base < idx # no loops! base_text = self.get(base) patch = self._get_raw(idx, idxrec) text = mdiff.bpatch(base_text, patch) return text def __len__(self): """Return number of revisions.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) if l < _RECORDSIZE: raise RevfileError("no header present in index of %r" % (self.basename)) return int(l / _RECORDSIZE) - 1 def __getitem__(self, idx): """Index by sequence id returns the index field""" ## TODO: Can avoid seek if we just moved there... self._seek_index(idx) return self._read_next_index() def _seek_index(self, idx): if idx < 0: raise RevfileError("invalid index %r" % idx) self.idxfile.seek((idx + 1) * _RECORDSIZE) def _read_next_index(self): rec = self.idxfile.read(_RECORDSIZE) if not rec: raise IndexError("end of index file") elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_RECORD: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def main(argv): r = Revfile("testrev") try: cmd = argv[1] except IndexError: sys.stderr.write("usage: revfile dump\n" " revfile add\n" " revfile add-delta BASE\n" " revfile get IDX\n" " revfile find-sha HEX\n") return 1 if cmd == 'add': new_idx = r.add(sys.stdin.read()) print 'added idx %d' % new_idx elif cmd == 'add-delta': new_idx = r.add(sys.stdin.read(), int(argv[2])) print 'added idx %d' % new_idx elif cmd == 'dump': r.dump() elif cmd == 'get': try: idx = int(argv[2]) except IndexError: sys.stderr.write("usage: revfile get IDX\n") return 1 if idx < 0 or idx >= len(r): sys.stderr.write("invalid index %r\n" % idx) return 1 sys.stdout.write(r.get(idx)) elif cmd == 'find-sha': try: s = unhexlify(argv[2]) except IndexError: sys.stderr.write("usage: revfile find-sha HEX\n") return 1 idx = r.find_sha(s) if idx == _NO_RECORD: sys.stderr.write("no such record\n") return 1 else: print idx else: sys.stderr.write("unknown command %r\n" % cmd) return 1 if __name__ == '__main__': import sys sys.exit(main(sys.argv) or 0) commit refs/heads/master mark :212 committer 1113023851 +1000 data 16 remove dead code from :211 M 644 inline bzrlib/revfile.py data 11594 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. """ # TODO: Something like pread() would make this slightly simpler and # perhaps more efficient. # TODO: Could also try to mmap things... import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify, unhexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_RECORD = 0xFFFFFFFFL # fields in the index record I_SHA = 0 I_BASE = 1 I_FLAGS = 2 I_OFFSET = 3 I_LEN = 4 FL_GZIP = 1 class RevfileError(Exception): pass class Revfile: def __init__(self, basename): # TODO: Option to open readonly # TODO: Lock file while open # TODO: advise of random access self.basename = basename idxname = basename + '.irev' dataname = basename + '.drev' idx_exists = os.path.exists(idxname) data_exists = os.path.exists(dataname) if idx_exists != data_exists: raise RevfileError("half-assed revfile") if not idx_exists: self.idxfile = open(idxname, 'w+b') self.datafile = open(dataname, 'w+b') print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: self.idxfile = open(idxname, 'r+b') self.datafile = open(dataname, 'r+b') h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def _check_index(self, idx): if idx < 0 or idx > len(self): raise RevfileError("invalid index %r" % idx) def find_sha(self, s): assert isinstance(s, str) assert len(s) == 20 for idx, idxrec in enumerate(self): if idxrec[I_SHA] == s: return idx else: return _NO_RECORD def _add_common(self, text_sha, data, base): """Add pre-processed data, can be either full text or delta. This does the compression if that makes sense.""" flags = 0 data_len = len(data) if data_len > 50: # don't do compression if it's too small; it's unlikely to win # enough to be worthwhile compr_data = zlib.compress(data) compr_len = len(compr_data) if compr_len < data_len: data = compr_data flags = FL_GZIP print '- compressed %d -> %d, %.1f%%' \ % (data_len, compr_len, float(compr_len)/float(data_len) * 100.0) idx = len(self) self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * (idx + 1) data_offset = self.datafile.tell() assert isinstance(data, str) # not unicode or anything wierd self.datafile.write(data) self.datafile.flush() assert isinstance(text_sha, str) entry = text_sha entry += struct.pack(">IIII12x", base, flags, data_offset, len(data)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def _add_full_text(self, text, text_sha): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" return self._add_common(text_sha, text, _NO_RECORD) def _add_delta(self, text, text_sha, base): """Add a text stored relative to a previous text.""" self._check_index(base) base_text = self.get(base) data = mdiff.bdiff(base_text, text) return self._add_common(text_sha, data, base) def add(self, text, base=_NO_RECORD): text_sha = sha.new(text).digest() idx = self.find_sha(text_sha) if idx != _NO_RECORD: return idx # already present if base == _NO_RECORD: return self._add_full_text(text, text_sha) else: return self._add_delta(text, text_sha, base) def get(self, idx): idxrec = self[idx] base = idxrec[I_BASE] if base == _NO_RECORD: text = self._get_full_text(idx, idxrec) else: text = self._get_patched(idx, idxrec) if sha.new(text).digest() != idxrec[I_SHA]: raise RevfileError("corrupt SHA-1 digest on record %d" % idx) return text def _get_raw(self, idx, idxrec): flags = idxrec[I_FLAGS] if flags & ~FL_GZIP: raise RevfileError("unsupported index flags %#x on index %d" % (flags, idx)) l = idxrec[I_LEN] if l == 0: return '' self.datafile.seek(idxrec[I_OFFSET]) data = self.datafile.read(l) if len(data) != l: raise RevfileError("short read %d of %d " "getting text for record %d in %r" % (len(data), l, idx, self.basename)) if flags & FL_GZIP: data = zlib.decompress(data) return data def _get_full_text(self, idx, idxrec): assert idxrec[I_BASE] == _NO_RECORD text = self._get_raw(idx, idxrec) return text def _get_patched(self, idx, idxrec): base = idxrec[I_BASE] assert base >= 0 assert base < idx # no loops! base_text = self.get(base) patch = self._get_raw(idx, idxrec) text = mdiff.bpatch(base_text, patch) return text def __len__(self): """Return number of revisions.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) if l < _RECORDSIZE: raise RevfileError("no header present in index of %r" % (self.basename)) return int(l / _RECORDSIZE) - 1 def __getitem__(self, idx): """Index by sequence id returns the index field""" ## TODO: Can avoid seek if we just moved there... self._seek_index(idx) return self._read_next_index() def _seek_index(self, idx): if idx < 0: raise RevfileError("invalid index %r" % idx) self.idxfile.seek((idx + 1) * _RECORDSIZE) def _read_next_index(self): rec = self.idxfile.read(_RECORDSIZE) if not rec: raise IndexError("end of index file") elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_RECORD: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def main(argv): r = Revfile("testrev") try: cmd = argv[1] except IndexError: sys.stderr.write("usage: revfile dump\n" " revfile add\n" " revfile add-delta BASE\n" " revfile get IDX\n" " revfile find-sha HEX\n") return 1 if cmd == 'add': new_idx = r.add(sys.stdin.read()) print 'added idx %d' % new_idx elif cmd == 'add-delta': new_idx = r.add(sys.stdin.read(), int(argv[2])) print 'added idx %d' % new_idx elif cmd == 'dump': r.dump() elif cmd == 'get': try: idx = int(argv[2]) except IndexError: sys.stderr.write("usage: revfile get IDX\n") return 1 if idx < 0 or idx >= len(r): sys.stderr.write("invalid index %r\n" % idx) return 1 sys.stdout.write(r.get(idx)) elif cmd == 'find-sha': try: s = unhexlify(argv[2]) except IndexError: sys.stderr.write("usage: revfile find-sha HEX\n") return 1 idx = r.find_sha(s) if idx == _NO_RECORD: sys.stderr.write("no such record\n") return 1 else: print idx else: sys.stderr.write("unknown command %r\n" % cmd) return 1 if __name__ == '__main__': import sys sys.exit(main(sys.argv) or 0) commit refs/heads/master mark :213 committer 1113024025 +1000 data 80 Revfile: don't store deltas if they'd be larger than just storing the whole text from :212 M 644 inline bzrlib/revfile.py data 11933 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. """ # TODO: Something like pread() would make this slightly simpler and # perhaps more efficient. # TODO: Could also try to mmap things... import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify, unhexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_RECORD = 0xFFFFFFFFL # fields in the index record I_SHA = 0 I_BASE = 1 I_FLAGS = 2 I_OFFSET = 3 I_LEN = 4 FL_GZIP = 1 class RevfileError(Exception): pass class Revfile: def __init__(self, basename): # TODO: Option to open readonly # TODO: Lock file while open # TODO: advise of random access self.basename = basename idxname = basename + '.irev' dataname = basename + '.drev' idx_exists = os.path.exists(idxname) data_exists = os.path.exists(dataname) if idx_exists != data_exists: raise RevfileError("half-assed revfile") if not idx_exists: self.idxfile = open(idxname, 'w+b') self.datafile = open(dataname, 'w+b') print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: self.idxfile = open(idxname, 'r+b') self.datafile = open(dataname, 'r+b') h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def _check_index(self, idx): if idx < 0 or idx > len(self): raise RevfileError("invalid index %r" % idx) def find_sha(self, s): assert isinstance(s, str) assert len(s) == 20 for idx, idxrec in enumerate(self): if idxrec[I_SHA] == s: return idx else: return _NO_RECORD def _add_common(self, text_sha, data, base): """Add pre-processed data, can be either full text or delta. This does the compression if that makes sense.""" flags = 0 data_len = len(data) if data_len > 50: # don't do compression if it's too small; it's unlikely to win # enough to be worthwhile compr_data = zlib.compress(data) compr_len = len(compr_data) if compr_len < data_len: data = compr_data flags = FL_GZIP print '- compressed %d -> %d, %.1f%%' \ % (data_len, compr_len, float(compr_len)/float(data_len) * 100.0) idx = len(self) self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * (idx + 1) data_offset = self.datafile.tell() assert isinstance(data, str) # not unicode or anything wierd self.datafile.write(data) self.datafile.flush() assert isinstance(text_sha, str) entry = text_sha entry += struct.pack(">IIII12x", base, flags, data_offset, len(data)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def _add_full_text(self, text, text_sha): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" return self._add_common(text_sha, text, _NO_RECORD) def _add_delta(self, text, text_sha, base): """Add a text stored relative to a previous text.""" self._check_index(base) base_text = self.get(base) data = mdiff.bdiff(base_text, text) # If the delta is larger than the text, we might as well just # store the text. (OK, the delta might be more compressible, # but the overhead of applying it probably still makes it # bad.) if len(data) >= len(text): return self._add_full_text(text, text_sha) else: return self._add_common(text_sha, data, base) def add(self, text, base=_NO_RECORD): text_sha = sha.new(text).digest() idx = self.find_sha(text_sha) if idx != _NO_RECORD: return idx # already present if base == _NO_RECORD: return self._add_full_text(text, text_sha) else: return self._add_delta(text, text_sha, base) def get(self, idx): idxrec = self[idx] base = idxrec[I_BASE] if base == _NO_RECORD: text = self._get_full_text(idx, idxrec) else: text = self._get_patched(idx, idxrec) if sha.new(text).digest() != idxrec[I_SHA]: raise RevfileError("corrupt SHA-1 digest on record %d" % idx) return text def _get_raw(self, idx, idxrec): flags = idxrec[I_FLAGS] if flags & ~FL_GZIP: raise RevfileError("unsupported index flags %#x on index %d" % (flags, idx)) l = idxrec[I_LEN] if l == 0: return '' self.datafile.seek(idxrec[I_OFFSET]) data = self.datafile.read(l) if len(data) != l: raise RevfileError("short read %d of %d " "getting text for record %d in %r" % (len(data), l, idx, self.basename)) if flags & FL_GZIP: data = zlib.decompress(data) return data def _get_full_text(self, idx, idxrec): assert idxrec[I_BASE] == _NO_RECORD text = self._get_raw(idx, idxrec) return text def _get_patched(self, idx, idxrec): base = idxrec[I_BASE] assert base >= 0 assert base < idx # no loops! base_text = self.get(base) patch = self._get_raw(idx, idxrec) text = mdiff.bpatch(base_text, patch) return text def __len__(self): """Return number of revisions.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) if l < _RECORDSIZE: raise RevfileError("no header present in index of %r" % (self.basename)) return int(l / _RECORDSIZE) - 1 def __getitem__(self, idx): """Index by sequence id returns the index field""" ## TODO: Can avoid seek if we just moved there... self._seek_index(idx) return self._read_next_index() def _seek_index(self, idx): if idx < 0: raise RevfileError("invalid index %r" % idx) self.idxfile.seek((idx + 1) * _RECORDSIZE) def _read_next_index(self): rec = self.idxfile.read(_RECORDSIZE) if not rec: raise IndexError("end of index file") elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_RECORD: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def main(argv): r = Revfile("testrev") try: cmd = argv[1] except IndexError: sys.stderr.write("usage: revfile dump\n" " revfile add\n" " revfile add-delta BASE\n" " revfile get IDX\n" " revfile find-sha HEX\n") return 1 if cmd == 'add': new_idx = r.add(sys.stdin.read()) print 'added idx %d' % new_idx elif cmd == 'add-delta': new_idx = r.add(sys.stdin.read(), int(argv[2])) print 'added idx %d' % new_idx elif cmd == 'dump': r.dump() elif cmd == 'get': try: idx = int(argv[2]) except IndexError: sys.stderr.write("usage: revfile get IDX\n") return 1 if idx < 0 or idx >= len(r): sys.stderr.write("invalid index %r\n" % idx) return 1 sys.stdout.write(r.get(idx)) elif cmd == 'find-sha': try: s = unhexlify(argv[2]) except IndexError: sys.stderr.write("usage: revfile find-sha HEX\n") return 1 idx = r.find_sha(s) if idx == _NO_RECORD: sys.stderr.write("no such record\n") return 1 else: print idx else: sys.stderr.write("unknown command %r\n" % cmd) return 1 if __name__ == '__main__': import sys sys.exit(main(sys.argv) or 0) commit refs/heads/master mark :214 committer 1113024054 +1000 data 3 doc from :213 M 644 inline bzrlib/revfile.py data 11988 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. """ # TODO: Something like pread() would make this slightly simpler and # perhaps more efficient. # TODO: Could also try to mmap things... import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify, unhexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_RECORD = 0xFFFFFFFFL # fields in the index record I_SHA = 0 I_BASE = 1 I_FLAGS = 2 I_OFFSET = 3 I_LEN = 4 FL_GZIP = 1 class RevfileError(Exception): pass class Revfile: def __init__(self, basename): # TODO: Option to open readonly # TODO: Lock file while open # TODO: advise of random access self.basename = basename idxname = basename + '.irev' dataname = basename + '.drev' idx_exists = os.path.exists(idxname) data_exists = os.path.exists(dataname) if idx_exists != data_exists: raise RevfileError("half-assed revfile") if not idx_exists: self.idxfile = open(idxname, 'w+b') self.datafile = open(dataname, 'w+b') print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: self.idxfile = open(idxname, 'r+b') self.datafile = open(dataname, 'r+b') h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def _check_index(self, idx): if idx < 0 or idx > len(self): raise RevfileError("invalid index %r" % idx) def find_sha(self, s): assert isinstance(s, str) assert len(s) == 20 for idx, idxrec in enumerate(self): if idxrec[I_SHA] == s: return idx else: return _NO_RECORD def _add_common(self, text_sha, data, base): """Add pre-processed data, can be either full text or delta. This does the compression if that makes sense.""" flags = 0 data_len = len(data) if data_len > 50: # don't do compression if it's too small; it's unlikely to win # enough to be worthwhile compr_data = zlib.compress(data) compr_len = len(compr_data) if compr_len < data_len: data = compr_data flags = FL_GZIP print '- compressed %d -> %d, %.1f%%' \ % (data_len, compr_len, float(compr_len)/float(data_len) * 100.0) idx = len(self) self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * (idx + 1) data_offset = self.datafile.tell() assert isinstance(data, str) # not unicode or anything wierd self.datafile.write(data) self.datafile.flush() assert isinstance(text_sha, str) entry = text_sha entry += struct.pack(">IIII12x", base, flags, data_offset, len(data)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def _add_full_text(self, text, text_sha): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" return self._add_common(text_sha, text, _NO_RECORD) def _add_delta(self, text, text_sha, base): """Add a text stored relative to a previous text.""" self._check_index(base) base_text = self.get(base) data = mdiff.bdiff(base_text, text) # If the delta is larger than the text, we might as well just # store the text. (OK, the delta might be more compressible, # but the overhead of applying it probably still makes it # bad, and I don't want to compress both of them to find out.) if len(data) >= len(text): return self._add_full_text(text, text_sha) else: return self._add_common(text_sha, data, base) def add(self, text, base=_NO_RECORD): text_sha = sha.new(text).digest() idx = self.find_sha(text_sha) if idx != _NO_RECORD: return idx # already present if base == _NO_RECORD: return self._add_full_text(text, text_sha) else: return self._add_delta(text, text_sha, base) def get(self, idx): idxrec = self[idx] base = idxrec[I_BASE] if base == _NO_RECORD: text = self._get_full_text(idx, idxrec) else: text = self._get_patched(idx, idxrec) if sha.new(text).digest() != idxrec[I_SHA]: raise RevfileError("corrupt SHA-1 digest on record %d" % idx) return text def _get_raw(self, idx, idxrec): flags = idxrec[I_FLAGS] if flags & ~FL_GZIP: raise RevfileError("unsupported index flags %#x on index %d" % (flags, idx)) l = idxrec[I_LEN] if l == 0: return '' self.datafile.seek(idxrec[I_OFFSET]) data = self.datafile.read(l) if len(data) != l: raise RevfileError("short read %d of %d " "getting text for record %d in %r" % (len(data), l, idx, self.basename)) if flags & FL_GZIP: data = zlib.decompress(data) return data def _get_full_text(self, idx, idxrec): assert idxrec[I_BASE] == _NO_RECORD text = self._get_raw(idx, idxrec) return text def _get_patched(self, idx, idxrec): base = idxrec[I_BASE] assert base >= 0 assert base < idx # no loops! base_text = self.get(base) patch = self._get_raw(idx, idxrec) text = mdiff.bpatch(base_text, patch) return text def __len__(self): """Return number of revisions.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) if l < _RECORDSIZE: raise RevfileError("no header present in index of %r" % (self.basename)) return int(l / _RECORDSIZE) - 1 def __getitem__(self, idx): """Index by sequence id returns the index field""" ## TODO: Can avoid seek if we just moved there... self._seek_index(idx) return self._read_next_index() def _seek_index(self, idx): if idx < 0: raise RevfileError("invalid index %r" % idx) self.idxfile.seek((idx + 1) * _RECORDSIZE) def _read_next_index(self): rec = self.idxfile.read(_RECORDSIZE) if not rec: raise IndexError("end of index file") elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_RECORD: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def main(argv): r = Revfile("testrev") try: cmd = argv[1] except IndexError: sys.stderr.write("usage: revfile dump\n" " revfile add\n" " revfile add-delta BASE\n" " revfile get IDX\n" " revfile find-sha HEX\n") return 1 if cmd == 'add': new_idx = r.add(sys.stdin.read()) print 'added idx %d' % new_idx elif cmd == 'add-delta': new_idx = r.add(sys.stdin.read(), int(argv[2])) print 'added idx %d' % new_idx elif cmd == 'dump': r.dump() elif cmd == 'get': try: idx = int(argv[2]) except IndexError: sys.stderr.write("usage: revfile get IDX\n") return 1 if idx < 0 or idx >= len(r): sys.stderr.write("invalid index %r\n" % idx) return 1 sys.stdout.write(r.get(idx)) elif cmd == 'find-sha': try: s = unhexlify(argv[2]) except IndexError: sys.stderr.write("usage: revfile find-sha HEX\n") return 1 idx = r.find_sha(s) if idx == _NO_RECORD: sys.stderr.write("no such record\n") return 1 else: print idx else: sys.stderr.write("unknown command %r\n" % cmd) return 1 if __name__ == '__main__': import sys sys.exit(main(sys.argv) or 0) commit refs/heads/master mark :215 committer 1113024296 +1000 data 3 Doc from :214 M 644 inline bzrlib/revfile.py data 12571 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. """ # TODO: Something like pread() would make this slightly simpler and # perhaps more efficient. # TODO: Could also try to mmap things... import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify, unhexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_RECORD = 0xFFFFFFFFL # fields in the index record I_SHA = 0 I_BASE = 1 I_FLAGS = 2 I_OFFSET = 3 I_LEN = 4 FL_GZIP = 1 class RevfileError(Exception): pass class Revfile: def __init__(self, basename): # TODO: Option to open readonly # TODO: Lock file while open # TODO: advise of random access self.basename = basename idxname = basename + '.irev' dataname = basename + '.drev' idx_exists = os.path.exists(idxname) data_exists = os.path.exists(dataname) if idx_exists != data_exists: raise RevfileError("half-assed revfile") if not idx_exists: self.idxfile = open(idxname, 'w+b') self.datafile = open(dataname, 'w+b') print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: self.idxfile = open(idxname, 'r+b') self.datafile = open(dataname, 'r+b') h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def _check_index(self, idx): if idx < 0 or idx > len(self): raise RevfileError("invalid index %r" % idx) def find_sha(self, s): assert isinstance(s, str) assert len(s) == 20 for idx, idxrec in enumerate(self): if idxrec[I_SHA] == s: return idx else: return _NO_RECORD def _add_common(self, text_sha, data, base): """Add pre-processed data, can be either full text or delta. This does the compression if that makes sense.""" flags = 0 data_len = len(data) if data_len > 50: # don't do compression if it's too small; it's unlikely to win # enough to be worthwhile compr_data = zlib.compress(data) compr_len = len(compr_data) if compr_len < data_len: data = compr_data flags = FL_GZIP print '- compressed %d -> %d, %.1f%%' \ % (data_len, compr_len, float(compr_len)/float(data_len) * 100.0) idx = len(self) self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * (idx + 1) data_offset = self.datafile.tell() assert isinstance(data, str) # not unicode or anything wierd self.datafile.write(data) self.datafile.flush() assert isinstance(text_sha, str) entry = text_sha entry += struct.pack(">IIII12x", base, flags, data_offset, len(data)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def _add_full_text(self, text, text_sha): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" return self._add_common(text_sha, text, _NO_RECORD) def _add_delta(self, text, text_sha, base): """Add a text stored relative to a previous text.""" self._check_index(base) base_text = self.get(base) data = mdiff.bdiff(base_text, text) # If the delta is larger than the text, we might as well just # store the text. (OK, the delta might be more compressible, # but the overhead of applying it probably still makes it # bad, and I don't want to compress both of them to find out.) if len(data) >= len(text): return self._add_full_text(text, text_sha) else: return self._add_common(text_sha, data, base) def add(self, text, base=_NO_RECORD): """Add a new text to the revfile. If the text is already present them its existing id is returned and the file is not changed. If a base index is specified, that text *may* be used for delta compression of the new text. Delta compression will only be used if it would be a size win and if the existing base is not at too long of a delta chain already. """ text_sha = sha.new(text).digest() idx = self.find_sha(text_sha) if idx != _NO_RECORD: # TODO: Optional paranoid mode where we read out that record and make sure # it's the same, in case someone ever breaks SHA-1. return idx # already present if base == _NO_RECORD: return self._add_full_text(text, text_sha) else: return self._add_delta(text, text_sha, base) def get(self, idx): idxrec = self[idx] base = idxrec[I_BASE] if base == _NO_RECORD: text = self._get_full_text(idx, idxrec) else: text = self._get_patched(idx, idxrec) if sha.new(text).digest() != idxrec[I_SHA]: raise RevfileError("corrupt SHA-1 digest on record %d" % idx) return text def _get_raw(self, idx, idxrec): flags = idxrec[I_FLAGS] if flags & ~FL_GZIP: raise RevfileError("unsupported index flags %#x on index %d" % (flags, idx)) l = idxrec[I_LEN] if l == 0: return '' self.datafile.seek(idxrec[I_OFFSET]) data = self.datafile.read(l) if len(data) != l: raise RevfileError("short read %d of %d " "getting text for record %d in %r" % (len(data), l, idx, self.basename)) if flags & FL_GZIP: data = zlib.decompress(data) return data def _get_full_text(self, idx, idxrec): assert idxrec[I_BASE] == _NO_RECORD text = self._get_raw(idx, idxrec) return text def _get_patched(self, idx, idxrec): base = idxrec[I_BASE] assert base >= 0 assert base < idx # no loops! base_text = self.get(base) patch = self._get_raw(idx, idxrec) text = mdiff.bpatch(base_text, patch) return text def __len__(self): """Return number of revisions.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) if l < _RECORDSIZE: raise RevfileError("no header present in index of %r" % (self.basename)) return int(l / _RECORDSIZE) - 1 def __getitem__(self, idx): """Index by sequence id returns the index field""" ## TODO: Can avoid seek if we just moved there... self._seek_index(idx) return self._read_next_index() def _seek_index(self, idx): if idx < 0: raise RevfileError("invalid index %r" % idx) self.idxfile.seek((idx + 1) * _RECORDSIZE) def _read_next_index(self): rec = self.idxfile.read(_RECORDSIZE) if not rec: raise IndexError("end of index file") elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_RECORD: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def main(argv): r = Revfile("testrev") try: cmd = argv[1] except IndexError: sys.stderr.write("usage: revfile dump\n" " revfile add\n" " revfile add-delta BASE\n" " revfile get IDX\n" " revfile find-sha HEX\n") return 1 if cmd == 'add': new_idx = r.add(sys.stdin.read()) print 'added idx %d' % new_idx elif cmd == 'add-delta': new_idx = r.add(sys.stdin.read(), int(argv[2])) print 'added idx %d' % new_idx elif cmd == 'dump': r.dump() elif cmd == 'get': try: idx = int(argv[2]) except IndexError: sys.stderr.write("usage: revfile get IDX\n") return 1 if idx < 0 or idx >= len(r): sys.stderr.write("invalid index %r\n" % idx) return 1 sys.stdout.write(r.get(idx)) elif cmd == 'find-sha': try: s = unhexlify(argv[2]) except IndexError: sys.stderr.write("usage: revfile find-sha HEX\n") return 1 idx = r.find_sha(s) if idx == _NO_RECORD: sys.stderr.write("no such record\n") return 1 else: print idx else: sys.stderr.write("unknown command %r\n" % cmd) return 1 if __name__ == '__main__': import sys sys.exit(main(sys.argv) or 0) commit refs/heads/master mark :216 committer 1113024411 +1000 data 74 revfile add and add-delta commands print just the index for use by scripts from :215 M 644 inline bzrlib/revfile.py data 12528 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. """ # TODO: Something like pread() would make this slightly simpler and # perhaps more efficient. # TODO: Could also try to mmap things... import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify, unhexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_RECORD = 0xFFFFFFFFL # fields in the index record I_SHA = 0 I_BASE = 1 I_FLAGS = 2 I_OFFSET = 3 I_LEN = 4 FL_GZIP = 1 class RevfileError(Exception): pass class Revfile: def __init__(self, basename): # TODO: Option to open readonly # TODO: Lock file while open # TODO: advise of random access self.basename = basename idxname = basename + '.irev' dataname = basename + '.drev' idx_exists = os.path.exists(idxname) data_exists = os.path.exists(dataname) if idx_exists != data_exists: raise RevfileError("half-assed revfile") if not idx_exists: self.idxfile = open(idxname, 'w+b') self.datafile = open(dataname, 'w+b') print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: self.idxfile = open(idxname, 'r+b') self.datafile = open(dataname, 'r+b') h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def _check_index(self, idx): if idx < 0 or idx > len(self): raise RevfileError("invalid index %r" % idx) def find_sha(self, s): assert isinstance(s, str) assert len(s) == 20 for idx, idxrec in enumerate(self): if idxrec[I_SHA] == s: return idx else: return _NO_RECORD def _add_common(self, text_sha, data, base): """Add pre-processed data, can be either full text or delta. This does the compression if that makes sense.""" flags = 0 data_len = len(data) if data_len > 50: # don't do compression if it's too small; it's unlikely to win # enough to be worthwhile compr_data = zlib.compress(data) compr_len = len(compr_data) if compr_len < data_len: data = compr_data flags = FL_GZIP print '- compressed %d -> %d, %.1f%%' \ % (data_len, compr_len, float(compr_len)/float(data_len) * 100.0) idx = len(self) self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * (idx + 1) data_offset = self.datafile.tell() assert isinstance(data, str) # not unicode or anything wierd self.datafile.write(data) self.datafile.flush() assert isinstance(text_sha, str) entry = text_sha entry += struct.pack(">IIII12x", base, flags, data_offset, len(data)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def _add_full_text(self, text, text_sha): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" return self._add_common(text_sha, text, _NO_RECORD) def _add_delta(self, text, text_sha, base): """Add a text stored relative to a previous text.""" self._check_index(base) base_text = self.get(base) data = mdiff.bdiff(base_text, text) # If the delta is larger than the text, we might as well just # store the text. (OK, the delta might be more compressible, # but the overhead of applying it probably still makes it # bad, and I don't want to compress both of them to find out.) if len(data) >= len(text): return self._add_full_text(text, text_sha) else: return self._add_common(text_sha, data, base) def add(self, text, base=_NO_RECORD): """Add a new text to the revfile. If the text is already present them its existing id is returned and the file is not changed. If a base index is specified, that text *may* be used for delta compression of the new text. Delta compression will only be used if it would be a size win and if the existing base is not at too long of a delta chain already. """ text_sha = sha.new(text).digest() idx = self.find_sha(text_sha) if idx != _NO_RECORD: # TODO: Optional paranoid mode where we read out that record and make sure # it's the same, in case someone ever breaks SHA-1. return idx # already present if base == _NO_RECORD: return self._add_full_text(text, text_sha) else: return self._add_delta(text, text_sha, base) def get(self, idx): idxrec = self[idx] base = idxrec[I_BASE] if base == _NO_RECORD: text = self._get_full_text(idx, idxrec) else: text = self._get_patched(idx, idxrec) if sha.new(text).digest() != idxrec[I_SHA]: raise RevfileError("corrupt SHA-1 digest on record %d" % idx) return text def _get_raw(self, idx, idxrec): flags = idxrec[I_FLAGS] if flags & ~FL_GZIP: raise RevfileError("unsupported index flags %#x on index %d" % (flags, idx)) l = idxrec[I_LEN] if l == 0: return '' self.datafile.seek(idxrec[I_OFFSET]) data = self.datafile.read(l) if len(data) != l: raise RevfileError("short read %d of %d " "getting text for record %d in %r" % (len(data), l, idx, self.basename)) if flags & FL_GZIP: data = zlib.decompress(data) return data def _get_full_text(self, idx, idxrec): assert idxrec[I_BASE] == _NO_RECORD text = self._get_raw(idx, idxrec) return text def _get_patched(self, idx, idxrec): base = idxrec[I_BASE] assert base >= 0 assert base < idx # no loops! base_text = self.get(base) patch = self._get_raw(idx, idxrec) text = mdiff.bpatch(base_text, patch) return text def __len__(self): """Return number of revisions.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) if l < _RECORDSIZE: raise RevfileError("no header present in index of %r" % (self.basename)) return int(l / _RECORDSIZE) - 1 def __getitem__(self, idx): """Index by sequence id returns the index field""" ## TODO: Can avoid seek if we just moved there... self._seek_index(idx) return self._read_next_index() def _seek_index(self, idx): if idx < 0: raise RevfileError("invalid index %r" % idx) self.idxfile.seek((idx + 1) * _RECORDSIZE) def _read_next_index(self): rec = self.idxfile.read(_RECORDSIZE) if not rec: raise IndexError("end of index file") elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_RECORD: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def main(argv): r = Revfile("testrev") try: cmd = argv[1] except IndexError: sys.stderr.write("usage: revfile dump\n" " revfile add\n" " revfile add-delta BASE\n" " revfile get IDX\n" " revfile find-sha HEX\n") return 1 if cmd == 'add': new_idx = r.add(sys.stdin.read()) print new_idx elif cmd == 'add-delta': new_idx = r.add(sys.stdin.read(), int(argv[2])) print new_idx elif cmd == 'dump': r.dump() elif cmd == 'get': try: idx = int(argv[2]) except IndexError: sys.stderr.write("usage: revfile get IDX\n") return 1 if idx < 0 or idx >= len(r): sys.stderr.write("invalid index %r\n" % idx) return 1 sys.stdout.write(r.get(idx)) elif cmd == 'find-sha': try: s = unhexlify(argv[2]) except IndexError: sys.stderr.write("usage: revfile find-sha HEX\n") return 1 idx = r.find_sha(s) if idx == _NO_RECORD: sys.stderr.write("no such record\n") return 1 else: print idx else: sys.stderr.write("unknown command %r\n" % cmd) return 1 if __name__ == '__main__': import sys sys.exit(main(sys.argv) or 0) commit refs/heads/master mark :217 committer 1113024910 +1000 data 93 Revfile: make compression optional, in case people are storing files they know won't compress from :216 M 644 inline bzrlib/revfile.py data 12926 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. """ # TODO: Something like pread() would make this slightly simpler and # perhaps more efficient. # TODO: Could also try to mmap things... import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify, unhexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_RECORD = 0xFFFFFFFFL # fields in the index record I_SHA = 0 I_BASE = 1 I_FLAGS = 2 I_OFFSET = 3 I_LEN = 4 FL_GZIP = 1 class RevfileError(Exception): pass class Revfile: def __init__(self, basename): # TODO: Option to open readonly # TODO: Lock file while open # TODO: advise of random access self.basename = basename idxname = basename + '.irev' dataname = basename + '.drev' idx_exists = os.path.exists(idxname) data_exists = os.path.exists(dataname) if idx_exists != data_exists: raise RevfileError("half-assed revfile") if not idx_exists: self.idxfile = open(idxname, 'w+b') self.datafile = open(dataname, 'w+b') print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: self.idxfile = open(idxname, 'r+b') self.datafile = open(dataname, 'r+b') h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def _check_index(self, idx): if idx < 0 or idx > len(self): raise RevfileError("invalid index %r" % idx) def find_sha(self, s): assert isinstance(s, str) assert len(s) == 20 for idx, idxrec in enumerate(self): if idxrec[I_SHA] == s: return idx else: return _NO_RECORD def _add_compressed(self, text_sha, data, base, compress): # well, maybe compress flags = 0 if compress: data_len = len(data) if data_len > 50: # don't do compression if it's too small; it's unlikely to win # enough to be worthwhile compr_data = zlib.compress(data) compr_len = len(compr_data) if compr_len < data_len: data = compr_data flags = FL_GZIP ##print '- compressed %d -> %d, %.1f%%' \ ## % (data_len, compr_len, float(compr_len)/float(data_len) * 100.0) return self._add_raw(text_sha, data, base, flags) def _add_raw(self, text_sha, data, base, flags): """Add pre-processed data, can be either full text or delta. This does the compression if that makes sense.""" idx = len(self) self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * (idx + 1) data_offset = self.datafile.tell() assert isinstance(data, str) # not unicode or anything wierd self.datafile.write(data) self.datafile.flush() assert isinstance(text_sha, str) entry = text_sha entry += struct.pack(">IIII12x", base, flags, data_offset, len(data)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def _add_full_text(self, text, text_sha): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" return self._add_compressed(text_sha, text, _NO_RECORD, compress) def _add_delta(self, text, text_sha, base, compress): """Add a text stored relative to a previous text.""" self._check_index(base) base_text = self.get(base) data = mdiff.bdiff(base_text, text) # If the delta is larger than the text, we might as well just # store the text. (OK, the delta might be more compressible, # but the overhead of applying it probably still makes it # bad, and I don't want to compress both of them to find out.) if len(data) >= len(text): return self._add_full_text(text, text_sha, compress) else: return self._add_compressed(text_sha, data, base, compress) def add(self, text, base=_NO_RECORD, compress=True): """Add a new text to the revfile. If the text is already present them its existing id is returned and the file is not changed. If compress is true then gzip compression will be used if it reduces the size. If a base index is specified, that text *may* be used for delta compression of the new text. Delta compression will only be used if it would be a size win and if the existing base is not at too long of a delta chain already. """ text_sha = sha.new(text).digest() idx = self.find_sha(text_sha) if idx != _NO_RECORD: # TODO: Optional paranoid mode where we read out that record and make sure # it's the same, in case someone ever breaks SHA-1. return idx # already present if base == _NO_RECORD: return self._add_full_text(text, text_sha, compress) else: return self._add_delta(text, text_sha, base, compress) def get(self, idx): idxrec = self[idx] base = idxrec[I_BASE] if base == _NO_RECORD: text = self._get_full_text(idx, idxrec) else: text = self._get_patched(idx, idxrec) if sha.new(text).digest() != idxrec[I_SHA]: raise RevfileError("corrupt SHA-1 digest on record %d" % idx) return text def _get_raw(self, idx, idxrec): flags = idxrec[I_FLAGS] if flags & ~FL_GZIP: raise RevfileError("unsupported index flags %#x on index %d" % (flags, idx)) l = idxrec[I_LEN] if l == 0: return '' self.datafile.seek(idxrec[I_OFFSET]) data = self.datafile.read(l) if len(data) != l: raise RevfileError("short read %d of %d " "getting text for record %d in %r" % (len(data), l, idx, self.basename)) if flags & FL_GZIP: data = zlib.decompress(data) return data def _get_full_text(self, idx, idxrec): assert idxrec[I_BASE] == _NO_RECORD text = self._get_raw(idx, idxrec) return text def _get_patched(self, idx, idxrec): base = idxrec[I_BASE] assert base >= 0 assert base < idx # no loops! base_text = self.get(base) patch = self._get_raw(idx, idxrec) text = mdiff.bpatch(base_text, patch) return text def __len__(self): """Return number of revisions.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) if l < _RECORDSIZE: raise RevfileError("no header present in index of %r" % (self.basename)) return int(l / _RECORDSIZE) - 1 def __getitem__(self, idx): """Index by sequence id returns the index field""" ## TODO: Can avoid seek if we just moved there... self._seek_index(idx) return self._read_next_index() def _seek_index(self, idx): if idx < 0: raise RevfileError("invalid index %r" % idx) self.idxfile.seek((idx + 1) * _RECORDSIZE) def _read_next_index(self): rec = self.idxfile.read(_RECORDSIZE) if not rec: raise IndexError("end of index file") elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_RECORD: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def main(argv): r = Revfile("testrev") try: cmd = argv[1] except IndexError: sys.stderr.write("usage: revfile dump\n" " revfile add\n" " revfile add-delta BASE\n" " revfile get IDX\n" " revfile find-sha HEX\n") return 1 if cmd == 'add': new_idx = r.add(sys.stdin.read()) print new_idx elif cmd == 'add-delta': new_idx = r.add(sys.stdin.read(), int(argv[2])) print new_idx elif cmd == 'dump': r.dump() elif cmd == 'get': try: idx = int(argv[2]) except IndexError: sys.stderr.write("usage: revfile get IDX\n") return 1 if idx < 0 or idx >= len(r): sys.stderr.write("invalid index %r\n" % idx) return 1 sys.stdout.write(r.get(idx)) elif cmd == 'find-sha': try: s = unhexlify(argv[2]) except IndexError: sys.stderr.write("usage: revfile find-sha HEX\n") return 1 idx = r.find_sha(s) if idx == _NO_RECORD: sys.stderr.write("no such record\n") return 1 else: print idx else: sys.stderr.write("unknown command %r\n" % cmd) return 1 if __name__ == '__main__': import sys sys.exit(main(sys.argv) or 0) commit refs/heads/master mark :218 committer 1113024979 +1000 data 4 todo from :217 M 644 inline bzrlib/revfile.py data 13045 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. """ # TODO: Something like pread() would make this slightly simpler and # perhaps more efficient. # TODO: Could also try to mmap things... import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify, unhexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_RECORD = 0xFFFFFFFFL # fields in the index record I_SHA = 0 I_BASE = 1 I_FLAGS = 2 I_OFFSET = 3 I_LEN = 4 FL_GZIP = 1 class RevfileError(Exception): pass class Revfile: def __init__(self, basename): # TODO: Option to open readonly # TODO: Lock file while open # TODO: advise of random access self.basename = basename idxname = basename + '.irev' dataname = basename + '.drev' idx_exists = os.path.exists(idxname) data_exists = os.path.exists(dataname) if idx_exists != data_exists: raise RevfileError("half-assed revfile") if not idx_exists: self.idxfile = open(idxname, 'w+b') self.datafile = open(dataname, 'w+b') print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: self.idxfile = open(idxname, 'r+b') self.datafile = open(dataname, 'r+b') h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def _check_index(self, idx): if idx < 0 or idx > len(self): raise RevfileError("invalid index %r" % idx) def find_sha(self, s): assert isinstance(s, str) assert len(s) == 20 for idx, idxrec in enumerate(self): if idxrec[I_SHA] == s: return idx else: return _NO_RECORD def _add_compressed(self, text_sha, data, base, compress): # well, maybe compress flags = 0 if compress: data_len = len(data) if data_len > 50: # don't do compression if it's too small; it's unlikely to win # enough to be worthwhile compr_data = zlib.compress(data) compr_len = len(compr_data) if compr_len < data_len: data = compr_data flags = FL_GZIP ##print '- compressed %d -> %d, %.1f%%' \ ## % (data_len, compr_len, float(compr_len)/float(data_len) * 100.0) return self._add_raw(text_sha, data, base, flags) def _add_raw(self, text_sha, data, base, flags): """Add pre-processed data, can be either full text or delta. This does the compression if that makes sense.""" idx = len(self) self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * (idx + 1) data_offset = self.datafile.tell() assert isinstance(data, str) # not unicode or anything wierd self.datafile.write(data) self.datafile.flush() assert isinstance(text_sha, str) entry = text_sha entry += struct.pack(">IIII12x", base, flags, data_offset, len(data)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def _add_full_text(self, text, text_sha): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" return self._add_compressed(text_sha, text, _NO_RECORD, compress) def _add_delta(self, text, text_sha, base, compress): """Add a text stored relative to a previous text.""" self._check_index(base) base_text = self.get(base) data = mdiff.bdiff(base_text, text) # If the delta is larger than the text, we might as well just # store the text. (OK, the delta might be more compressible, # but the overhead of applying it probably still makes it # bad, and I don't want to compress both of them to find out.) if len(data) >= len(text): return self._add_full_text(text, text_sha, compress) else: return self._add_compressed(text_sha, data, base, compress) def add(self, text, base=_NO_RECORD, compress=True): """Add a new text to the revfile. If the text is already present them its existing id is returned and the file is not changed. If compress is true then gzip compression will be used if it reduces the size. If a base index is specified, that text *may* be used for delta compression of the new text. Delta compression will only be used if it would be a size win and if the existing base is not at too long of a delta chain already. """ text_sha = sha.new(text).digest() idx = self.find_sha(text_sha) if idx != _NO_RECORD: # TODO: Optional paranoid mode where we read out that record and make sure # it's the same, in case someone ever breaks SHA-1. return idx # already present if base == _NO_RECORD: return self._add_full_text(text, text_sha, compress) else: return self._add_delta(text, text_sha, base, compress) def get(self, idx): idxrec = self[idx] base = idxrec[I_BASE] if base == _NO_RECORD: text = self._get_full_text(idx, idxrec) else: text = self._get_patched(idx, idxrec) if sha.new(text).digest() != idxrec[I_SHA]: raise RevfileError("corrupt SHA-1 digest on record %d" % idx) return text def _get_raw(self, idx, idxrec): flags = idxrec[I_FLAGS] if flags & ~FL_GZIP: raise RevfileError("unsupported index flags %#x on index %d" % (flags, idx)) l = idxrec[I_LEN] if l == 0: return '' self.datafile.seek(idxrec[I_OFFSET]) data = self.datafile.read(l) if len(data) != l: raise RevfileError("short read %d of %d " "getting text for record %d in %r" % (len(data), l, idx, self.basename)) if flags & FL_GZIP: data = zlib.decompress(data) return data def _get_full_text(self, idx, idxrec): assert idxrec[I_BASE] == _NO_RECORD text = self._get_raw(idx, idxrec) return text def _get_patched(self, idx, idxrec): base = idxrec[I_BASE] assert base >= 0 assert base < idx # no loops! base_text = self.get(base) patch = self._get_raw(idx, idxrec) text = mdiff.bpatch(base_text, patch) return text def __len__(self): """Return number of revisions.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) if l < _RECORDSIZE: raise RevfileError("no header present in index of %r" % (self.basename)) return int(l / _RECORDSIZE) - 1 def __getitem__(self, idx): """Index by sequence id returns the index field""" ## TODO: Can avoid seek if we just moved there... self._seek_index(idx) return self._read_next_index() def _seek_index(self, idx): if idx < 0: raise RevfileError("invalid index %r" % idx) self.idxfile.seek((idx + 1) * _RECORDSIZE) def _read_next_index(self): rec = self.idxfile.read(_RECORDSIZE) if not rec: raise IndexError("end of index file") elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_RECORD: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def main(argv): r = Revfile("testrev") try: cmd = argv[1] except IndexError: sys.stderr.write("usage: revfile dump\n" " revfile add\n" " revfile add-delta BASE\n" " revfile get IDX\n" " revfile find-sha HEX\n") return 1 # TODO: Something to show compression ratio, e.g. total size of # all files vs size of index and data files. if cmd == 'add': new_idx = r.add(sys.stdin.read()) print new_idx elif cmd == 'add-delta': new_idx = r.add(sys.stdin.read(), int(argv[2])) print new_idx elif cmd == 'dump': r.dump() elif cmd == 'get': try: idx = int(argv[2]) except IndexError: sys.stderr.write("usage: revfile get IDX\n") return 1 if idx < 0 or idx >= len(r): sys.stderr.write("invalid index %r\n" % idx) return 1 sys.stdout.write(r.get(idx)) elif cmd == 'find-sha': try: s = unhexlify(argv[2]) except IndexError: sys.stderr.write("usage: revfile find-sha HEX\n") return 1 idx = r.find_sha(s) if idx == _NO_RECORD: sys.stderr.write("no such record\n") return 1 else: print idx else: sys.stderr.write("unknown command %r\n" % cmd) return 1 if __name__ == '__main__': import sys sys.exit(main(sys.argv) or 0) commit refs/heads/master mark :219 committer 1113025062 +1000 data 4 todo from :218 M 644 inline bzrlib/revfile.py data 13228 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. """ # TODO: Something like pread() would make this slightly simpler and # perhaps more efficient. # TODO: Could also try to mmap things... Might be faster for the # index in particular? # TODO: Some kind of faster lookup of SHAs? The bad thing is that probably means # rewriting existing records, which is not so nice. import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify, unhexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_RECORD = 0xFFFFFFFFL # fields in the index record I_SHA = 0 I_BASE = 1 I_FLAGS = 2 I_OFFSET = 3 I_LEN = 4 FL_GZIP = 1 class RevfileError(Exception): pass class Revfile: def __init__(self, basename): # TODO: Option to open readonly # TODO: Lock file while open # TODO: advise of random access self.basename = basename idxname = basename + '.irev' dataname = basename + '.drev' idx_exists = os.path.exists(idxname) data_exists = os.path.exists(dataname) if idx_exists != data_exists: raise RevfileError("half-assed revfile") if not idx_exists: self.idxfile = open(idxname, 'w+b') self.datafile = open(dataname, 'w+b') print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: self.idxfile = open(idxname, 'r+b') self.datafile = open(dataname, 'r+b') h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def _check_index(self, idx): if idx < 0 or idx > len(self): raise RevfileError("invalid index %r" % idx) def find_sha(self, s): assert isinstance(s, str) assert len(s) == 20 for idx, idxrec in enumerate(self): if idxrec[I_SHA] == s: return idx else: return _NO_RECORD def _add_compressed(self, text_sha, data, base, compress): # well, maybe compress flags = 0 if compress: data_len = len(data) if data_len > 50: # don't do compression if it's too small; it's unlikely to win # enough to be worthwhile compr_data = zlib.compress(data) compr_len = len(compr_data) if compr_len < data_len: data = compr_data flags = FL_GZIP ##print '- compressed %d -> %d, %.1f%%' \ ## % (data_len, compr_len, float(compr_len)/float(data_len) * 100.0) return self._add_raw(text_sha, data, base, flags) def _add_raw(self, text_sha, data, base, flags): """Add pre-processed data, can be either full text or delta. This does the compression if that makes sense.""" idx = len(self) self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * (idx + 1) data_offset = self.datafile.tell() assert isinstance(data, str) # not unicode or anything wierd self.datafile.write(data) self.datafile.flush() assert isinstance(text_sha, str) entry = text_sha entry += struct.pack(">IIII12x", base, flags, data_offset, len(data)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def _add_full_text(self, text, text_sha): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" return self._add_compressed(text_sha, text, _NO_RECORD, compress) def _add_delta(self, text, text_sha, base, compress): """Add a text stored relative to a previous text.""" self._check_index(base) base_text = self.get(base) data = mdiff.bdiff(base_text, text) # If the delta is larger than the text, we might as well just # store the text. (OK, the delta might be more compressible, # but the overhead of applying it probably still makes it # bad, and I don't want to compress both of them to find out.) if len(data) >= len(text): return self._add_full_text(text, text_sha, compress) else: return self._add_compressed(text_sha, data, base, compress) def add(self, text, base=_NO_RECORD, compress=True): """Add a new text to the revfile. If the text is already present them its existing id is returned and the file is not changed. If compress is true then gzip compression will be used if it reduces the size. If a base index is specified, that text *may* be used for delta compression of the new text. Delta compression will only be used if it would be a size win and if the existing base is not at too long of a delta chain already. """ text_sha = sha.new(text).digest() idx = self.find_sha(text_sha) if idx != _NO_RECORD: # TODO: Optional paranoid mode where we read out that record and make sure # it's the same, in case someone ever breaks SHA-1. return idx # already present if base == _NO_RECORD: return self._add_full_text(text, text_sha, compress) else: return self._add_delta(text, text_sha, base, compress) def get(self, idx): idxrec = self[idx] base = idxrec[I_BASE] if base == _NO_RECORD: text = self._get_full_text(idx, idxrec) else: text = self._get_patched(idx, idxrec) if sha.new(text).digest() != idxrec[I_SHA]: raise RevfileError("corrupt SHA-1 digest on record %d" % idx) return text def _get_raw(self, idx, idxrec): flags = idxrec[I_FLAGS] if flags & ~FL_GZIP: raise RevfileError("unsupported index flags %#x on index %d" % (flags, idx)) l = idxrec[I_LEN] if l == 0: return '' self.datafile.seek(idxrec[I_OFFSET]) data = self.datafile.read(l) if len(data) != l: raise RevfileError("short read %d of %d " "getting text for record %d in %r" % (len(data), l, idx, self.basename)) if flags & FL_GZIP: data = zlib.decompress(data) return data def _get_full_text(self, idx, idxrec): assert idxrec[I_BASE] == _NO_RECORD text = self._get_raw(idx, idxrec) return text def _get_patched(self, idx, idxrec): base = idxrec[I_BASE] assert base >= 0 assert base < idx # no loops! base_text = self.get(base) patch = self._get_raw(idx, idxrec) text = mdiff.bpatch(base_text, patch) return text def __len__(self): """Return number of revisions.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) if l < _RECORDSIZE: raise RevfileError("no header present in index of %r" % (self.basename)) return int(l / _RECORDSIZE) - 1 def __getitem__(self, idx): """Index by sequence id returns the index field""" ## TODO: Can avoid seek if we just moved there... self._seek_index(idx) return self._read_next_index() def _seek_index(self, idx): if idx < 0: raise RevfileError("invalid index %r" % idx) self.idxfile.seek((idx + 1) * _RECORDSIZE) def _read_next_index(self): rec = self.idxfile.read(_RECORDSIZE) if not rec: raise IndexError("end of index file") elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_RECORD: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def main(argv): r = Revfile("testrev") try: cmd = argv[1] except IndexError: sys.stderr.write("usage: revfile dump\n" " revfile add\n" " revfile add-delta BASE\n" " revfile get IDX\n" " revfile find-sha HEX\n") return 1 # TODO: Something to show compression ratio, e.g. total size of # all files vs size of index and data files. if cmd == 'add': new_idx = r.add(sys.stdin.read()) print new_idx elif cmd == 'add-delta': new_idx = r.add(sys.stdin.read(), int(argv[2])) print new_idx elif cmd == 'dump': r.dump() elif cmd == 'get': try: idx = int(argv[2]) except IndexError: sys.stderr.write("usage: revfile get IDX\n") return 1 if idx < 0 or idx >= len(r): sys.stderr.write("invalid index %r\n" % idx) return 1 sys.stdout.write(r.get(idx)) elif cmd == 'find-sha': try: s = unhexlify(argv[2]) except IndexError: sys.stderr.write("usage: revfile find-sha HEX\n") return 1 idx = r.find_sha(s) if idx == _NO_RECORD: sys.stderr.write("no such record\n") return 1 else: print idx else: sys.stderr.write("unknown command %r\n" % cmd) return 1 if __name__ == '__main__': import sys sys.exit(main(sys.argv) or 0) commit refs/heads/master mark :220 committer 1113025619 +1000 data 35 limit the number of chained patches from :219 M 644 inline bzrlib/revfile.py data 14204 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. """ # TODO: Something like pread() would make this slightly simpler and # perhaps more efficient. # TODO: Could also try to mmap things... Might be faster for the # index in particular? # TODO: Some kind of faster lookup of SHAs? The bad thing is that probably means # rewriting existing records, which is not so nice. import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify, unhexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_RECORD = 0xFFFFFFFFL # fields in the index record I_SHA = 0 I_BASE = 1 I_FLAGS = 2 I_OFFSET = 3 I_LEN = 4 FL_GZIP = 1 # maximum number of patches in a row before recording a whole text. # intentionally pretty low for testing purposes. CHAIN_LIMIT = 2 class RevfileError(Exception): pass class LimitHitException(Exception): pass class Revfile: def __init__(self, basename): # TODO: Option to open readonly # TODO: Lock file while open # TODO: advise of random access self.basename = basename idxname = basename + '.irev' dataname = basename + '.drev' idx_exists = os.path.exists(idxname) data_exists = os.path.exists(dataname) if idx_exists != data_exists: raise RevfileError("half-assed revfile") if not idx_exists: self.idxfile = open(idxname, 'w+b') self.datafile = open(dataname, 'w+b') print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: self.idxfile = open(idxname, 'r+b') self.datafile = open(dataname, 'r+b') h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def _check_index(self, idx): if idx < 0 or idx > len(self): raise RevfileError("invalid index %r" % idx) def find_sha(self, s): assert isinstance(s, str) assert len(s) == 20 for idx, idxrec in enumerate(self): if idxrec[I_SHA] == s: return idx else: return _NO_RECORD def _add_compressed(self, text_sha, data, base, compress): # well, maybe compress flags = 0 if compress: data_len = len(data) if data_len > 50: # don't do compression if it's too small; it's unlikely to win # enough to be worthwhile compr_data = zlib.compress(data) compr_len = len(compr_data) if compr_len < data_len: data = compr_data flags = FL_GZIP ##print '- compressed %d -> %d, %.1f%%' \ ## % (data_len, compr_len, float(compr_len)/float(data_len) * 100.0) return self._add_raw(text_sha, data, base, flags) def _add_raw(self, text_sha, data, base, flags): """Add pre-processed data, can be either full text or delta. This does the compression if that makes sense.""" idx = len(self) self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * (idx + 1) data_offset = self.datafile.tell() assert isinstance(data, str) # not unicode or anything wierd self.datafile.write(data) self.datafile.flush() assert isinstance(text_sha, str) entry = text_sha entry += struct.pack(">IIII12x", base, flags, data_offset, len(data)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def _add_full_text(self, text, text_sha, compress): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" return self._add_compressed(text_sha, text, _NO_RECORD, compress) def _add_delta(self, text, text_sha, base, compress): """Add a text stored relative to a previous text.""" self._check_index(base) try: base_text = self.get(base, recursion_limit=CHAIN_LIMIT) except LimitHitException: return self._add_full_text(text, text_sha, compress) data = mdiff.bdiff(base_text, text) # If the delta is larger than the text, we might as well just # store the text. (OK, the delta might be more compressible, # but the overhead of applying it probably still makes it # bad, and I don't want to compress both of them to find out.) if len(data) >= len(text): return self._add_full_text(text, text_sha, compress) else: return self._add_compressed(text_sha, data, base, compress) def add(self, text, base=_NO_RECORD, compress=True): """Add a new text to the revfile. If the text is already present them its existing id is returned and the file is not changed. If compress is true then gzip compression will be used if it reduces the size. If a base index is specified, that text *may* be used for delta compression of the new text. Delta compression will only be used if it would be a size win and if the existing base is not at too long of a delta chain already. """ text_sha = sha.new(text).digest() idx = self.find_sha(text_sha) if idx != _NO_RECORD: # TODO: Optional paranoid mode where we read out that record and make sure # it's the same, in case someone ever breaks SHA-1. return idx # already present if base == _NO_RECORD: return self._add_full_text(text, text_sha, compress) else: return self._add_delta(text, text_sha, base, compress) def get(self, idx, recursion_limit=None): """Retrieve text of a previous revision. If recursion_limit is an integer then walk back at most that many revisions and then raise LimitHitException, indicating that we ought to record a new file text instead of another delta. Don't use this when trying to get out an existing revision.""" idxrec = self[idx] base = idxrec[I_BASE] if base == _NO_RECORD: text = self._get_full_text(idx, idxrec) else: text = self._get_patched(idx, idxrec, recursion_limit) if sha.new(text).digest() != idxrec[I_SHA]: raise RevfileError("corrupt SHA-1 digest on record %d" % idx) return text def _get_raw(self, idx, idxrec): flags = idxrec[I_FLAGS] if flags & ~FL_GZIP: raise RevfileError("unsupported index flags %#x on index %d" % (flags, idx)) l = idxrec[I_LEN] if l == 0: return '' self.datafile.seek(idxrec[I_OFFSET]) data = self.datafile.read(l) if len(data) != l: raise RevfileError("short read %d of %d " "getting text for record %d in %r" % (len(data), l, idx, self.basename)) if flags & FL_GZIP: data = zlib.decompress(data) return data def _get_full_text(self, idx, idxrec): assert idxrec[I_BASE] == _NO_RECORD text = self._get_raw(idx, idxrec) return text def _get_patched(self, idx, idxrec, recursion_limit): base = idxrec[I_BASE] assert base >= 0 assert base < idx # no loops! if recursion_limit == None: sub_limit = None else: sub_limit = recursion_limit - 1 if sub_limit < 0: raise LimitHitException() base_text = self.get(base, sub_limit) patch = self._get_raw(idx, idxrec) text = mdiff.bpatch(base_text, patch) return text def __len__(self): """Return number of revisions.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) if l < _RECORDSIZE: raise RevfileError("no header present in index of %r" % (self.basename)) return int(l / _RECORDSIZE) - 1 def __getitem__(self, idx): """Index by sequence id returns the index field""" ## TODO: Can avoid seek if we just moved there... self._seek_index(idx) return self._read_next_index() def _seek_index(self, idx): if idx < 0: raise RevfileError("invalid index %r" % idx) self.idxfile.seek((idx + 1) * _RECORDSIZE) def _read_next_index(self): rec = self.idxfile.read(_RECORDSIZE) if not rec: raise IndexError("end of index file") elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_RECORD: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def main(argv): r = Revfile("testrev") try: cmd = argv[1] except IndexError: sys.stderr.write("usage: revfile dump\n" " revfile add\n" " revfile add-delta BASE\n" " revfile get IDX\n" " revfile find-sha HEX\n") return 1 # TODO: Something to show compression ratio, e.g. total size of # all files vs size of index and data files. if cmd == 'add': new_idx = r.add(sys.stdin.read()) print new_idx elif cmd == 'add-delta': new_idx = r.add(sys.stdin.read(), int(argv[2])) print new_idx elif cmd == 'dump': r.dump() elif cmd == 'get': try: idx = int(argv[2]) except IndexError: sys.stderr.write("usage: revfile get IDX\n") return 1 if idx < 0 or idx >= len(r): sys.stderr.write("invalid index %r\n" % idx) return 1 sys.stdout.write(r.get(idx)) elif cmd == 'find-sha': try: s = unhexlify(argv[2]) except IndexError: sys.stderr.write("usage: revfile find-sha HEX\n") return 1 idx = r.find_sha(s) if idx == _NO_RECORD: sys.stderr.write("no such record\n") return 1 else: print idx else: sys.stderr.write("unknown command %r\n" % cmd) return 1 if __name__ == '__main__': import sys sys.exit(main(sys.argv) or 0) commit refs/heads/master mark :221 committer 1113025767 +1000 data 36 Revfile: new command total-text-size from :220 M 644 inline bzrlib/revfile.py data 14256 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. """ # TODO: Something like pread() would make this slightly simpler and # perhaps more efficient. # TODO: Could also try to mmap things... Might be faster for the # index in particular? # TODO: Some kind of faster lookup of SHAs? The bad thing is that probably means # rewriting existing records, which is not so nice. import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify, unhexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_RECORD = 0xFFFFFFFFL # fields in the index record I_SHA = 0 I_BASE = 1 I_FLAGS = 2 I_OFFSET = 3 I_LEN = 4 FL_GZIP = 1 # maximum number of patches in a row before recording a whole text. # intentionally pretty low for testing purposes. CHAIN_LIMIT = 2 class RevfileError(Exception): pass class LimitHitException(Exception): pass class Revfile: def __init__(self, basename): # TODO: Option to open readonly # TODO: Lock file while open # TODO: advise of random access self.basename = basename idxname = basename + '.irev' dataname = basename + '.drev' idx_exists = os.path.exists(idxname) data_exists = os.path.exists(dataname) if idx_exists != data_exists: raise RevfileError("half-assed revfile") if not idx_exists: self.idxfile = open(idxname, 'w+b') self.datafile = open(dataname, 'w+b') print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: self.idxfile = open(idxname, 'r+b') self.datafile = open(dataname, 'r+b') h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def _check_index(self, idx): if idx < 0 or idx > len(self): raise RevfileError("invalid index %r" % idx) def find_sha(self, s): assert isinstance(s, str) assert len(s) == 20 for idx, idxrec in enumerate(self): if idxrec[I_SHA] == s: return idx else: return _NO_RECORD def _add_compressed(self, text_sha, data, base, compress): # well, maybe compress flags = 0 if compress: data_len = len(data) if data_len > 50: # don't do compression if it's too small; it's unlikely to win # enough to be worthwhile compr_data = zlib.compress(data) compr_len = len(compr_data) if compr_len < data_len: data = compr_data flags = FL_GZIP ##print '- compressed %d -> %d, %.1f%%' \ ## % (data_len, compr_len, float(compr_len)/float(data_len) * 100.0) return self._add_raw(text_sha, data, base, flags) def _add_raw(self, text_sha, data, base, flags): """Add pre-processed data, can be either full text or delta. This does the compression if that makes sense.""" idx = len(self) self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * (idx + 1) data_offset = self.datafile.tell() assert isinstance(data, str) # not unicode or anything wierd self.datafile.write(data) self.datafile.flush() assert isinstance(text_sha, str) entry = text_sha entry += struct.pack(">IIII12x", base, flags, data_offset, len(data)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def _add_full_text(self, text, text_sha, compress): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" return self._add_compressed(text_sha, text, _NO_RECORD, compress) def _add_delta(self, text, text_sha, base, compress): """Add a text stored relative to a previous text.""" self._check_index(base) try: base_text = self.get(base, recursion_limit=CHAIN_LIMIT) except LimitHitException: return self._add_full_text(text, text_sha, compress) data = mdiff.bdiff(base_text, text) # If the delta is larger than the text, we might as well just # store the text. (OK, the delta might be more compressible, # but the overhead of applying it probably still makes it # bad, and I don't want to compress both of them to find out.) if len(data) >= len(text): return self._add_full_text(text, text_sha, compress) else: return self._add_compressed(text_sha, data, base, compress) def add(self, text, base=_NO_RECORD, compress=True): """Add a new text to the revfile. If the text is already present them its existing id is returned and the file is not changed. If compress is true then gzip compression will be used if it reduces the size. If a base index is specified, that text *may* be used for delta compression of the new text. Delta compression will only be used if it would be a size win and if the existing base is not at too long of a delta chain already. """ text_sha = sha.new(text).digest() idx = self.find_sha(text_sha) if idx != _NO_RECORD: # TODO: Optional paranoid mode where we read out that record and make sure # it's the same, in case someone ever breaks SHA-1. return idx # already present if base == _NO_RECORD: return self._add_full_text(text, text_sha, compress) else: return self._add_delta(text, text_sha, base, compress) def get(self, idx, recursion_limit=None): """Retrieve text of a previous revision. If recursion_limit is an integer then walk back at most that many revisions and then raise LimitHitException, indicating that we ought to record a new file text instead of another delta. Don't use this when trying to get out an existing revision.""" idxrec = self[idx] base = idxrec[I_BASE] if base == _NO_RECORD: text = self._get_full_text(idx, idxrec) else: text = self._get_patched(idx, idxrec, recursion_limit) if sha.new(text).digest() != idxrec[I_SHA]: raise RevfileError("corrupt SHA-1 digest on record %d" % idx) return text def _get_raw(self, idx, idxrec): flags = idxrec[I_FLAGS] if flags & ~FL_GZIP: raise RevfileError("unsupported index flags %#x on index %d" % (flags, idx)) l = idxrec[I_LEN] if l == 0: return '' self.datafile.seek(idxrec[I_OFFSET]) data = self.datafile.read(l) if len(data) != l: raise RevfileError("short read %d of %d " "getting text for record %d in %r" % (len(data), l, idx, self.basename)) if flags & FL_GZIP: data = zlib.decompress(data) return data def _get_full_text(self, idx, idxrec): assert idxrec[I_BASE] == _NO_RECORD text = self._get_raw(idx, idxrec) return text def _get_patched(self, idx, idxrec, recursion_limit): base = idxrec[I_BASE] assert base >= 0 assert base < idx # no loops! if recursion_limit == None: sub_limit = None else: sub_limit = recursion_limit - 1 if sub_limit < 0: raise LimitHitException() base_text = self.get(base, sub_limit) patch = self._get_raw(idx, idxrec) text = mdiff.bpatch(base_text, patch) return text def __len__(self): """Return number of revisions.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) if l < _RECORDSIZE: raise RevfileError("no header present in index of %r" % (self.basename)) return int(l / _RECORDSIZE) - 1 def __getitem__(self, idx): """Index by sequence id returns the index field""" ## TODO: Can avoid seek if we just moved there... self._seek_index(idx) return self._read_next_index() def _seek_index(self, idx): if idx < 0: raise RevfileError("invalid index %r" % idx) self.idxfile.seek((idx + 1) * _RECORDSIZE) def _read_next_index(self): rec = self.idxfile.read(_RECORDSIZE) if not rec: raise IndexError("end of index file") elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_RECORD: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def main(argv): r = Revfile("testrev") try: cmd = argv[1] except IndexError: sys.stderr.write("usage: revfile dump\n" " revfile add\n" " revfile add-delta BASE\n" " revfile get IDX\n" " revfile find-sha HEX\n" " revfile total-text-size\n") return 1 if cmd == 'add': new_idx = r.add(sys.stdin.read()) print new_idx elif cmd == 'add-delta': new_idx = r.add(sys.stdin.read(), int(argv[2])) print new_idx elif cmd == 'dump': r.dump() elif cmd == 'get': try: idx = int(argv[2]) except IndexError: sys.stderr.write("usage: revfile get IDX\n") return 1 if idx < 0 or idx >= len(r): sys.stderr.write("invalid index %r\n" % idx) return 1 sys.stdout.write(r.get(idx)) elif cmd == 'find-sha': try: s = unhexlify(argv[2]) except IndexError: sys.stderr.write("usage: revfile find-sha HEX\n") return 1 idx = r.find_sha(s) if idx == _NO_RECORD: sys.stderr.write("no such record\n") return 1 else: print idx elif cmd == 'total-text-size': t = 0L for idx in range(len(r)): t += len(r.get(idx)) print t else: sys.stderr.write("unknown command %r\n" % cmd) return 1 if __name__ == '__main__': import sys sys.exit(main(sys.argv) or 0) commit refs/heads/master mark :222 committer 1113025822 +1000 data 24 refactor total_text_size from :221 M 644 inline bzrlib/revfile.py data 14329 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. """ # TODO: Something like pread() would make this slightly simpler and # perhaps more efficient. # TODO: Could also try to mmap things... Might be faster for the # index in particular? # TODO: Some kind of faster lookup of SHAs? The bad thing is that probably means # rewriting existing records, which is not so nice. import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify, unhexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_RECORD = 0xFFFFFFFFL # fields in the index record I_SHA = 0 I_BASE = 1 I_FLAGS = 2 I_OFFSET = 3 I_LEN = 4 FL_GZIP = 1 # maximum number of patches in a row before recording a whole text. # intentionally pretty low for testing purposes. CHAIN_LIMIT = 2 class RevfileError(Exception): pass class LimitHitException(Exception): pass class Revfile: def __init__(self, basename): # TODO: Option to open readonly # TODO: Lock file while open # TODO: advise of random access self.basename = basename idxname = basename + '.irev' dataname = basename + '.drev' idx_exists = os.path.exists(idxname) data_exists = os.path.exists(dataname) if idx_exists != data_exists: raise RevfileError("half-assed revfile") if not idx_exists: self.idxfile = open(idxname, 'w+b') self.datafile = open(dataname, 'w+b') print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: self.idxfile = open(idxname, 'r+b') self.datafile = open(dataname, 'r+b') h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def _check_index(self, idx): if idx < 0 or idx > len(self): raise RevfileError("invalid index %r" % idx) def find_sha(self, s): assert isinstance(s, str) assert len(s) == 20 for idx, idxrec in enumerate(self): if idxrec[I_SHA] == s: return idx else: return _NO_RECORD def _add_compressed(self, text_sha, data, base, compress): # well, maybe compress flags = 0 if compress: data_len = len(data) if data_len > 50: # don't do compression if it's too small; it's unlikely to win # enough to be worthwhile compr_data = zlib.compress(data) compr_len = len(compr_data) if compr_len < data_len: data = compr_data flags = FL_GZIP ##print '- compressed %d -> %d, %.1f%%' \ ## % (data_len, compr_len, float(compr_len)/float(data_len) * 100.0) return self._add_raw(text_sha, data, base, flags) def _add_raw(self, text_sha, data, base, flags): """Add pre-processed data, can be either full text or delta. This does the compression if that makes sense.""" idx = len(self) self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * (idx + 1) data_offset = self.datafile.tell() assert isinstance(data, str) # not unicode or anything wierd self.datafile.write(data) self.datafile.flush() assert isinstance(text_sha, str) entry = text_sha entry += struct.pack(">IIII12x", base, flags, data_offset, len(data)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def _add_full_text(self, text, text_sha, compress): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" return self._add_compressed(text_sha, text, _NO_RECORD, compress) def _add_delta(self, text, text_sha, base, compress): """Add a text stored relative to a previous text.""" self._check_index(base) try: base_text = self.get(base, recursion_limit=CHAIN_LIMIT) except LimitHitException: return self._add_full_text(text, text_sha, compress) data = mdiff.bdiff(base_text, text) # If the delta is larger than the text, we might as well just # store the text. (OK, the delta might be more compressible, # but the overhead of applying it probably still makes it # bad, and I don't want to compress both of them to find out.) if len(data) >= len(text): return self._add_full_text(text, text_sha, compress) else: return self._add_compressed(text_sha, data, base, compress) def add(self, text, base=_NO_RECORD, compress=True): """Add a new text to the revfile. If the text is already present them its existing id is returned and the file is not changed. If compress is true then gzip compression will be used if it reduces the size. If a base index is specified, that text *may* be used for delta compression of the new text. Delta compression will only be used if it would be a size win and if the existing base is not at too long of a delta chain already. """ text_sha = sha.new(text).digest() idx = self.find_sha(text_sha) if idx != _NO_RECORD: # TODO: Optional paranoid mode where we read out that record and make sure # it's the same, in case someone ever breaks SHA-1. return idx # already present if base == _NO_RECORD: return self._add_full_text(text, text_sha, compress) else: return self._add_delta(text, text_sha, base, compress) def get(self, idx, recursion_limit=None): """Retrieve text of a previous revision. If recursion_limit is an integer then walk back at most that many revisions and then raise LimitHitException, indicating that we ought to record a new file text instead of another delta. Don't use this when trying to get out an existing revision.""" idxrec = self[idx] base = idxrec[I_BASE] if base == _NO_RECORD: text = self._get_full_text(idx, idxrec) else: text = self._get_patched(idx, idxrec, recursion_limit) if sha.new(text).digest() != idxrec[I_SHA]: raise RevfileError("corrupt SHA-1 digest on record %d" % idx) return text def _get_raw(self, idx, idxrec): flags = idxrec[I_FLAGS] if flags & ~FL_GZIP: raise RevfileError("unsupported index flags %#x on index %d" % (flags, idx)) l = idxrec[I_LEN] if l == 0: return '' self.datafile.seek(idxrec[I_OFFSET]) data = self.datafile.read(l) if len(data) != l: raise RevfileError("short read %d of %d " "getting text for record %d in %r" % (len(data), l, idx, self.basename)) if flags & FL_GZIP: data = zlib.decompress(data) return data def _get_full_text(self, idx, idxrec): assert idxrec[I_BASE] == _NO_RECORD text = self._get_raw(idx, idxrec) return text def _get_patched(self, idx, idxrec, recursion_limit): base = idxrec[I_BASE] assert base >= 0 assert base < idx # no loops! if recursion_limit == None: sub_limit = None else: sub_limit = recursion_limit - 1 if sub_limit < 0: raise LimitHitException() base_text = self.get(base, sub_limit) patch = self._get_raw(idx, idxrec) text = mdiff.bpatch(base_text, patch) return text def __len__(self): """Return number of revisions.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) if l < _RECORDSIZE: raise RevfileError("no header present in index of %r" % (self.basename)) return int(l / _RECORDSIZE) - 1 def __getitem__(self, idx): """Index by sequence id returns the index field""" ## TODO: Can avoid seek if we just moved there... self._seek_index(idx) return self._read_next_index() def _seek_index(self, idx): if idx < 0: raise RevfileError("invalid index %r" % idx) self.idxfile.seek((idx + 1) * _RECORDSIZE) def _read_next_index(self): rec = self.idxfile.read(_RECORDSIZE) if not rec: raise IndexError("end of index file") elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_RECORD: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def total_text_size(self): t = 0L for idx in range(len(self)): t += len(self.get(idx)) return t def main(argv): r = Revfile("testrev") try: cmd = argv[1] except IndexError: sys.stderr.write("usage: revfile dump\n" " revfile add\n" " revfile add-delta BASE\n" " revfile get IDX\n" " revfile find-sha HEX\n" " revfile total-text-size\n") return 1 if cmd == 'add': new_idx = r.add(sys.stdin.read()) print new_idx elif cmd == 'add-delta': new_idx = r.add(sys.stdin.read(), int(argv[2])) print new_idx elif cmd == 'dump': r.dump() elif cmd == 'get': try: idx = int(argv[2]) except IndexError: sys.stderr.write("usage: revfile get IDX\n") return 1 if idx < 0 or idx >= len(r): sys.stderr.write("invalid index %r\n" % idx) return 1 sys.stdout.write(r.get(idx)) elif cmd == 'find-sha': try: s = unhexlify(argv[2]) except IndexError: sys.stderr.write("usage: revfile find-sha HEX\n") return 1 idx = r.find_sha(s) if idx == _NO_RECORD: sys.stderr.write("no such record\n") return 1 else: print idx elif cmd == 'total-text-size': print r.total_text_size() else: sys.stderr.write("unknown command %r\n" % cmd) return 1 if __name__ == '__main__': import sys sys.exit(main(sys.argv) or 0) commit refs/heads/master mark :223 committer 1113025882 +1000 data 3 doc from :222 M 644 inline bzrlib/revfile.py data 14640 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. """ # TODO: Something like pread() would make this slightly simpler and # perhaps more efficient. # TODO: Could also try to mmap things... Might be faster for the # index in particular? # TODO: Some kind of faster lookup of SHAs? The bad thing is that probably means # rewriting existing records, which is not so nice. import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify, unhexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_RECORD = 0xFFFFFFFFL # fields in the index record I_SHA = 0 I_BASE = 1 I_FLAGS = 2 I_OFFSET = 3 I_LEN = 4 FL_GZIP = 1 # maximum number of patches in a row before recording a whole text. # intentionally pretty low for testing purposes. CHAIN_LIMIT = 2 class RevfileError(Exception): pass class LimitHitException(Exception): pass class Revfile: def __init__(self, basename): # TODO: Option to open readonly # TODO: Lock file while open # TODO: advise of random access self.basename = basename idxname = basename + '.irev' dataname = basename + '.drev' idx_exists = os.path.exists(idxname) data_exists = os.path.exists(dataname) if idx_exists != data_exists: raise RevfileError("half-assed revfile") if not idx_exists: self.idxfile = open(idxname, 'w+b') self.datafile = open(dataname, 'w+b') print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: self.idxfile = open(idxname, 'r+b') self.datafile = open(dataname, 'r+b') h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def _check_index(self, idx): if idx < 0 or idx > len(self): raise RevfileError("invalid index %r" % idx) def find_sha(self, s): assert isinstance(s, str) assert len(s) == 20 for idx, idxrec in enumerate(self): if idxrec[I_SHA] == s: return idx else: return _NO_RECORD def _add_compressed(self, text_sha, data, base, compress): # well, maybe compress flags = 0 if compress: data_len = len(data) if data_len > 50: # don't do compression if it's too small; it's unlikely to win # enough to be worthwhile compr_data = zlib.compress(data) compr_len = len(compr_data) if compr_len < data_len: data = compr_data flags = FL_GZIP ##print '- compressed %d -> %d, %.1f%%' \ ## % (data_len, compr_len, float(compr_len)/float(data_len) * 100.0) return self._add_raw(text_sha, data, base, flags) def _add_raw(self, text_sha, data, base, flags): """Add pre-processed data, can be either full text or delta. This does the compression if that makes sense.""" idx = len(self) self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * (idx + 1) data_offset = self.datafile.tell() assert isinstance(data, str) # not unicode or anything wierd self.datafile.write(data) self.datafile.flush() assert isinstance(text_sha, str) entry = text_sha entry += struct.pack(">IIII12x", base, flags, data_offset, len(data)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def _add_full_text(self, text, text_sha, compress): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" return self._add_compressed(text_sha, text, _NO_RECORD, compress) def _add_delta(self, text, text_sha, base, compress): """Add a text stored relative to a previous text.""" self._check_index(base) try: base_text = self.get(base, recursion_limit=CHAIN_LIMIT) except LimitHitException: return self._add_full_text(text, text_sha, compress) data = mdiff.bdiff(base_text, text) # If the delta is larger than the text, we might as well just # store the text. (OK, the delta might be more compressible, # but the overhead of applying it probably still makes it # bad, and I don't want to compress both of them to find out.) if len(data) >= len(text): return self._add_full_text(text, text_sha, compress) else: return self._add_compressed(text_sha, data, base, compress) def add(self, text, base=_NO_RECORD, compress=True): """Add a new text to the revfile. If the text is already present them its existing id is returned and the file is not changed. If compress is true then gzip compression will be used if it reduces the size. If a base index is specified, that text *may* be used for delta compression of the new text. Delta compression will only be used if it would be a size win and if the existing base is not at too long of a delta chain already. """ text_sha = sha.new(text).digest() idx = self.find_sha(text_sha) if idx != _NO_RECORD: # TODO: Optional paranoid mode where we read out that record and make sure # it's the same, in case someone ever breaks SHA-1. return idx # already present if base == _NO_RECORD: return self._add_full_text(text, text_sha, compress) else: return self._add_delta(text, text_sha, base, compress) def get(self, idx, recursion_limit=None): """Retrieve text of a previous revision. If recursion_limit is an integer then walk back at most that many revisions and then raise LimitHitException, indicating that we ought to record a new file text instead of another delta. Don't use this when trying to get out an existing revision.""" idxrec = self[idx] base = idxrec[I_BASE] if base == _NO_RECORD: text = self._get_full_text(idx, idxrec) else: text = self._get_patched(idx, idxrec, recursion_limit) if sha.new(text).digest() != idxrec[I_SHA]: raise RevfileError("corrupt SHA-1 digest on record %d" % idx) return text def _get_raw(self, idx, idxrec): flags = idxrec[I_FLAGS] if flags & ~FL_GZIP: raise RevfileError("unsupported index flags %#x on index %d" % (flags, idx)) l = idxrec[I_LEN] if l == 0: return '' self.datafile.seek(idxrec[I_OFFSET]) data = self.datafile.read(l) if len(data) != l: raise RevfileError("short read %d of %d " "getting text for record %d in %r" % (len(data), l, idx, self.basename)) if flags & FL_GZIP: data = zlib.decompress(data) return data def _get_full_text(self, idx, idxrec): assert idxrec[I_BASE] == _NO_RECORD text = self._get_raw(idx, idxrec) return text def _get_patched(self, idx, idxrec, recursion_limit): base = idxrec[I_BASE] assert base >= 0 assert base < idx # no loops! if recursion_limit == None: sub_limit = None else: sub_limit = recursion_limit - 1 if sub_limit < 0: raise LimitHitException() base_text = self.get(base, sub_limit) patch = self._get_raw(idx, idxrec) text = mdiff.bpatch(base_text, patch) return text def __len__(self): """Return number of revisions.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) if l < _RECORDSIZE: raise RevfileError("no header present in index of %r" % (self.basename)) return int(l / _RECORDSIZE) - 1 def __getitem__(self, idx): """Index by sequence id returns the index field""" ## TODO: Can avoid seek if we just moved there... self._seek_index(idx) return self._read_next_index() def _seek_index(self, idx): if idx < 0: raise RevfileError("invalid index %r" % idx) self.idxfile.seek((idx + 1) * _RECORDSIZE) def _read_next_index(self): rec = self.idxfile.read(_RECORDSIZE) if not rec: raise IndexError("end of index file") elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_RECORD: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def total_text_size(self): """Return the sum of sizes of all file texts. This is how much space they would occupy if they were stored without delta and gzip compression. As a side effect this completely validates the Revfile, checking that all texts can be reproduced with the correct SHA-1.""" t = 0L for idx in range(len(self)): t += len(self.get(idx)) return t def main(argv): r = Revfile("testrev") try: cmd = argv[1] except IndexError: sys.stderr.write("usage: revfile dump\n" " revfile add\n" " revfile add-delta BASE\n" " revfile get IDX\n" " revfile find-sha HEX\n" " revfile total-text-size\n") return 1 if cmd == 'add': new_idx = r.add(sys.stdin.read()) print new_idx elif cmd == 'add-delta': new_idx = r.add(sys.stdin.read(), int(argv[2])) print new_idx elif cmd == 'dump': r.dump() elif cmd == 'get': try: idx = int(argv[2]) except IndexError: sys.stderr.write("usage: revfile get IDX\n") return 1 if idx < 0 or idx >= len(r): sys.stderr.write("invalid index %r\n" % idx) return 1 sys.stdout.write(r.get(idx)) elif cmd == 'find-sha': try: s = unhexlify(argv[2]) except IndexError: sys.stderr.write("usage: revfile find-sha HEX\n") return 1 idx = r.find_sha(s) if idx == _NO_RECORD: sys.stderr.write("no such record\n") return 1 else: print idx elif cmd == 'total-text-size': print r.total_text_size() else: sys.stderr.write("unknown command %r\n" % cmd) return 1 if __name__ == '__main__': import sys sys.exit(main(sys.argv) or 0) commit refs/heads/master mark :224 committer 1113026435 +1000 data 3 doc from :223 M 644 inline bzrlib/revfile.py data 15217 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. This performs pretty well except when trying to calculate deltas of really large files. For that the main thing would be to plug in something faster than difflib, which is after all pure Python. Another approach is to just store the gzipped full text of big files, though perhaps that's too perverse? """ # TODO: Something like pread() would make this slightly simpler and # perhaps more efficient. # TODO: Could also try to mmap things... Might be faster for the # index in particular? # TODO: Some kind of faster lookup of SHAs? The bad thing is that probably means # rewriting existing records, which is not so nice. # TODO: Something to check that regions identified in the index file # completely butt up and do not overlap. Strictly it's not a problem # if there are gaps and that can happen if we're interrupted while # writing to the datafile. Overlapping would be very bad though. import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify, unhexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_RECORD = 0xFFFFFFFFL # fields in the index record I_SHA = 0 I_BASE = 1 I_FLAGS = 2 I_OFFSET = 3 I_LEN = 4 FL_GZIP = 1 # maximum number of patches in a row before recording a whole text. # intentionally pretty low for testing purposes. CHAIN_LIMIT = 2 class RevfileError(Exception): pass class LimitHitException(Exception): pass class Revfile: def __init__(self, basename): # TODO: Option to open readonly # TODO: Lock file while open # TODO: advise of random access self.basename = basename idxname = basename + '.irev' dataname = basename + '.drev' idx_exists = os.path.exists(idxname) data_exists = os.path.exists(dataname) if idx_exists != data_exists: raise RevfileError("half-assed revfile") if not idx_exists: self.idxfile = open(idxname, 'w+b') self.datafile = open(dataname, 'w+b') print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: self.idxfile = open(idxname, 'r+b') self.datafile = open(dataname, 'r+b') h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def _check_index(self, idx): if idx < 0 or idx > len(self): raise RevfileError("invalid index %r" % idx) def find_sha(self, s): assert isinstance(s, str) assert len(s) == 20 for idx, idxrec in enumerate(self): if idxrec[I_SHA] == s: return idx else: return _NO_RECORD def _add_compressed(self, text_sha, data, base, compress): # well, maybe compress flags = 0 if compress: data_len = len(data) if data_len > 50: # don't do compression if it's too small; it's unlikely to win # enough to be worthwhile compr_data = zlib.compress(data) compr_len = len(compr_data) if compr_len < data_len: data = compr_data flags = FL_GZIP ##print '- compressed %d -> %d, %.1f%%' \ ## % (data_len, compr_len, float(compr_len)/float(data_len) * 100.0) return self._add_raw(text_sha, data, base, flags) def _add_raw(self, text_sha, data, base, flags): """Add pre-processed data, can be either full text or delta. This does the compression if that makes sense.""" idx = len(self) self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * (idx + 1) data_offset = self.datafile.tell() assert isinstance(data, str) # not unicode or anything wierd self.datafile.write(data) self.datafile.flush() assert isinstance(text_sha, str) entry = text_sha entry += struct.pack(">IIII12x", base, flags, data_offset, len(data)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def _add_full_text(self, text, text_sha, compress): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" return self._add_compressed(text_sha, text, _NO_RECORD, compress) def _add_delta(self, text, text_sha, base, compress): """Add a text stored relative to a previous text.""" self._check_index(base) try: base_text = self.get(base, recursion_limit=CHAIN_LIMIT) except LimitHitException: return self._add_full_text(text, text_sha, compress) data = mdiff.bdiff(base_text, text) # If the delta is larger than the text, we might as well just # store the text. (OK, the delta might be more compressible, # but the overhead of applying it probably still makes it # bad, and I don't want to compress both of them to find out.) if len(data) >= len(text): return self._add_full_text(text, text_sha, compress) else: return self._add_compressed(text_sha, data, base, compress) def add(self, text, base=_NO_RECORD, compress=True): """Add a new text to the revfile. If the text is already present them its existing id is returned and the file is not changed. If compress is true then gzip compression will be used if it reduces the size. If a base index is specified, that text *may* be used for delta compression of the new text. Delta compression will only be used if it would be a size win and if the existing base is not at too long of a delta chain already. """ text_sha = sha.new(text).digest() idx = self.find_sha(text_sha) if idx != _NO_RECORD: # TODO: Optional paranoid mode where we read out that record and make sure # it's the same, in case someone ever breaks SHA-1. return idx # already present if base == _NO_RECORD: return self._add_full_text(text, text_sha, compress) else: return self._add_delta(text, text_sha, base, compress) def get(self, idx, recursion_limit=None): """Retrieve text of a previous revision. If recursion_limit is an integer then walk back at most that many revisions and then raise LimitHitException, indicating that we ought to record a new file text instead of another delta. Don't use this when trying to get out an existing revision.""" idxrec = self[idx] base = idxrec[I_BASE] if base == _NO_RECORD: text = self._get_full_text(idx, idxrec) else: text = self._get_patched(idx, idxrec, recursion_limit) if sha.new(text).digest() != idxrec[I_SHA]: raise RevfileError("corrupt SHA-1 digest on record %d" % idx) return text def _get_raw(self, idx, idxrec): flags = idxrec[I_FLAGS] if flags & ~FL_GZIP: raise RevfileError("unsupported index flags %#x on index %d" % (flags, idx)) l = idxrec[I_LEN] if l == 0: return '' self.datafile.seek(idxrec[I_OFFSET]) data = self.datafile.read(l) if len(data) != l: raise RevfileError("short read %d of %d " "getting text for record %d in %r" % (len(data), l, idx, self.basename)) if flags & FL_GZIP: data = zlib.decompress(data) return data def _get_full_text(self, idx, idxrec): assert idxrec[I_BASE] == _NO_RECORD text = self._get_raw(idx, idxrec) return text def _get_patched(self, idx, idxrec, recursion_limit): base = idxrec[I_BASE] assert base >= 0 assert base < idx # no loops! if recursion_limit == None: sub_limit = None else: sub_limit = recursion_limit - 1 if sub_limit < 0: raise LimitHitException() base_text = self.get(base, sub_limit) patch = self._get_raw(idx, idxrec) text = mdiff.bpatch(base_text, patch) return text def __len__(self): """Return number of revisions.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) if l < _RECORDSIZE: raise RevfileError("no header present in index of %r" % (self.basename)) return int(l / _RECORDSIZE) - 1 def __getitem__(self, idx): """Index by sequence id returns the index field""" ## TODO: Can avoid seek if we just moved there... self._seek_index(idx) return self._read_next_index() def _seek_index(self, idx): if idx < 0: raise RevfileError("invalid index %r" % idx) self.idxfile.seek((idx + 1) * _RECORDSIZE) def _read_next_index(self): rec = self.idxfile.read(_RECORDSIZE) if not rec: raise IndexError("end of index file") elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_RECORD: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def total_text_size(self): """Return the sum of sizes of all file texts. This is how much space they would occupy if they were stored without delta and gzip compression. As a side effect this completely validates the Revfile, checking that all texts can be reproduced with the correct SHA-1.""" t = 0L for idx in range(len(self)): t += len(self.get(idx)) return t def main(argv): r = Revfile("testrev") try: cmd = argv[1] except IndexError: sys.stderr.write("usage: revfile dump\n" " revfile add\n" " revfile add-delta BASE\n" " revfile get IDX\n" " revfile find-sha HEX\n" " revfile total-text-size\n") return 1 if cmd == 'add': new_idx = r.add(sys.stdin.read()) print new_idx elif cmd == 'add-delta': new_idx = r.add(sys.stdin.read(), int(argv[2])) print new_idx elif cmd == 'dump': r.dump() elif cmd == 'get': try: idx = int(argv[2]) except IndexError: sys.stderr.write("usage: revfile get IDX\n") return 1 if idx < 0 or idx >= len(r): sys.stderr.write("invalid index %r\n" % idx) return 1 sys.stdout.write(r.get(idx)) elif cmd == 'find-sha': try: s = unhexlify(argv[2]) except IndexError: sys.stderr.write("usage: revfile find-sha HEX\n") return 1 idx = r.find_sha(s) if idx == _NO_RECORD: sys.stderr.write("no such record\n") return 1 else: print idx elif cmd == 'total-text-size': print r.total_text_size() else: sys.stderr.write("unknown command %r\n" % cmd) return 1 if __name__ == '__main__': import sys sys.exit(main(sys.argv) or 0) commit refs/heads/master mark :225 committer 1113027704 +1000 data 12 debug output from :224 M 644 inline bzrlib/mdiff.py data 2320 # (C) 2005 Matt Mackall # (C) 2005 Canonical Ltd # based on code by Matt Mackall, hacked by Martin Pool # mm's code works line-by-line; this just works on byte strings. # Possibly slower; possibly gives better results for code not # regularly separated by newlines and anyhow a bit simpler. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # TODO: maybe work on files not strings? import difflib, sys, struct from cStringIO import StringIO def diff(a, b): d = difflib.SequenceMatcher(None, a, b) ## sys.stderr.write(' ~ real_quick_ratio: %.4f\n' % d.real_quick_ratio()) for o, m, n, s, t in d.get_opcodes(): if o == 'equal': continue # a[m:n] should be replaced by b[s:t] if s == t: yield m, n, '' else: yield m, n, b[s:t] def tobinary(ops): b = StringIO() for f in ops: b.write(struct.pack(">III", f[0], f[1], len(f[2]))) b.write(f[2]) return b.getvalue() def bdiff(a, b): return tobinary(diff(a, b)) def patch(t, ops): last = 0 b = StringIO() for m, n, r in ops: b.write(t[last:m]) if r: b.write(r) last = n b.write(t[last:]) return b.getvalue() def frombinary(b): bin = StringIO(b) while True: p = bin.read(12) if not p: break m, n, l = struct.unpack(">III", p) if l == 0: r = '' else: r = bin.read(l) if len(r) != l: raise Exception("truncated patch data") yield m, n, r def bpatch(t, b): return patch(t, frombinary(b)) commit refs/heads/master mark :226 committer 1113028184 +1000 data 24 revf: new command 'last' from :225 M 644 inline bzrlib/revfile.py data 15313 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. This performs pretty well except when trying to calculate deltas of really large files. For that the main thing would be to plug in something faster than difflib, which is after all pure Python. Another approach is to just store the gzipped full text of big files, though perhaps that's too perverse? """ # TODO: Something like pread() would make this slightly simpler and # perhaps more efficient. # TODO: Could also try to mmap things... Might be faster for the # index in particular? # TODO: Some kind of faster lookup of SHAs? The bad thing is that probably means # rewriting existing records, which is not so nice. # TODO: Something to check that regions identified in the index file # completely butt up and do not overlap. Strictly it's not a problem # if there are gaps and that can happen if we're interrupted while # writing to the datafile. Overlapping would be very bad though. import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify, unhexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_RECORD = 0xFFFFFFFFL # fields in the index record I_SHA = 0 I_BASE = 1 I_FLAGS = 2 I_OFFSET = 3 I_LEN = 4 FL_GZIP = 1 # maximum number of patches in a row before recording a whole text. # intentionally pretty low for testing purposes. CHAIN_LIMIT = 2 class RevfileError(Exception): pass class LimitHitException(Exception): pass class Revfile: def __init__(self, basename): # TODO: Option to open readonly # TODO: Lock file while open # TODO: advise of random access self.basename = basename idxname = basename + '.irev' dataname = basename + '.drev' idx_exists = os.path.exists(idxname) data_exists = os.path.exists(dataname) if idx_exists != data_exists: raise RevfileError("half-assed revfile") if not idx_exists: self.idxfile = open(idxname, 'w+b') self.datafile = open(dataname, 'w+b') print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: self.idxfile = open(idxname, 'r+b') self.datafile = open(dataname, 'r+b') h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def _check_index(self, idx): if idx < 0 or idx > len(self): raise RevfileError("invalid index %r" % idx) def find_sha(self, s): assert isinstance(s, str) assert len(s) == 20 for idx, idxrec in enumerate(self): if idxrec[I_SHA] == s: return idx else: return _NO_RECORD def _add_compressed(self, text_sha, data, base, compress): # well, maybe compress flags = 0 if compress: data_len = len(data) if data_len > 50: # don't do compression if it's too small; it's unlikely to win # enough to be worthwhile compr_data = zlib.compress(data) compr_len = len(compr_data) if compr_len < data_len: data = compr_data flags = FL_GZIP ##print '- compressed %d -> %d, %.1f%%' \ ## % (data_len, compr_len, float(compr_len)/float(data_len) * 100.0) return self._add_raw(text_sha, data, base, flags) def _add_raw(self, text_sha, data, base, flags): """Add pre-processed data, can be either full text or delta. This does the compression if that makes sense.""" idx = len(self) self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * (idx + 1) data_offset = self.datafile.tell() assert isinstance(data, str) # not unicode or anything wierd self.datafile.write(data) self.datafile.flush() assert isinstance(text_sha, str) entry = text_sha entry += struct.pack(">IIII12x", base, flags, data_offset, len(data)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def _add_full_text(self, text, text_sha, compress): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" return self._add_compressed(text_sha, text, _NO_RECORD, compress) def _add_delta(self, text, text_sha, base, compress): """Add a text stored relative to a previous text.""" self._check_index(base) try: base_text = self.get(base, recursion_limit=CHAIN_LIMIT) except LimitHitException: return self._add_full_text(text, text_sha, compress) data = mdiff.bdiff(base_text, text) # If the delta is larger than the text, we might as well just # store the text. (OK, the delta might be more compressible, # but the overhead of applying it probably still makes it # bad, and I don't want to compress both of them to find out.) if len(data) >= len(text): return self._add_full_text(text, text_sha, compress) else: return self._add_compressed(text_sha, data, base, compress) def add(self, text, base=_NO_RECORD, compress=True): """Add a new text to the revfile. If the text is already present them its existing id is returned and the file is not changed. If compress is true then gzip compression will be used if it reduces the size. If a base index is specified, that text *may* be used for delta compression of the new text. Delta compression will only be used if it would be a size win and if the existing base is not at too long of a delta chain already. """ text_sha = sha.new(text).digest() idx = self.find_sha(text_sha) if idx != _NO_RECORD: # TODO: Optional paranoid mode where we read out that record and make sure # it's the same, in case someone ever breaks SHA-1. return idx # already present if base == _NO_RECORD: return self._add_full_text(text, text_sha, compress) else: return self._add_delta(text, text_sha, base, compress) def get(self, idx, recursion_limit=None): """Retrieve text of a previous revision. If recursion_limit is an integer then walk back at most that many revisions and then raise LimitHitException, indicating that we ought to record a new file text instead of another delta. Don't use this when trying to get out an existing revision.""" idxrec = self[idx] base = idxrec[I_BASE] if base == _NO_RECORD: text = self._get_full_text(idx, idxrec) else: text = self._get_patched(idx, idxrec, recursion_limit) if sha.new(text).digest() != idxrec[I_SHA]: raise RevfileError("corrupt SHA-1 digest on record %d" % idx) return text def _get_raw(self, idx, idxrec): flags = idxrec[I_FLAGS] if flags & ~FL_GZIP: raise RevfileError("unsupported index flags %#x on index %d" % (flags, idx)) l = idxrec[I_LEN] if l == 0: return '' self.datafile.seek(idxrec[I_OFFSET]) data = self.datafile.read(l) if len(data) != l: raise RevfileError("short read %d of %d " "getting text for record %d in %r" % (len(data), l, idx, self.basename)) if flags & FL_GZIP: data = zlib.decompress(data) return data def _get_full_text(self, idx, idxrec): assert idxrec[I_BASE] == _NO_RECORD text = self._get_raw(idx, idxrec) return text def _get_patched(self, idx, idxrec, recursion_limit): base = idxrec[I_BASE] assert base >= 0 assert base < idx # no loops! if recursion_limit == None: sub_limit = None else: sub_limit = recursion_limit - 1 if sub_limit < 0: raise LimitHitException() base_text = self.get(base, sub_limit) patch = self._get_raw(idx, idxrec) text = mdiff.bpatch(base_text, patch) return text def __len__(self): """Return number of revisions.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) if l < _RECORDSIZE: raise RevfileError("no header present in index of %r" % (self.basename)) return int(l / _RECORDSIZE) - 1 def __getitem__(self, idx): """Index by sequence id returns the index field""" ## TODO: Can avoid seek if we just moved there... self._seek_index(idx) return self._read_next_index() def _seek_index(self, idx): if idx < 0: raise RevfileError("invalid index %r" % idx) self.idxfile.seek((idx + 1) * _RECORDSIZE) def _read_next_index(self): rec = self.idxfile.read(_RECORDSIZE) if not rec: raise IndexError("end of index file") elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_RECORD: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def total_text_size(self): """Return the sum of sizes of all file texts. This is how much space they would occupy if they were stored without delta and gzip compression. As a side effect this completely validates the Revfile, checking that all texts can be reproduced with the correct SHA-1.""" t = 0L for idx in range(len(self)): t += len(self.get(idx)) return t def main(argv): r = Revfile("testrev") try: cmd = argv[1] except IndexError: sys.stderr.write("usage: revfile dump\n" " revfile add\n" " revfile add-delta BASE\n" " revfile get IDX\n" " revfile find-sha HEX\n" " revfile total-text-size\n" " revfile last\n") return 1 if cmd == 'add': new_idx = r.add(sys.stdin.read()) print new_idx elif cmd == 'add-delta': new_idx = r.add(sys.stdin.read(), int(argv[2])) print new_idx elif cmd == 'dump': r.dump() elif cmd == 'get': try: idx = int(argv[2]) except IndexError: sys.stderr.write("usage: revfile get IDX\n") return 1 if idx < 0 or idx >= len(r): sys.stderr.write("invalid index %r\n" % idx) return 1 sys.stdout.write(r.get(idx)) elif cmd == 'find-sha': try: s = unhexlify(argv[2]) except IndexError: sys.stderr.write("usage: revfile find-sha HEX\n") return 1 idx = r.find_sha(s) if idx == _NO_RECORD: sys.stderr.write("no such record\n") return 1 else: print idx elif cmd == 'total-text-size': print r.total_text_size() elif cmd == 'last': print len(r)-1 else: sys.stderr.write("unknown command %r\n" % cmd) return 1 if __name__ == '__main__': import sys sys.exit(main(sys.argv) or 0) commit refs/heads/master mark :227 committer 1113028199 +1000 data 29 increase patch chaining limit from :226 M 644 inline bzrlib/revfile.py data 15265 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. This performs pretty well except when trying to calculate deltas of really large files. For that the main thing would be to plug in something faster than difflib, which is after all pure Python. Another approach is to just store the gzipped full text of big files, though perhaps that's too perverse? """ # TODO: Something like pread() would make this slightly simpler and # perhaps more efficient. # TODO: Could also try to mmap things... Might be faster for the # index in particular? # TODO: Some kind of faster lookup of SHAs? The bad thing is that probably means # rewriting existing records, which is not so nice. # TODO: Something to check that regions identified in the index file # completely butt up and do not overlap. Strictly it's not a problem # if there are gaps and that can happen if we're interrupted while # writing to the datafile. Overlapping would be very bad though. import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify, unhexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_RECORD = 0xFFFFFFFFL # fields in the index record I_SHA = 0 I_BASE = 1 I_FLAGS = 2 I_OFFSET = 3 I_LEN = 4 FL_GZIP = 1 # maximum number of patches in a row before recording a whole text. CHAIN_LIMIT = 50 class RevfileError(Exception): pass class LimitHitException(Exception): pass class Revfile: def __init__(self, basename): # TODO: Option to open readonly # TODO: Lock file while open # TODO: advise of random access self.basename = basename idxname = basename + '.irev' dataname = basename + '.drev' idx_exists = os.path.exists(idxname) data_exists = os.path.exists(dataname) if idx_exists != data_exists: raise RevfileError("half-assed revfile") if not idx_exists: self.idxfile = open(idxname, 'w+b') self.datafile = open(dataname, 'w+b') print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: self.idxfile = open(idxname, 'r+b') self.datafile = open(dataname, 'r+b') h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def _check_index(self, idx): if idx < 0 or idx > len(self): raise RevfileError("invalid index %r" % idx) def find_sha(self, s): assert isinstance(s, str) assert len(s) == 20 for idx, idxrec in enumerate(self): if idxrec[I_SHA] == s: return idx else: return _NO_RECORD def _add_compressed(self, text_sha, data, base, compress): # well, maybe compress flags = 0 if compress: data_len = len(data) if data_len > 50: # don't do compression if it's too small; it's unlikely to win # enough to be worthwhile compr_data = zlib.compress(data) compr_len = len(compr_data) if compr_len < data_len: data = compr_data flags = FL_GZIP ##print '- compressed %d -> %d, %.1f%%' \ ## % (data_len, compr_len, float(compr_len)/float(data_len) * 100.0) return self._add_raw(text_sha, data, base, flags) def _add_raw(self, text_sha, data, base, flags): """Add pre-processed data, can be either full text or delta. This does the compression if that makes sense.""" idx = len(self) self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * (idx + 1) data_offset = self.datafile.tell() assert isinstance(data, str) # not unicode or anything wierd self.datafile.write(data) self.datafile.flush() assert isinstance(text_sha, str) entry = text_sha entry += struct.pack(">IIII12x", base, flags, data_offset, len(data)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def _add_full_text(self, text, text_sha, compress): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" return self._add_compressed(text_sha, text, _NO_RECORD, compress) def _add_delta(self, text, text_sha, base, compress): """Add a text stored relative to a previous text.""" self._check_index(base) try: base_text = self.get(base, recursion_limit=CHAIN_LIMIT) except LimitHitException: return self._add_full_text(text, text_sha, compress) data = mdiff.bdiff(base_text, text) # If the delta is larger than the text, we might as well just # store the text. (OK, the delta might be more compressible, # but the overhead of applying it probably still makes it # bad, and I don't want to compress both of them to find out.) if len(data) >= len(text): return self._add_full_text(text, text_sha, compress) else: return self._add_compressed(text_sha, data, base, compress) def add(self, text, base=_NO_RECORD, compress=True): """Add a new text to the revfile. If the text is already present them its existing id is returned and the file is not changed. If compress is true then gzip compression will be used if it reduces the size. If a base index is specified, that text *may* be used for delta compression of the new text. Delta compression will only be used if it would be a size win and if the existing base is not at too long of a delta chain already. """ text_sha = sha.new(text).digest() idx = self.find_sha(text_sha) if idx != _NO_RECORD: # TODO: Optional paranoid mode where we read out that record and make sure # it's the same, in case someone ever breaks SHA-1. return idx # already present if base == _NO_RECORD: return self._add_full_text(text, text_sha, compress) else: return self._add_delta(text, text_sha, base, compress) def get(self, idx, recursion_limit=None): """Retrieve text of a previous revision. If recursion_limit is an integer then walk back at most that many revisions and then raise LimitHitException, indicating that we ought to record a new file text instead of another delta. Don't use this when trying to get out an existing revision.""" idxrec = self[idx] base = idxrec[I_BASE] if base == _NO_RECORD: text = self._get_full_text(idx, idxrec) else: text = self._get_patched(idx, idxrec, recursion_limit) if sha.new(text).digest() != idxrec[I_SHA]: raise RevfileError("corrupt SHA-1 digest on record %d" % idx) return text def _get_raw(self, idx, idxrec): flags = idxrec[I_FLAGS] if flags & ~FL_GZIP: raise RevfileError("unsupported index flags %#x on index %d" % (flags, idx)) l = idxrec[I_LEN] if l == 0: return '' self.datafile.seek(idxrec[I_OFFSET]) data = self.datafile.read(l) if len(data) != l: raise RevfileError("short read %d of %d " "getting text for record %d in %r" % (len(data), l, idx, self.basename)) if flags & FL_GZIP: data = zlib.decompress(data) return data def _get_full_text(self, idx, idxrec): assert idxrec[I_BASE] == _NO_RECORD text = self._get_raw(idx, idxrec) return text def _get_patched(self, idx, idxrec, recursion_limit): base = idxrec[I_BASE] assert base >= 0 assert base < idx # no loops! if recursion_limit == None: sub_limit = None else: sub_limit = recursion_limit - 1 if sub_limit < 0: raise LimitHitException() base_text = self.get(base, sub_limit) patch = self._get_raw(idx, idxrec) text = mdiff.bpatch(base_text, patch) return text def __len__(self): """Return number of revisions.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) if l < _RECORDSIZE: raise RevfileError("no header present in index of %r" % (self.basename)) return int(l / _RECORDSIZE) - 1 def __getitem__(self, idx): """Index by sequence id returns the index field""" ## TODO: Can avoid seek if we just moved there... self._seek_index(idx) return self._read_next_index() def _seek_index(self, idx): if idx < 0: raise RevfileError("invalid index %r" % idx) self.idxfile.seek((idx + 1) * _RECORDSIZE) def _read_next_index(self): rec = self.idxfile.read(_RECORDSIZE) if not rec: raise IndexError("end of index file") elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_RECORD: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def total_text_size(self): """Return the sum of sizes of all file texts. This is how much space they would occupy if they were stored without delta and gzip compression. As a side effect this completely validates the Revfile, checking that all texts can be reproduced with the correct SHA-1.""" t = 0L for idx in range(len(self)): t += len(self.get(idx)) return t def main(argv): r = Revfile("testrev") try: cmd = argv[1] except IndexError: sys.stderr.write("usage: revfile dump\n" " revfile add\n" " revfile add-delta BASE\n" " revfile get IDX\n" " revfile find-sha HEX\n" " revfile total-text-size\n" " revfile last\n") return 1 if cmd == 'add': new_idx = r.add(sys.stdin.read()) print new_idx elif cmd == 'add-delta': new_idx = r.add(sys.stdin.read(), int(argv[2])) print new_idx elif cmd == 'dump': r.dump() elif cmd == 'get': try: idx = int(argv[2]) except IndexError: sys.stderr.write("usage: revfile get IDX\n") return 1 if idx < 0 or idx >= len(r): sys.stderr.write("invalid index %r\n" % idx) return 1 sys.stdout.write(r.get(idx)) elif cmd == 'find-sha': try: s = unhexlify(argv[2]) except IndexError: sys.stderr.write("usage: revfile find-sha HEX\n") return 1 idx = r.find_sha(s) if idx == _NO_RECORD: sys.stderr.write("no such record\n") return 1 else: print idx elif cmd == 'total-text-size': print r.total_text_size() elif cmd == 'last': print len(r)-1 else: sys.stderr.write("unknown command %r\n" % cmd) return 1 if __name__ == '__main__': import sys sys.exit(main(sys.argv) or 0) commit refs/heads/master mark :228 committer 1113028600 +1000 data 29 update rsync exclude patterns from :227 M 644 inline .rsyncexclude data 115 *.pyc *.pyo *~ # arch can bite me {arch} .arch-ids ,,* ++* /doc/*.html *.tmp bzr-test.log [#]*# .#* testrev.* /tmp commit refs/heads/master mark :229 committer 1113029450 +1000 data 37 Allow opening revision file read-only from :228 M 644 inline bzrlib/revfile.py data 15817 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. This performs pretty well except when trying to calculate deltas of really large files. For that the main thing would be to plug in something faster than difflib, which is after all pure Python. Another approach is to just store the gzipped full text of big files, though perhaps that's too perverse? """ # TODO: Something like pread() would make this slightly simpler and # perhaps more efficient. # TODO: Could also try to mmap things... Might be faster for the # index in particular? # TODO: Some kind of faster lookup of SHAs? The bad thing is that probably means # rewriting existing records, which is not so nice. # TODO: Something to check that regions identified in the index file # completely butt up and do not overlap. Strictly it's not a problem # if there are gaps and that can happen if we're interrupted while # writing to the datafile. Overlapping would be very bad though. import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify, unhexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_RECORD = 0xFFFFFFFFL # fields in the index record I_SHA = 0 I_BASE = 1 I_FLAGS = 2 I_OFFSET = 3 I_LEN = 4 FL_GZIP = 1 # maximum number of patches in a row before recording a whole text. CHAIN_LIMIT = 50 class RevfileError(Exception): pass class LimitHitException(Exception): pass class Revfile: def __init__(self, basename, mode): # TODO: Lock file while open # TODO: advise of random access self.basename = basename if mode not in ['r', 'w']: raise RevfileError("invalid open mode %r" % mode) self.mode = mode idxname = basename + '.irev' dataname = basename + '.drev' idx_exists = os.path.exists(idxname) data_exists = os.path.exists(dataname) if idx_exists != data_exists: raise RevfileError("half-assed revfile") if not idx_exists: if mode == 'r': raise RevfileError("Revfile %r does not exist" % basename) self.idxfile = open(idxname, 'w+b') self.datafile = open(dataname, 'w+b') print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: if mode == 'r': diskmode = 'rb' else: diskmode = 'r+b' self.idxfile = open(idxname, diskmode) self.datafile = open(dataname, diskmode) h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def _check_index(self, idx): if idx < 0 or idx > len(self): raise RevfileError("invalid index %r" % idx) def _check_write(self): if self.mode != 'w': raise RevfileError("%r is open readonly" % self.basename) def find_sha(self, s): assert isinstance(s, str) assert len(s) == 20 for idx, idxrec in enumerate(self): if idxrec[I_SHA] == s: return idx else: return _NO_RECORD def _add_compressed(self, text_sha, data, base, compress): # well, maybe compress flags = 0 if compress: data_len = len(data) if data_len > 50: # don't do compression if it's too small; it's unlikely to win # enough to be worthwhile compr_data = zlib.compress(data) compr_len = len(compr_data) if compr_len < data_len: data = compr_data flags = FL_GZIP ##print '- compressed %d -> %d, %.1f%%' \ ## % (data_len, compr_len, float(compr_len)/float(data_len) * 100.0) return self._add_raw(text_sha, data, base, flags) def _add_raw(self, text_sha, data, base, flags): """Add pre-processed data, can be either full text or delta. This does the compression if that makes sense.""" idx = len(self) self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * (idx + 1) data_offset = self.datafile.tell() assert isinstance(data, str) # not unicode or anything wierd self.datafile.write(data) self.datafile.flush() assert isinstance(text_sha, str) entry = text_sha entry += struct.pack(">IIII12x", base, flags, data_offset, len(data)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def _add_full_text(self, text, text_sha, compress): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" return self._add_compressed(text_sha, text, _NO_RECORD, compress) def _add_delta(self, text, text_sha, base, compress): """Add a text stored relative to a previous text.""" self._check_index(base) try: base_text = self.get(base, recursion_limit=CHAIN_LIMIT) except LimitHitException: return self._add_full_text(text, text_sha, compress) data = mdiff.bdiff(base_text, text) # If the delta is larger than the text, we might as well just # store the text. (OK, the delta might be more compressible, # but the overhead of applying it probably still makes it # bad, and I don't want to compress both of them to find out.) if len(data) >= len(text): return self._add_full_text(text, text_sha, compress) else: return self._add_compressed(text_sha, data, base, compress) def add(self, text, base=_NO_RECORD, compress=True): """Add a new text to the revfile. If the text is already present them its existing id is returned and the file is not changed. If compress is true then gzip compression will be used if it reduces the size. If a base index is specified, that text *may* be used for delta compression of the new text. Delta compression will only be used if it would be a size win and if the existing base is not at too long of a delta chain already. """ self._check_write() text_sha = sha.new(text).digest() idx = self.find_sha(text_sha) if idx != _NO_RECORD: # TODO: Optional paranoid mode where we read out that record and make sure # it's the same, in case someone ever breaks SHA-1. return idx # already present if base == _NO_RECORD: return self._add_full_text(text, text_sha, compress) else: return self._add_delta(text, text_sha, base, compress) def get(self, idx, recursion_limit=None): """Retrieve text of a previous revision. If recursion_limit is an integer then walk back at most that many revisions and then raise LimitHitException, indicating that we ought to record a new file text instead of another delta. Don't use this when trying to get out an existing revision.""" idxrec = self[idx] base = idxrec[I_BASE] if base == _NO_RECORD: text = self._get_full_text(idx, idxrec) else: text = self._get_patched(idx, idxrec, recursion_limit) if sha.new(text).digest() != idxrec[I_SHA]: raise RevfileError("corrupt SHA-1 digest on record %d" % idx) return text def _get_raw(self, idx, idxrec): flags = idxrec[I_FLAGS] if flags & ~FL_GZIP: raise RevfileError("unsupported index flags %#x on index %d" % (flags, idx)) l = idxrec[I_LEN] if l == 0: return '' self.datafile.seek(idxrec[I_OFFSET]) data = self.datafile.read(l) if len(data) != l: raise RevfileError("short read %d of %d " "getting text for record %d in %r" % (len(data), l, idx, self.basename)) if flags & FL_GZIP: data = zlib.decompress(data) return data def _get_full_text(self, idx, idxrec): assert idxrec[I_BASE] == _NO_RECORD text = self._get_raw(idx, idxrec) return text def _get_patched(self, idx, idxrec, recursion_limit): base = idxrec[I_BASE] assert base >= 0 assert base < idx # no loops! if recursion_limit == None: sub_limit = None else: sub_limit = recursion_limit - 1 if sub_limit < 0: raise LimitHitException() base_text = self.get(base, sub_limit) patch = self._get_raw(idx, idxrec) text = mdiff.bpatch(base_text, patch) return text def __len__(self): """Return number of revisions.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) if l < _RECORDSIZE: raise RevfileError("no header present in index of %r" % (self.basename)) return int(l / _RECORDSIZE) - 1 def __getitem__(self, idx): """Index by sequence id returns the index field""" ## TODO: Can avoid seek if we just moved there... self._seek_index(idx) return self._read_next_index() def _seek_index(self, idx): if idx < 0: raise RevfileError("invalid index %r" % idx) self.idxfile.seek((idx + 1) * _RECORDSIZE) def _read_next_index(self): rec = self.idxfile.read(_RECORDSIZE) if not rec: raise IndexError("end of index file") elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_RECORD: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def total_text_size(self): """Return the sum of sizes of all file texts. This is how much space they would occupy if they were stored without delta and gzip compression. As a side effect this completely validates the Revfile, checking that all texts can be reproduced with the correct SHA-1.""" t = 0L for idx in range(len(self)): t += len(self.get(idx)) return t def main(argv): try: cmd = argv[1] except IndexError: sys.stderr.write("usage: revfile dump\n" " revfile add\n" " revfile add-delta BASE\n" " revfile get IDX\n" " revfile find-sha HEX\n" " revfile total-text-size\n" " revfile last\n") return 1 def rw(): return Revfile('testrev', 'w') def ro(): return Revfile('testrev', 'r') if cmd == 'add': print rw().add(sys.stdin.read()) elif cmd == 'add-delta': print rw().add(sys.stdin.read(), int(argv[2])) elif cmd == 'dump': ro().dump() elif cmd == 'get': try: idx = int(argv[2]) except IndexError: sys.stderr.write("usage: revfile get IDX\n") return 1 if idx < 0 or idx >= len(r): sys.stderr.write("invalid index %r\n" % idx) return 1 sys.stdout.write(ro().get(idx)) elif cmd == 'find-sha': try: s = unhexlify(argv[2]) except IndexError: sys.stderr.write("usage: revfile find-sha HEX\n") return 1 idx = ro().find_sha(s) if idx == _NO_RECORD: sys.stderr.write("no such record\n") return 1 else: print idx elif cmd == 'total-text-size': print ro().total_text_size() elif cmd == 'last': print len(ro())-1 else: sys.stderr.write("unknown command %r\n" % cmd) return 1 if __name__ == '__main__': import sys sys.exit(main(sys.argv) or 0) commit refs/heads/master mark :230 committer 1113030267 +1000 data 74 Revfile: better __iter__ method that reads the whole index file in one go! from :229 M 644 inline bzrlib/revfile.py data 16238 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. This performs pretty well except when trying to calculate deltas of really large files. For that the main thing would be to plug in something faster than difflib, which is after all pure Python. Another approach is to just store the gzipped full text of big files, though perhaps that's too perverse? """ # TODO: Something like pread() would make this slightly simpler and # perhaps more efficient. # TODO: Could also try to mmap things... Might be faster for the # index in particular? # TODO: Some kind of faster lookup of SHAs? The bad thing is that probably means # rewriting existing records, which is not so nice. # TODO: Something to check that regions identified in the index file # completely butt up and do not overlap. Strictly it's not a problem # if there are gaps and that can happen if we're interrupted while # writing to the datafile. Overlapping would be very bad though. import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify, unhexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_RECORD = 0xFFFFFFFFL # fields in the index record I_SHA = 0 I_BASE = 1 I_FLAGS = 2 I_OFFSET = 3 I_LEN = 4 FL_GZIP = 1 # maximum number of patches in a row before recording a whole text. CHAIN_LIMIT = 50 class RevfileError(Exception): pass class LimitHitException(Exception): pass class Revfile: def __init__(self, basename, mode): # TODO: Lock file while open # TODO: advise of random access self.basename = basename if mode not in ['r', 'w']: raise RevfileError("invalid open mode %r" % mode) self.mode = mode idxname = basename + '.irev' dataname = basename + '.drev' idx_exists = os.path.exists(idxname) data_exists = os.path.exists(dataname) if idx_exists != data_exists: raise RevfileError("half-assed revfile") if not idx_exists: if mode == 'r': raise RevfileError("Revfile %r does not exist" % basename) self.idxfile = open(idxname, 'w+b') self.datafile = open(dataname, 'w+b') print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: if mode == 'r': diskmode = 'rb' else: diskmode = 'r+b' self.idxfile = open(idxname, diskmode) self.datafile = open(dataname, diskmode) h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def _check_index(self, idx): if idx < 0 or idx > len(self): raise RevfileError("invalid index %r" % idx) def _check_write(self): if self.mode != 'w': raise RevfileError("%r is open readonly" % self.basename) def find_sha(self, s): assert isinstance(s, str) assert len(s) == 20 for idx, idxrec in enumerate(self): if idxrec[I_SHA] == s: return idx else: return _NO_RECORD def _add_compressed(self, text_sha, data, base, compress): # well, maybe compress flags = 0 if compress: data_len = len(data) if data_len > 50: # don't do compression if it's too small; it's unlikely to win # enough to be worthwhile compr_data = zlib.compress(data) compr_len = len(compr_data) if compr_len < data_len: data = compr_data flags = FL_GZIP ##print '- compressed %d -> %d, %.1f%%' \ ## % (data_len, compr_len, float(compr_len)/float(data_len) * 100.0) return self._add_raw(text_sha, data, base, flags) def _add_raw(self, text_sha, data, base, flags): """Add pre-processed data, can be either full text or delta. This does the compression if that makes sense.""" idx = len(self) self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * (idx + 1) data_offset = self.datafile.tell() assert isinstance(data, str) # not unicode or anything wierd self.datafile.write(data) self.datafile.flush() assert isinstance(text_sha, str) entry = text_sha entry += struct.pack(">IIII12x", base, flags, data_offset, len(data)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def _add_full_text(self, text, text_sha, compress): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" return self._add_compressed(text_sha, text, _NO_RECORD, compress) def _add_delta(self, text, text_sha, base, compress): """Add a text stored relative to a previous text.""" self._check_index(base) try: base_text = self.get(base, recursion_limit=CHAIN_LIMIT) except LimitHitException: return self._add_full_text(text, text_sha, compress) data = mdiff.bdiff(base_text, text) # If the delta is larger than the text, we might as well just # store the text. (OK, the delta might be more compressible, # but the overhead of applying it probably still makes it # bad, and I don't want to compress both of them to find out.) if len(data) >= len(text): return self._add_full_text(text, text_sha, compress) else: return self._add_compressed(text_sha, data, base, compress) def add(self, text, base=_NO_RECORD, compress=True): """Add a new text to the revfile. If the text is already present them its existing id is returned and the file is not changed. If compress is true then gzip compression will be used if it reduces the size. If a base index is specified, that text *may* be used for delta compression of the new text. Delta compression will only be used if it would be a size win and if the existing base is not at too long of a delta chain already. """ self._check_write() text_sha = sha.new(text).digest() idx = self.find_sha(text_sha) if idx != _NO_RECORD: # TODO: Optional paranoid mode where we read out that record and make sure # it's the same, in case someone ever breaks SHA-1. return idx # already present if base == _NO_RECORD: return self._add_full_text(text, text_sha, compress) else: return self._add_delta(text, text_sha, base, compress) def get(self, idx, recursion_limit=None): """Retrieve text of a previous revision. If recursion_limit is an integer then walk back at most that many revisions and then raise LimitHitException, indicating that we ought to record a new file text instead of another delta. Don't use this when trying to get out an existing revision.""" idxrec = self[idx] base = idxrec[I_BASE] if base == _NO_RECORD: text = self._get_full_text(idx, idxrec) else: text = self._get_patched(idx, idxrec, recursion_limit) if sha.new(text).digest() != idxrec[I_SHA]: raise RevfileError("corrupt SHA-1 digest on record %d" % idx) return text def _get_raw(self, idx, idxrec): flags = idxrec[I_FLAGS] if flags & ~FL_GZIP: raise RevfileError("unsupported index flags %#x on index %d" % (flags, idx)) l = idxrec[I_LEN] if l == 0: return '' self.datafile.seek(idxrec[I_OFFSET]) data = self.datafile.read(l) if len(data) != l: raise RevfileError("short read %d of %d " "getting text for record %d in %r" % (len(data), l, idx, self.basename)) if flags & FL_GZIP: data = zlib.decompress(data) return data def _get_full_text(self, idx, idxrec): assert idxrec[I_BASE] == _NO_RECORD text = self._get_raw(idx, idxrec) return text def _get_patched(self, idx, idxrec, recursion_limit): base = idxrec[I_BASE] assert base >= 0 assert base < idx # no loops! if recursion_limit == None: sub_limit = None else: sub_limit = recursion_limit - 1 if sub_limit < 0: raise LimitHitException() base_text = self.get(base, sub_limit) patch = self._get_raw(idx, idxrec) text = mdiff.bpatch(base_text, patch) return text def __len__(self): """Return number of revisions.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) if l < _RECORDSIZE: raise RevfileError("no header present in index of %r" % (self.basename)) return int(l / _RECORDSIZE) - 1 def __getitem__(self, idx): """Index by sequence id returns the index field""" ## TODO: Can avoid seek if we just moved there... self._seek_index(idx) idxrec = self._read_next_index() if idxrec == None: raise IndexError() else: return idxrec def _seek_index(self, idx): if idx < 0: raise RevfileError("invalid index %r" % idx) self.idxfile.seek((idx + 1) * _RECORDSIZE) def __iter__(self): """Read back all index records. Do not seek the index file while this is underway!""" sys.stderr.write(" ** iter called ** \n") self._seek_index(0) while True: idxrec = self._read_next_index() if not idxrec: break yield idxrec def _read_next_index(self): rec = self.idxfile.read(_RECORDSIZE) if not rec: return None elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_RECORD: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def total_text_size(self): """Return the sum of sizes of all file texts. This is how much space they would occupy if they were stored without delta and gzip compression. As a side effect this completely validates the Revfile, checking that all texts can be reproduced with the correct SHA-1.""" t = 0L for idx in range(len(self)): t += len(self.get(idx)) return t def main(argv): try: cmd = argv[1] except IndexError: sys.stderr.write("usage: revfile dump\n" " revfile add\n" " revfile add-delta BASE\n" " revfile get IDX\n" " revfile find-sha HEX\n" " revfile total-text-size\n" " revfile last\n") return 1 def rw(): return Revfile('testrev', 'w') def ro(): return Revfile('testrev', 'r') if cmd == 'add': print rw().add(sys.stdin.read()) elif cmd == 'add-delta': print rw().add(sys.stdin.read(), int(argv[2])) elif cmd == 'dump': ro().dump() elif cmd == 'get': try: idx = int(argv[2]) except IndexError: sys.stderr.write("usage: revfile get IDX\n") return 1 if idx < 0 or idx >= len(r): sys.stderr.write("invalid index %r\n" % idx) return 1 sys.stdout.write(ro().get(idx)) elif cmd == 'find-sha': try: s = unhexlify(argv[2]) except IndexError: sys.stderr.write("usage: revfile find-sha HEX\n") return 1 idx = ro().find_sha(s) if idx == _NO_RECORD: sys.stderr.write("no such record\n") return 1 else: print idx elif cmd == 'total-text-size': print ro().total_text_size() elif cmd == 'last': print len(ro())-1 else: sys.stderr.write("unknown command %r\n" % cmd) return 1 if __name__ == '__main__': import sys sys.exit(main(sys.argv) or 0) commit refs/heads/master mark :231 committer 1113106768 +1000 data 11 revfile doc from :230 M 644 inline bzrlib/revfile.py data 16487 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. This performs pretty well except when trying to calculate deltas of really large files. For that the main thing would be to plug in something faster than difflib, which is after all pure Python. Another approach is to just store the gzipped full text of big files, though perhaps that's too perverse? The iter method here will generally read through the whole index file in one go. With readahead in the kernel and python/libc (typically 128kB) this means that there should be no seeks and often only one read() call to get everything into memory. """ # TODO: Something like pread() would make this slightly simpler and # perhaps more efficient. # TODO: Could also try to mmap things... Might be faster for the # index in particular? # TODO: Some kind of faster lookup of SHAs? The bad thing is that probably means # rewriting existing records, which is not so nice. # TODO: Something to check that regions identified in the index file # completely butt up and do not overlap. Strictly it's not a problem # if there are gaps and that can happen if we're interrupted while # writing to the datafile. Overlapping would be very bad though. import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify, unhexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_RECORD = 0xFFFFFFFFL # fields in the index record I_SHA = 0 I_BASE = 1 I_FLAGS = 2 I_OFFSET = 3 I_LEN = 4 FL_GZIP = 1 # maximum number of patches in a row before recording a whole text. CHAIN_LIMIT = 50 class RevfileError(Exception): pass class LimitHitException(Exception): pass class Revfile: def __init__(self, basename, mode): # TODO: Lock file while open # TODO: advise of random access self.basename = basename if mode not in ['r', 'w']: raise RevfileError("invalid open mode %r" % mode) self.mode = mode idxname = basename + '.irev' dataname = basename + '.drev' idx_exists = os.path.exists(idxname) data_exists = os.path.exists(dataname) if idx_exists != data_exists: raise RevfileError("half-assed revfile") if not idx_exists: if mode == 'r': raise RevfileError("Revfile %r does not exist" % basename) self.idxfile = open(idxname, 'w+b') self.datafile = open(dataname, 'w+b') print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: if mode == 'r': diskmode = 'rb' else: diskmode = 'r+b' self.idxfile = open(idxname, diskmode) self.datafile = open(dataname, diskmode) h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def _check_index(self, idx): if idx < 0 or idx > len(self): raise RevfileError("invalid index %r" % idx) def _check_write(self): if self.mode != 'w': raise RevfileError("%r is open readonly" % self.basename) def find_sha(self, s): assert isinstance(s, str) assert len(s) == 20 for idx, idxrec in enumerate(self): if idxrec[I_SHA] == s: return idx else: return _NO_RECORD def _add_compressed(self, text_sha, data, base, compress): # well, maybe compress flags = 0 if compress: data_len = len(data) if data_len > 50: # don't do compression if it's too small; it's unlikely to win # enough to be worthwhile compr_data = zlib.compress(data) compr_len = len(compr_data) if compr_len < data_len: data = compr_data flags = FL_GZIP ##print '- compressed %d -> %d, %.1f%%' \ ## % (data_len, compr_len, float(compr_len)/float(data_len) * 100.0) return self._add_raw(text_sha, data, base, flags) def _add_raw(self, text_sha, data, base, flags): """Add pre-processed data, can be either full text or delta. This does the compression if that makes sense.""" idx = len(self) self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * (idx + 1) data_offset = self.datafile.tell() assert isinstance(data, str) # not unicode or anything wierd self.datafile.write(data) self.datafile.flush() assert isinstance(text_sha, str) entry = text_sha entry += struct.pack(">IIII12x", base, flags, data_offset, len(data)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def _add_full_text(self, text, text_sha, compress): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" return self._add_compressed(text_sha, text, _NO_RECORD, compress) def _add_delta(self, text, text_sha, base, compress): """Add a text stored relative to a previous text.""" self._check_index(base) try: base_text = self.get(base, recursion_limit=CHAIN_LIMIT) except LimitHitException: return self._add_full_text(text, text_sha, compress) data = mdiff.bdiff(base_text, text) # If the delta is larger than the text, we might as well just # store the text. (OK, the delta might be more compressible, # but the overhead of applying it probably still makes it # bad, and I don't want to compress both of them to find out.) if len(data) >= len(text): return self._add_full_text(text, text_sha, compress) else: return self._add_compressed(text_sha, data, base, compress) def add(self, text, base=_NO_RECORD, compress=True): """Add a new text to the revfile. If the text is already present them its existing id is returned and the file is not changed. If compress is true then gzip compression will be used if it reduces the size. If a base index is specified, that text *may* be used for delta compression of the new text. Delta compression will only be used if it would be a size win and if the existing base is not at too long of a delta chain already. """ self._check_write() text_sha = sha.new(text).digest() idx = self.find_sha(text_sha) if idx != _NO_RECORD: # TODO: Optional paranoid mode where we read out that record and make sure # it's the same, in case someone ever breaks SHA-1. return idx # already present if base == _NO_RECORD: return self._add_full_text(text, text_sha, compress) else: return self._add_delta(text, text_sha, base, compress) def get(self, idx, recursion_limit=None): """Retrieve text of a previous revision. If recursion_limit is an integer then walk back at most that many revisions and then raise LimitHitException, indicating that we ought to record a new file text instead of another delta. Don't use this when trying to get out an existing revision.""" idxrec = self[idx] base = idxrec[I_BASE] if base == _NO_RECORD: text = self._get_full_text(idx, idxrec) else: text = self._get_patched(idx, idxrec, recursion_limit) if sha.new(text).digest() != idxrec[I_SHA]: raise RevfileError("corrupt SHA-1 digest on record %d" % idx) return text def _get_raw(self, idx, idxrec): flags = idxrec[I_FLAGS] if flags & ~FL_GZIP: raise RevfileError("unsupported index flags %#x on index %d" % (flags, idx)) l = idxrec[I_LEN] if l == 0: return '' self.datafile.seek(idxrec[I_OFFSET]) data = self.datafile.read(l) if len(data) != l: raise RevfileError("short read %d of %d " "getting text for record %d in %r" % (len(data), l, idx, self.basename)) if flags & FL_GZIP: data = zlib.decompress(data) return data def _get_full_text(self, idx, idxrec): assert idxrec[I_BASE] == _NO_RECORD text = self._get_raw(idx, idxrec) return text def _get_patched(self, idx, idxrec, recursion_limit): base = idxrec[I_BASE] assert base >= 0 assert base < idx # no loops! if recursion_limit == None: sub_limit = None else: sub_limit = recursion_limit - 1 if sub_limit < 0: raise LimitHitException() base_text = self.get(base, sub_limit) patch = self._get_raw(idx, idxrec) text = mdiff.bpatch(base_text, patch) return text def __len__(self): """Return number of revisions.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) if l < _RECORDSIZE: raise RevfileError("no header present in index of %r" % (self.basename)) return int(l / _RECORDSIZE) - 1 def __getitem__(self, idx): """Index by sequence id returns the index field""" ## TODO: Can avoid seek if we just moved there... self._seek_index(idx) idxrec = self._read_next_index() if idxrec == None: raise IndexError() else: return idxrec def _seek_index(self, idx): if idx < 0: raise RevfileError("invalid index %r" % idx) self.idxfile.seek((idx + 1) * _RECORDSIZE) def __iter__(self): """Read back all index records. Do not seek the index file while this is underway!""" sys.stderr.write(" ** iter called ** \n") self._seek_index(0) while True: idxrec = self._read_next_index() if not idxrec: break yield idxrec def _read_next_index(self): rec = self.idxfile.read(_RECORDSIZE) if not rec: return None elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_RECORD: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def total_text_size(self): """Return the sum of sizes of all file texts. This is how much space they would occupy if they were stored without delta and gzip compression. As a side effect this completely validates the Revfile, checking that all texts can be reproduced with the correct SHA-1.""" t = 0L for idx in range(len(self)): t += len(self.get(idx)) return t def main(argv): try: cmd = argv[1] except IndexError: sys.stderr.write("usage: revfile dump\n" " revfile add\n" " revfile add-delta BASE\n" " revfile get IDX\n" " revfile find-sha HEX\n" " revfile total-text-size\n" " revfile last\n") return 1 def rw(): return Revfile('testrev', 'w') def ro(): return Revfile('testrev', 'r') if cmd == 'add': print rw().add(sys.stdin.read()) elif cmd == 'add-delta': print rw().add(sys.stdin.read(), int(argv[2])) elif cmd == 'dump': ro().dump() elif cmd == 'get': try: idx = int(argv[2]) except IndexError: sys.stderr.write("usage: revfile get IDX\n") return 1 if idx < 0 or idx >= len(r): sys.stderr.write("invalid index %r\n" % idx) return 1 sys.stdout.write(ro().get(idx)) elif cmd == 'find-sha': try: s = unhexlify(argv[2]) except IndexError: sys.stderr.write("usage: revfile find-sha HEX\n") return 1 idx = ro().find_sha(s) if idx == _NO_RECORD: sys.stderr.write("no such record\n") return 1 else: print idx elif cmd == 'total-text-size': print ro().total_text_size() elif cmd == 'last': print len(ro())-1 else: sys.stderr.write("unknown command %r\n" % cmd) return 1 if __name__ == '__main__': import sys sys.exit(main(sys.argv) or 0) commit refs/heads/master mark :232 committer 1113181295 +1000 data 50 Allow docstrings for help to be in PEP0257 format. from :231 M 644 inline NEWS data 2691 bzr-0.0.4 NOT RELEASED YET ENHANCEMENTS: * bzr diff optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 29587 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, time, types, shutil, tempfile, traceback, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and new_name not in file_list: continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot prof = hotshot.Profile('.bzr.profile') ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load('.bzr.profile') #stats.strip_dirs() stats.sort_stats('time') stats.print_stats(20) return ret else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') traceback.print_exc(None, bzrlib.trace._tracefile) log_error('(see $HOME/.bzr.log for debug information)\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error('(see $HOME/.bzr.log for debug information)\n') traceback.print_exc(None, bzrlib.trace._tracefile) ## traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :233 committer 1113184738 +1000 data 82 - more output from test.sh - write revison-history in a way that is hardlink-safe from :232 M 644 inline bzrlib/branch.py data 33145 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. :todo: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. :todo: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. :todo: Keep the on-disk branch locked while the object exists. :todo: mkdir() method. """ def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. :param base: Base directory for the branch. :param init: If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. :param find_root: If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch""" return file(self.controlfilename(file_or_path), mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'wb').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'rb').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() inv = Inventory.read_xml(self.controlfile('inventory', 'r')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'w') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. This puts the files in the Added state, so that they will be recorded by the next commit. :todo: Perhaps have an option to add the ids even if the files do not (yet) exist. :todo: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. :todo: Option to specify file id. :todo: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on :todo: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True :todo: Do something useful with directories. :todo: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. :param timestamp: if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) self.append_revision(rev_id) if verbose: note("commited r%d" % self.revno()) def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. :todo: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] else: return None def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original'): """Write out human-readable log of commits to this branch :param utc: If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l revno += 1 precursor = p def rename_one(self, from_rel, to_rel): tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(self, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo :todo: Get state for single files. :todo: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = self.basis_tree() new = self.working_tree() for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("wierd file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" try: shutil.rmtree(self.base) except OSError: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/commands.py data 29657 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, time, types, shutil, tempfile, traceback, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and new_name not in file_list: continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot prof = hotshot.Profile('.bzr.profile') ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load('.bzr.profile') #stats.strip_dirs() stats.sort_stats('time') stats.print_stats(20) return ret else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') traceback.print_exc(None, bzrlib.trace._tracefile) log_error('(see $HOME/.bzr.log for debug information)\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error('(see $HOME/.bzr.log for debug information)\n') traceback.print_exc(None, bzrlib.trace._tracefile) ## traceback.print_exc(None, sys.stderr) return 1 # TODO: Maybe nicer handling of IOError? if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') M 644 inline test.sh data 2094 #! /bin/sh -pe # Simple shell-based tests for bzr. # This is meant to exercise the external behaviour, command line # parsing and similar things and compliment the inwardly-turned # testing done by doctest. # This must already exist and be in the right place if ! [ -d bzr-test.tmp ] then echo "please create directory bzr-test.tmp" exit 1 fi echo "testing `which bzr`" bzr --version | head -n 1 echo rm -rf bzr-test.tmp mkdir bzr-test.tmp # save it for real errors exec 3>&2 exec > bzr-test.log exec 2>&1 set -x quitter() { echo "tests failed, look in bzr-test.log" >&3; exit 2; } trap quitter ERR cd bzr-test.tmp rm -rf .bzr mkdir branch1 cd branch1 # some information commands bzr help bzr version # invalid commands are detected ! bzr pants # some experiments with renames bzr init echo "hello world" > test.txt bzr unknowns # should be the only unknown file [ "`bzr unknowns`" = test.txt ] # can't rename unversioned files; use the regular unix rename command ! bzr rename test.txt new-test.txt # ok, so now add it and see what happens bzr add test.txt [ -z "`bzr unknowns`" ] # after adding even before committing you can rename files bzr rename test.txt newname.txt [ "`bzr status`" = "A newname.txt" ] [ `bzr revno` = 0 ] bzr commit -m "add first revision" [ `bzr revno` = 1 ] # now more complicated renames mkdir sub1 ! bzr rename newname.txt sub1 ! bzr rename newname.txt sub1/foo.txt bzr add sub1 ! bzr rename newname.txt sub1 bzr rename newname.txt sub1/foo.txt [ -f sub1/foo.txt ] [ ! -f newname.txt ] bzr rename sub1/foo.txt newname.txt [ -f newname.txt ] bzr rename newname.txt sub1/foo.txt bzr rename sub1/foo.txt sub1/bar.txt cd sub1 mkdir sub2 bzr add sub2 bzr rename bar.txt sub2/bar.txt cd sub2 bzr rename bar.txt ../../bar.txt cd ../../ bzr commit -m "more renames" [ `bzr revno` = 2 ] # now try pulling that file back out, checking it was stored properly [ "`bzr cat -r 1 newname.txt`" = "hello world" ] ! bzr rename sub1 sub1/knotted-up # now test hardlinked branches in subdirectories echo "tests completed ok" >&3 commit refs/heads/master mark :234 committer 1113185024 +1000 data 54 - check that commits to hardlinked trees work properly from :233 M 644 inline test.sh data 2357 #! /bin/sh -pe # Simple shell-based tests for bzr. # This is meant to exercise the external behaviour, command line # parsing and similar things and compliment the inwardly-turned # testing done by doctest. # This must already exist and be in the right place if ! [ -d bzr-test.tmp ] then echo "please create directory bzr-test.tmp" exit 1 fi echo "testing `which bzr`" bzr --version | head -n 1 echo rm -rf bzr-test.tmp mkdir bzr-test.tmp # save it for real errors exec 3>&2 exec > bzr-test.log exec 2>&1 set -x quitter() { echo "tests failed, look in bzr-test.log" >&3; exit 2; } trap quitter ERR cd bzr-test.tmp rm -rf .bzr mkdir branch1 cd branch1 # some information commands bzr help bzr version # invalid commands are detected ! bzr pants # some experiments with renames bzr init echo "hello world" > test.txt bzr unknowns # should be the only unknown file [ "`bzr unknowns`" = test.txt ] # can't rename unversioned files; use the regular unix rename command ! bzr rename test.txt new-test.txt # ok, so now add it and see what happens bzr add test.txt [ -z "`bzr unknowns`" ] # after adding even before committing you can rename files bzr rename test.txt newname.txt [ "`bzr status`" = "A newname.txt" ] [ `bzr revno` = 0 ] bzr commit -m "add first revision" [ `bzr revno` = 1 ] # now more complicated renames mkdir sub1 ! bzr rename newname.txt sub1 ! bzr rename newname.txt sub1/foo.txt bzr add sub1 ! bzr rename newname.txt sub1 bzr rename newname.txt sub1/foo.txt [ -f sub1/foo.txt ] [ ! -f newname.txt ] bzr rename sub1/foo.txt newname.txt [ -f newname.txt ] bzr rename newname.txt sub1/foo.txt bzr rename sub1/foo.txt sub1/bar.txt cd sub1 mkdir sub2 bzr add sub2 bzr rename bar.txt sub2/bar.txt cd sub2 bzr rename bar.txt ../../bar.txt cd ../../ bzr commit -m "more renames" [ `bzr revno` = 2 ] # now try pulling that file back out, checking it was stored properly [ "`bzr cat -r 1 newname.txt`" = "hello world" ] ! bzr rename sub1 sub1/knotted-up # now test hardlinked branches in subdirectories cd .. [ -d branch2 ] && rm -rf branch2 cp -al branch1 branch2 cd branch2 bzr log [ `bzr revno` = 2 ] echo "added in branch2" > new-in-2.txt bzr add new-in-2.txt bzr commit -m "add file to branch 2 only" [ `bzr revno` = 3 ] cd ../branch1 [ `bzr revno` = 2 ] echo "tests completed ok" >&3 commit refs/heads/master mark :235 committer 1113185193 +1000 data 11 update NEWS from :234 M 644 inline NEWS data 2788 bzr-0.0.4 NOT RELEASED YET ENHANCEMENTS: * bzr diff optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. BUG FIXES: * Make commit safe for hardlinked bzr trees. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. commit refs/heads/master mark :236 committer 1113187485 +1000 data 38 - Experiments in inventory performance from :235 M 644 inline bzrlib/textinv.py data 2607 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from errors import BzrError from inventory import InventoryEntry, Inventory START_MARK = "# bzr inventory format 3\n" END_MARK = "# end of inventory\n" def escape(s): """Very simple URL-like escaping. (Why not just use backslashes? Because then we couldn't parse lines just by splitting on spaces.)""" return (s.replace('\\', r'\x5c') .replace(' ', r'\x20') .replace('\t', r'\x09') .replace('\n', r'\x0a')) def unescape(s): assert s.find(' ') == -1 s = (s.replace(r'\x20', ' ') .replace(r'\x09', '\t') .replace(r'\x0a', '\n') .replace(r'\x5c', '\\')) # TODO: What if there's anything else? return s def write_text_inventory(inv, outf): """Write out inv in a simple trad-unix text format.""" outf.write(START_MARK) for path, ie in inv.iter_entries(): if ie.kind == 'root_directory': continue outf.write(ie.file_id + ' ') outf.write(escape(ie.name) + ' ') outf.write(ie.kind + ' ') outf.write(ie.parent_id + ' ') if ie.kind == 'file': outf.write(ie.text_id) outf.write(' ' + ie.text_sha1) outf.write(' ' + str(ie.text_size)) outf.write("\n") outf.write(END_MARK) def read_text_inventory(tf): """Return an inventory read in from tf""" if tf.readline() != START_MARK: raise BzrError("missing start mark") inv = Inventory() for l in tf: fields = l.split(' ') if fields[0] == '#': break ie = {'file_id': fields[0], 'name': unescape(fields[1]), 'kind': fields[2], 'parent_id': fields[3]} ##inv.add(ie) if l != END_MARK: raise BzrError("missing end mark") return inv M 644 inline bzrlib/commands.py data 30024 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, time, types, shutil, tempfile, traceback, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and new_name not in file_list: continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_dump_text_inventory(): import bzrlib.textinv inv = Branch('.').basis_tree().inventory bzrlib.textinv.write_text_inventory(inv, sys.stdout) def cmd_load_text_inventory(): import bzrlib.textinv inv = bzrlib.textinv.read_text_inventory(sys.stdin) print 'loaded %d entries' % len(inv) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot prof = hotshot.Profile('.bzr.profile') ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load('.bzr.profile') #stats.strip_dirs() stats.sort_stats('time') stats.print_stats(20) return ret else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') traceback.print_exc(None, bzrlib.trace._tracefile) log_error('(see $HOME/.bzr.log for debug information)\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error('(see $HOME/.bzr.log for debug information)\n') traceback.print_exc(None, bzrlib.trace._tracefile) ## traceback.print_exc(None, sys.stderr) return 1 ## TODO: Trap AssertionError # TODO: Maybe nicer handling of IOError especially for broken pipe. if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :237 committer 1113187504 +1000 data 49 - Better assertions in InventoryEntry constructor from :236 M 644 inline bzrlib/inventory.py data 19311 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Inventories map files to their name in a revision.""" # TODO: Maybe store inventory_id in the file? Not really needed. __author__ = "Martin Pool " # This should really be an id randomly assigned when the tree is # created, but it's not for now. ROOT_ID = "TREE_ROOT" import sys, os.path, types, re from sets import Set try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from xml import XMLMixin from errors import bailout, BzrError import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') 'TREE_ROOT' >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID)) >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123')) >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' :todo: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ # TODO: split InventoryEntry into subclasses for files, # directories, etc etc. def __init__(self, file_id, name, kind, parent_id, text_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID) >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID) Traceback (most recent call last): BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", []) """ if len(splitpath(name)) != 1: bailout('InventoryEntry name is not a simple filename: %r' % name) self.file_id = file_id self.name = name self.kind = kind self.text_id = text_id self.parent_id = parent_id self.text_sha1 = None self.text_size = None if kind == 'directory': self.children = {} elif kind == 'file': pass else: raise BzrError("unhandled entry kind %r" % kind) def sorted_children(self): l = self.children.items() l.sort() return l def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.parent_id, text_id=self.text_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size != None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1']: v = getattr(self, f) if v != None: e.set(f, v) # to be conservative, we don't externalize the root pointers # for now, leaving them as null in the xml form. in a future # version it will be implied by nested elements. if self.parent_id != ROOT_ID: assert isinstance(self.parent_id, basestring) e.set('parent_id', self.parent_id) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' ## original format inventories don't have a parent_id for ## nodes in the root directory, but it's cleaner to use one ## internally. parent_id = elt.get('parent_id') if parent_id == None: parent_id = ROOT_ID self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __cmp__(self, other): if self is other: return 0 if not isinstance(other, InventoryEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.name, other.name) \ or cmp(self.text_sha1, other.text_sha1) \ or cmp(self.text_size, other.text_size) \ or cmp(self.text_id, other.text_id) \ or cmp(self.parent_id, other.parent_id) \ or cmp(self.kind, other.kind) class RootEntry(InventoryEntry): def __init__(self, file_id): self.file_id = file_id self.children = {} self.kind = 'root_directory' self.parent_id = None self.name = '' def __cmp__(self, other): if self is other: return 0 if not isinstance(other, RootEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.children, other.children) class Inventory(XMLMixin): """Inventory of versioned files in a tree. An Inventory acts like a set of InventoryEntry items. You can also look files up by their file_id or name. May be read from and written to a metadata file in a tree. To manipulate the inventory (for example to add a file), it is read in, modified, and then written back out. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted, other than through the Inventory API. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID)) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ ## TODO: Make sure only canonical filenames are stored. ## TODO: Do something sensible about the possible collisions on ## case-losing filesystems. Perhaps we should just always forbid ## such collisions. ## TODO: No special cases for root, rather just give it a file id ## like everything else. ## TODO: Probably change XML serialization to use nesting def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. The inventory is created with a default root directory, with an id of None. """ self.root = RootEntry(ROOT_ID) self._byid = {self.root.file_id: self.root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, from_dir=None): """Return (path, entry) pairs, in order by name.""" if from_dir == None: assert self.root from_dir = self.root elif isinstance(from_dir, basestring): from_dir = self._byid[from_dir] kids = from_dir.children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(from_dir=ie.file_id): yield '/'.join((name, cn)), cie def directories(self): """Return (path, entry) pairs for all directories. """ def descend(parent_ie): parent_name = parent_ie.name yield parent_name, parent_ie # directory children in sorted order dn = [] for ie in parent_ie.children.itervalues(): if ie.kind == 'directory': dn.append((ie.name, ie)) dn.sort() for name, child_ie in dn: for sub_name, sub_ie in descend(child_ie): yield appendpath(parent_name, sub_name), sub_ie for name, ie in descend(self.root): yield name, ie def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID)) >>> inv['123123'].name 'hello.c' """ if file_id == None: raise BzrError("can't look up file_id None") try: return self._byid[file_id] except KeyError: raise BzrError("file_id {%s} not in inventory" % file_id) def get_child(self, parent_id, filename): return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self._byid: bailout("inventory already contains entry with id {%s}" % entry.file_id) try: parent = self._byid[entry.parent_id] except KeyError: bailout("parent_id {%s} not in inventory" % entry.parent_id) if parent.children.has_key(entry.name): bailout("%s is already versioned" % appendpath(self.id2path(parent.file_id), entry.name)) self._byid[entry.file_id] = entry parent.children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: bailout("cannot re-add root of inventory") if file_id == None: file_id = bzrlib.branch.gen_file_id(relpath) parent_id = self.path2id(parts[:-1]) assert parent_id != None ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def id_set(self): return Set(self._byid) def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID)) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __cmp__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 True """ if self is other: return 0 if not isinstance(other, Inventory): return NotImplemented if self.id_set() ^ other.id_set(): return 1 for file_id in self._byid: c = cmp(self[file_id], other[file_id]) if c: return c return 0 def get_idpath(self, file_id): """Return a list of file_ids for the path to an entry. The list contains one element for each directory followed by the id of the file itself. So the length of the returned list is equal to the depth of the file in the tree, counting the root directory as depth 1. """ p = [] while file_id != None: try: ie = self._byid[file_id] except KeyError: bailout("file_id {%s} not found in inventory" % file_id) p.insert(0, ie.file_id) file_id = ie.parent_id return p def id2path(self, file_id): """Return as a list the path to file_id.""" # get all names, skipping root p = [self[fid].name for fid in self.get_idpath(file_id)[1:]] return '/'.join(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. Returns None iff the path is not found. """ if isinstance(name, types.StringTypes): name = splitpath(name) mutter("lookup path %r" % name) parent = self.root for f in name: try: cie = parent.children[f] assert cie.name == f assert cie.parent_id == parent.file_id parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): return self._byid.has_key(file_id) def rename(self, file_id, new_parent_id, new_name): """Move a file within the inventory. This can change either the name, or the parent, or both. This does not move the working file.""" if not is_valid_name(new_name): bailout("not an acceptable filename: %r" % new_name) new_parent = self._byid[new_parent_id] if new_name in new_parent.children: bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id))) new_parent_idpath = self.get_idpath(new_parent_id) if file_id in new_parent_idpath: bailout("cannot move directory %r into a subdirectory of itself, %r" % (self.id2path(file_id), self.id2path(new_parent_id))) file_ie = self._byid[file_id] old_parent = self._byid[file_ie.parent_id] # TODO: Don't leave things messed up if this fails del old_parent.children[file_ie.name] new_parent.children[new_name] = file_ie file_ie.name = new_name file_ie.parent_id = new_parent_id _NAME_RE = re.compile(r'^[^/\\]+$') def is_valid_name(name): return bool(_NAME_RE.match(name)) commit refs/heads/master mark :238 committer 1113187808 +1000 data 52 - Don't put profiling temp file in current directory from :237 M 644 inline NEWS data 2862 bzr-0.0.4 NOT RELEASED YET ENHANCEMENTS: * bzr diff optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. BUG FIXES: * Make commit safe for hardlinked bzr trees. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 30172 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, time, types, shutil, tempfile, traceback, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and new_name not in file_list: continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_dump_text_inventory(): import bzrlib.textinv inv = Branch('.').basis_tree().inventory bzrlib.textinv.write_text_inventory(inv, sys.stdout) def cmd_load_text_inventory(): import bzrlib.textinv inv = bzrlib.textinv.read_text_inventory(sys.stdin) print 'loaded %d entries' % len(inv) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot pfname = tempfile.mkstemp()[1] prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') traceback.print_exc(None, bzrlib.trace._tracefile) log_error('(see $HOME/.bzr.log for debug information)\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error('(see $HOME/.bzr.log for debug information)\n') traceback.print_exc(None, bzrlib.trace._tracefile) ## traceback.print_exc(None, sys.stderr) return 1 ## TODO: Trap AssertionError # TODO: Maybe nicer handling of IOError especially for broken pipe. if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :239 committer 1113188037 +1000 data 42 - remove profiler temporary file when done from :238 M 644 inline NEWS data 2892 bzr-0.0.4 NOT RELEASED YET ENHANCEMENTS: * bzr diff optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. BUG FIXES: * Make commit safe for hardlinked bzr trees. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 30327 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, time, types, shutil, tempfile, traceback, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and new_name not in file_list: continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_dump_text_inventory(): import bzrlib.textinv inv = Branch('.').basis_tree().inventory bzrlib.textinv.write_text_inventory(inv, sys.stdout) def cmd_load_text_inventory(): import bzrlib.textinv inv = bzrlib.textinv.read_text_inventory(sys.stdin) print 'loaded %d entries' % len(inv) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original'): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') traceback.print_exc(None, bzrlib.trace._tracefile) log_error('(see $HOME/.bzr.log for debug information)\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error('(see $HOME/.bzr.log for debug information)\n') traceback.print_exc(None, bzrlib.trace._tracefile) ## traceback.print_exc(None, sys.stderr) return 1 ## TODO: Trap AssertionError # TODO: Maybe nicer handling of IOError especially for broken pipe. if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :240 committer 1113201476 +1000 data 3 doc from :239 M 644 inline bzrlib/inventory.py data 19278 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # TODO: Maybe store inventory_id in the file? Not really needed. # This should really be an id randomly assigned when the tree is # created, but it's not for now. ROOT_ID = "TREE_ROOT" import sys, os.path, types, re from sets import Set try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from xml import XMLMixin from errors import bailout, BzrError import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') 'TREE_ROOT' >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID)) >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123')) >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' :todo: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ # TODO: split InventoryEntry into subclasses for files, # directories, etc etc. def __init__(self, file_id, name, kind, parent_id, text_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID) >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID) Traceback (most recent call last): BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", []) """ if len(splitpath(name)) != 1: bailout('InventoryEntry name is not a simple filename: %r' % name) self.file_id = file_id self.name = name self.kind = kind self.text_id = text_id self.parent_id = parent_id self.text_sha1 = None self.text_size = None if kind == 'directory': self.children = {} elif kind == 'file': pass else: raise BzrError("unhandled entry kind %r" % kind) def sorted_children(self): l = self.children.items() l.sort() return l def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.parent_id, text_id=self.text_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size != None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1']: v = getattr(self, f) if v != None: e.set(f, v) # to be conservative, we don't externalize the root pointers # for now, leaving them as null in the xml form. in a future # version it will be implied by nested elements. if self.parent_id != ROOT_ID: assert isinstance(self.parent_id, basestring) e.set('parent_id', self.parent_id) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' ## original format inventories don't have a parent_id for ## nodes in the root directory, but it's cleaner to use one ## internally. parent_id = elt.get('parent_id') if parent_id == None: parent_id = ROOT_ID self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __cmp__(self, other): if self is other: return 0 if not isinstance(other, InventoryEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.name, other.name) \ or cmp(self.text_sha1, other.text_sha1) \ or cmp(self.text_size, other.text_size) \ or cmp(self.text_id, other.text_id) \ or cmp(self.parent_id, other.parent_id) \ or cmp(self.kind, other.kind) class RootEntry(InventoryEntry): def __init__(self, file_id): self.file_id = file_id self.children = {} self.kind = 'root_directory' self.parent_id = None self.name = '' def __cmp__(self, other): if self is other: return 0 if not isinstance(other, RootEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.children, other.children) class Inventory(XMLMixin): """Inventory of versioned files in a tree. This describes which file_id is present at each point in the tree, and possibly the SHA-1 or other information about the file. Entries can be looked up either by path or by file_id. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted, other than through the Inventory API. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID)) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ ## TODO: Make sure only canonical filenames are stored. ## TODO: Do something sensible about the possible collisions on ## case-losing filesystems. Perhaps we should just always forbid ## such collisions. ## TODO: No special cases for root, rather just give it a file id ## like everything else. ## TODO: Probably change XML serialization to use nesting rather ## than parent_id pointers. ## TODO: Perhaps hold the ElementTree in memory and work directly ## on that rather than converting into Python objects every time? def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. The inventory is created with a default root directory, with an id of None. """ self.root = RootEntry(ROOT_ID) self._byid = {self.root.file_id: self.root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, from_dir=None): """Return (path, entry) pairs, in order by name.""" if from_dir == None: assert self.root from_dir = self.root elif isinstance(from_dir, basestring): from_dir = self._byid[from_dir] kids = from_dir.children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(from_dir=ie.file_id): yield '/'.join((name, cn)), cie def directories(self): """Return (path, entry) pairs for all directories. """ def descend(parent_ie): parent_name = parent_ie.name yield parent_name, parent_ie # directory children in sorted order dn = [] for ie in parent_ie.children.itervalues(): if ie.kind == 'directory': dn.append((ie.name, ie)) dn.sort() for name, child_ie in dn: for sub_name, sub_ie in descend(child_ie): yield appendpath(parent_name, sub_name), sub_ie for name, ie in descend(self.root): yield name, ie def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID)) >>> inv['123123'].name 'hello.c' """ if file_id == None: raise BzrError("can't look up file_id None") try: return self._byid[file_id] except KeyError: raise BzrError("file_id {%s} not in inventory" % file_id) def get_child(self, parent_id, filename): return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self._byid: bailout("inventory already contains entry with id {%s}" % entry.file_id) try: parent = self._byid[entry.parent_id] except KeyError: bailout("parent_id {%s} not in inventory" % entry.parent_id) if parent.children.has_key(entry.name): bailout("%s is already versioned" % appendpath(self.id2path(parent.file_id), entry.name)) self._byid[entry.file_id] = entry parent.children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: bailout("cannot re-add root of inventory") if file_id == None: file_id = bzrlib.branch.gen_file_id(relpath) parent_id = self.path2id(parts[:-1]) assert parent_id != None ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def id_set(self): return Set(self._byid) def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID)) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __cmp__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 True """ if self is other: return 0 if not isinstance(other, Inventory): return NotImplemented if self.id_set() ^ other.id_set(): return 1 for file_id in self._byid: c = cmp(self[file_id], other[file_id]) if c: return c return 0 def get_idpath(self, file_id): """Return a list of file_ids for the path to an entry. The list contains one element for each directory followed by the id of the file itself. So the length of the returned list is equal to the depth of the file in the tree, counting the root directory as depth 1. """ p = [] while file_id != None: try: ie = self._byid[file_id] except KeyError: bailout("file_id {%s} not found in inventory" % file_id) p.insert(0, ie.file_id) file_id = ie.parent_id return p def id2path(self, file_id): """Return as a list the path to file_id.""" # get all names, skipping root p = [self[fid].name for fid in self.get_idpath(file_id)[1:]] return '/'.join(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. Returns None iff the path is not found. """ if isinstance(name, types.StringTypes): name = splitpath(name) mutter("lookup path %r" % name) parent = self.root for f in name: try: cie = parent.children[f] assert cie.name == f assert cie.parent_id == parent.file_id parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): return self._byid.has_key(file_id) def rename(self, file_id, new_parent_id, new_name): """Move a file within the inventory. This can change either the name, or the parent, or both. This does not move the working file.""" if not is_valid_name(new_name): bailout("not an acceptable filename: %r" % new_name) new_parent = self._byid[new_parent_id] if new_name in new_parent.children: bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id))) new_parent_idpath = self.get_idpath(new_parent_id) if file_id in new_parent_idpath: bailout("cannot move directory %r into a subdirectory of itself, %r" % (self.id2path(file_id), self.id2path(new_parent_id))) file_ie = self._byid[file_id] old_parent = self._byid[file_ie.parent_id] # TODO: Don't leave things messed up if this fails del old_parent.children[file_ie.name] new_parent.children[new_name] = file_ie file_ie.name = new_name file_ie.parent_id = new_parent_id _NAME_RE = re.compile(r'^[^/\\]+$') def is_valid_name(name): return bool(_NAME_RE.match(name)) commit refs/heads/master mark :241 committer 1113286820 +1000 data 26 - Add more ignore patterns from :240 M 644 inline bzrlib/__init__.py data 1528 # (C) 2005 Canonical Development Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr library""" from inventory import Inventory, InventoryEntry from branch import Branch, ScratchBranch from osutils import format_date from tree import Tree from diff import diff_trees from trace import mutter, warning import add BZRDIR = ".bzr" DEFAULT_IGNORE = ['.bzr.log', '*~', '#*#', '*$', '.#*', '*.tmp', '*.bak', '*.BAK', '*.orig', '*.o', '*.obj', '*.a', '*.py[oc]', '*.so', '*.exe', '*.elc', '{arch}', 'CVS', '.svn', '_darcs', 'SCCS', 'RCS', 'BitKeeper', 'TAGS', '.make.state', '.sconsign', '.tmp*'] IGNORE_FILENAME = ".bzrignore" __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __version__ = '0.0.4pre' M 644 inline doc/random.txt data 9557 I think Ruby's point is right: we need to think about how a tool *feels* as you're using it. Making regular commits gives a nice rhythm to to working; in some ways it's nicer to just commit single files with C-x v v than to build complex changesets. (See gmane.c.v-c.arch.devel post 19 Nov, Tom Lord.) * Would like to generate an activity report, to e.g. mail to your boss or post to your blog. "What did I change today, across all these specified branches?" * It is possibly nice that tla by default forbids you from committing if emacs autosave or lock files exist -- I find it confusing to commit somethin other than what is shown in the editor window because there are unsaved changes. However, grumbling about unknown files is annoying, and requiring people to edit regexps in the id-tagging-method file to fix it is totally unreasonable. Perhaps there should be a preference to abort on unknown files, or perhaps it should be possible to specify forbidden files. Perhaps this is related to a mechanism to detect conflicted files: should refuse to commit if there are any .rej files lying around. *Those who lose history are doomed to recreate it.* -- broked (on #gnu.arch.users) *A universal convention supplies all of maintainability, clarity, consistency, and a foundation for good programming habits too. What it doesn't do is insist that you follow it against your will. That's Python!* -- Tim Peters on comp.lang.python, 2001-06-16 (Bazaar provides mechanism and convention, but it is up to you whether you wish to follow or enforce that convention.) ---- jblack asks for A way to subtract merges, so that you can see the work you've done to a branch since conception. ---- :: now that is a neat idea: advertise branches over zeroconf should make lca fun :-) ---- http://thedailywtf.com/ShowPost.aspx?PostID=24281 Source control is necessary and useful, but in a team of one (or even two) people the setup overhead isn't always worth it--especially if you're going to join source control in a month, and you don't want to have to migrate everything out of your existing (in my case, skunkworks) system before you can use it. At least that was my experience--I putzed with CVS a bit and knew other source control systems pretty well, but in the day-to-day it wasn't worth the bother (granted, I was a bit offended at having to wait to use the mainline source control, but that's another matter). I think Bazaar-NG will have such low setup overhead (just ``init``, ``add``) that it can be easily used for even tiny projects. The ability to merge previously-unrelated trees means they can fold their project in later. ---- From tridge: * cope without $EMAIL better * notes at start of .bzr.log: * you can delete this * or include it in bug reports * should you be able to remove things from the default ignore list? * headers at start of diff, giving some comments, perhaps dates * is diff against /dev/null really OK? I think so. * separate remove/delete commands? * detect files which were removed and now in 'missing' state * should we actually compare files for 'status', or check mtime and size; reading every file in the samba source tree can take a long time. without this, doing a status on a large tree can be very slow. but relying on mtime/size is a bit dangerous. people really do work on trees which take a large chunk of memory and which will not stay in memory * status up-to-date files: not 'U', and don't list without --all * if status does compare file text, then it should be quick when checking just a single file * wrapper for svn that every time run logs - command - all inputs - time it took - sufficient to replay everything - record all files * status crashes if a file is missing * option for -p1 level on diff, etc. perhaps * commit without message should start $EDITOR * don't duplicate all files on commit * start importing tridge-junkcode * perhaps need xdelta storage sooner rather than later, to handle very large file ---- The first operation most people do with a new version-control system is *not* making their own project, but rather getting a checkout of an existing project, building it, and possibly submitting a patch. So those operations should be *extremely* easy. ---- * Way to check that a branch is fully merged, and no longer needed: should mean all its changes have been integrated upstream, no uncommitted changes or rejects or unknown files. * Filter revisions by containing a particular word (as for log). Perhaps have key-value fields that might be used for e.g. line-of-development or bug nr? * List difference in the revisions on one branch vs another. * Perhaps use a partially-readable but still hopefully unique ID for revisions/inventories? * Preview what will happen in a merge before it is applied * When a changeset deletes a file, should have the option to just make it unknown/ignored. Perhaps this is best handled by an interactive merge. If the file is unchanged locally and deleted remotely, it will by default be deleted (but the user has the option to reject the delete, or to make it just unversioned, or to save a copy.) If it is modified locall then the user still needs to choose between those options but there is no default (or perhaps the default is to reject the delete.) * interactive commit, prompting whether each hunk should be sent (as for darcs) * Write up something about detection of unmodified files * Preview a merge so as to get some idea what will happen: * What revisions will be merged (log entries, etc) * What files will be affected? * Are those simple updates, or have they been updated locally as well. * Any renames or metadata clashes? * Show diffs or conflict markers. * Do the merge, but write into a second directory. * "Show me all changesets that touch this file" Can be done by walking back through all revisions, and filtering out those where the file-id either gets a new name or a new text. * Way to commit backdated revisions or pretend to be something by someone else, for the benefit of import tools; in general allow everything taken from the current environment to be overridden. * Cope well when trying to checkout or update over a flaky connection. Passive HTTP possibly helps with this: we can fetch all the file texts first, then the inventory, and can even retry interrupted connections. * Use readline for reading log messages, and store a history of previous commit messages! * Warn when adding huge files(?) - more than say 10MB? On the other hand, why not just cope? * Perhaps allow people to specify a revision-id, much as people have unique but human-assigned names for patches at the moment? ---- 20050218090900.GA2071@opteron.random Subject: Re: [darcs-users] Re: [BK] upgrade will be needed From: Andrea Arcangeli Newsgroups: gmane.linux.kernel Date: Fri, 18 Feb 2005 10:09:00 +0100 On Thu, Feb 17, 2005 at 06:24:53PM -0800, Tupshin Harper wrote: > small to medium sized ones). Last I checked, Arch was still too slow in > some areas, though that might have changed in recent months. Also, many IMHO someone needs to rewrite ARCH using the RCS or SCCS format for the backend and a single file for the changesets and with sane parameters conventions miming SVN. The internal algorithms of arch seems the most advanced possible. It's just the interface and the fs backend that's so bad and doesn't compress in the backups either. SVN bsddb doesn't compress either by default, but at least the new fsfs compresses pretty well, not as good as CVS, but not as badly as bsddb and arch either. I may be completely wrong, so take the above just as a humble suggestion. darcs scares me a bit because it's in haskell, I don't believe very much in functional languages for compute intensive stuff, ram utilization skyrockets sometime (I wouldn't like to need >1G of ram to manage the tree). Other languages like python or perl are much slower than C/C++ too but at least ram utilization can be normally dominated to sane levels with them and they can be greatly optimized easily with C/C++ extensions of the performance critical parts. ----- * Fix up diffs for files without a trailing newline ----- * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. ---- When sending patches by email (optionall) send each one as a separate mail, with a sequence number [%03d/%03d] at the start of the Subject. See mail from linus 2005-04-07. http://linux.yyz.us/patch-format.html http://www.zip.com.au/~akpm/linux/patches/stuff/tpp.txt ---- dwmw2 (2005-04-07) reorder patches by cherry-picking them from a main development tree before sending them on. ---- ignore BitKeeper re-add should give the same id as before commit refs/heads/master mark :242 committer 1113287252 +1000 data 36 Fix opening of ~/.bzr.log on Windows from :241 M 644 inline NEWS data 3020 bzr-0.0.4 NOT RELEASED YET ENHANCEMENTS: * bzr diff optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. BUG FIXES: * Make commit safe for hardlinked bzr trees. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/trace.py data 3712 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os, time, socket, stat, codecs import bzrlib ###################################################################### # messages and logging ## TODO: If --verbose is given then write to both stderr and ## _tracefile; perhaps replace _tracefile with a tee thing. global _tracefile, _starttime _tracefile = None # used to have % (os.environ['USER'], time.time(), os.getpid()), 'w') _starttime = None # If false, notes also go to stdout; should replace this with --silent # at some point. silent = False # TODO: Somehow tie this to the --verbose option? verbose = False def warning(msg): b = 'bzr: warning: ' + msg + '\n' sys.stderr.write(b) _tracefile.write(b) #_tracefile.flush() def mutter(msg): _tracefile.write(msg) _tracefile.write('\n') # _tracefile.flush() if verbose: sys.stderr.write('- ' + msg + '\n') def note(msg): b = '* ' + str(msg) + '\n' if not silent: sys.stderr.write(b) _tracefile.write(b) # _tracefile.flush() def log_error(msg): sys.stderr.write(msg) _tracefile.write(msg) # _tracefile.flush() def create_tracefile(argv): # TODO: Also show contents of /etc/lsb-release, if it can be parsed. # Perhaps that should eventually go into the platform library? # TODO: If the file doesn't exist, add a note describing it. # Messages are always written to here, so that we have some # information if something goes wrong. In a future version this # file will be removed on successful completion. global _starttime, _tracefile _starttime = os.times()[4] # XXX: Does this always work on Windows? trace_fname = os.path.join(os.path.expanduser('~/.bzr.log')) _tracefile = codecs.open(trace_fname, 'at', 'utf8') t = _tracefile if os.fstat(t.fileno())[stat.ST_SIZE] == 0: t.write("\nthis is a debug log for diagnosing/reporting problems in bzr\n") t.write("you can delete or truncate this file, or include sections in\n") t.write("bug reports to bazaar-ng@lists.canonical.com\n\n") # TODO: If we failed to create the file, perhaps give a warning # but don't abort; send things to /dev/null instead? t.write('-' * 60 + '\n') t.write('bzr invoked at %s\n' % bzrlib.osutils.format_date(time.time())) t.write(' version: %s\n' % bzrlib.__version__) t.write(' by %s on %s\n' % (bzrlib.osutils.username(), socket.getfqdn())) t.write(' arguments: %r\n' % argv) t.write(' working dir: %s\n' % os.getcwdu()) t.write(' platform: %s\n' % sys.platform) t.write(' python: %s\n' % (sys.version_info,)) import atexit atexit.register(_close_trace) def _close_trace(): times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum, %.3f elapsed" % (times[:4] + ((times[4] - _starttime),))) commit refs/heads/master mark :243 committer 1113356937 +1000 data 22 ignore more arch poopy from :242 M 644 inline .bzrignore data 98 ./doc/*.html *.py[oc] *~ .arch-ids .bzr.profile .arch-inventory {arch} CHANGELOG bzr-test.log ,,* commit refs/heads/master mark :244 committer 1113358390 +1000 data 45 - New 'bzr log --verbose' from Sebastian Cote from :243 M 644 inline NEWS data 3135 bzr-0.0.4 NOT RELEASED YET ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. BUG FIXES: * Make commit safe for hardlinked bzr trees. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/branch.py data 33915 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. :todo: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. :todo: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. :todo: Keep the on-disk branch locked while the object exists. :todo: mkdir() method. """ def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. :param base: Base directory for the branch. :param init: If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. :param find_root: If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch""" return file(self.controlfilename(file_or_path), mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'wb').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'rb').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() inv = Inventory.read_xml(self.controlfile('inventory', 'r')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'w') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. This puts the files in the Added state, so that they will be recorded by the next commit. :todo: Perhaps have an option to add the ids even if the files do not (yet) exist. :todo: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. :todo: Option to specify file id. :todo: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on :todo: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True :todo: Do something useful with directories. :todo: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. :param timestamp: if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) self.append_revision(rev_id) if verbose: note("commited r%d" % self.revno()) def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. :todo: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] else: return None def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original', verbose=False): """Write out human-readable log of commits to this branch :param utc: If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l if verbose == True and precursor != None: print 'changed files:' tree = self.revision_tree(p) prevtree = self.revision_tree(precursor) for file_state, fid, old_name, new_name, kind in \ diff_trees(prevtree, tree, ): if file_state == 'A' or file_state == 'M': show_status(file_state, kind, new_name) elif file_state == 'D': show_status(file_state, kind, old_name) elif file_state == 'R': show_status(file_state, kind, old_name + ' => ' + new_name) revno += 1 precursor = p def rename_one(self, from_rel, to_rel): tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(self, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo :todo: Get state for single files. :todo: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = self.basis_tree() new = self.working_tree() for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("wierd file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" try: shutil.rmtree(self.base) except OSError: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/commands.py data 30365 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, time, types, shutil, tempfile, traceback, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and new_name not in file_list: continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_dump_text_inventory(): import bzrlib.textinv inv = Branch('.').basis_tree().inventory bzrlib.textinv.write_text_inventory(inv, sys.stdout) def cmd_load_text_inventory(): import bzrlib.textinv inv = bzrlib.textinv.read_text_inventory(sys.stdin) print 'loaded %d entries' % len(inv) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original', verbose=False): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone, verbose=verbose) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone', 'verbose'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') traceback.print_exc(None, bzrlib.trace._tracefile) log_error('(see $HOME/.bzr.log for debug information)\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error('(see $HOME/.bzr.log for debug information)\n') traceback.print_exc(None, bzrlib.trace._tracefile) ## traceback.print_exc(None, sys.stderr) return 1 ## TODO: Trap AssertionError # TODO: Maybe nicer handling of IOError especially for broken pipe. if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :245 committer 1113365220 +1000 data 100 - control files always in utf-8-unix format - command line arguments interpreted as locale encoding from :244 M 644 inline NEWS data 3169 bzr-0.0.4 NOT RELEASED YET ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/branch.py data 34628 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. :todo: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. :todo: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. :todo: Keep the on-disk branch locked while the object exists. :todo: mkdir() method. """ def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. :param base: Base directory for the branch. :param init: If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. :param find_root: If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation import codecs return codecs.open(fn, mode + 'b', 'utf-8') else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. This puts the files in the Added state, so that they will be recorded by the next commit. :todo: Perhaps have an option to add the ids even if the files do not (yet) exist. :todo: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. :todo: Option to specify file id. :todo: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on :todo: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True :todo: Do something useful with directories. :todo: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. :param timestamp: if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) self.append_revision(rev_id) if verbose: note("commited r%d" % self.revno()) def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. :todo: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history', 'r').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] else: return None def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original', verbose=False): """Write out human-readable log of commits to this branch :param utc: If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l if verbose == True and precursor != None: print 'changed files:' tree = self.revision_tree(p) prevtree = self.revision_tree(precursor) for file_state, fid, old_name, new_name, kind in \ diff_trees(prevtree, tree, ): if file_state == 'A' or file_state == 'M': show_status(file_state, kind, new_name) elif file_state == 'D': show_status(file_state, kind, old_name) elif file_state == 'R': show_status(file_state, kind, old_name + ' => ' + new_name) revno += 1 precursor = p def rename_one(self, from_rel, to_rel): tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(self, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo :todo: Get state for single files. :todo: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = self.basis_tree() new = self.working_tree() for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("wierd file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" try: shutil.rmtree(self.base) except OSError: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/commands.py data 30472 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, time, types, shutil, tempfile, traceback, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. :todo: Don't show unchanged files unless ``--all`` is given? """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and new_name not in file_list: continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_dump_text_inventory(): import bzrlib.textinv inv = Branch('.').basis_tree().inventory bzrlib.textinv.write_text_inventory(inv, sys.stdout) def cmd_load_text_inventory(): import bzrlib.textinv inv = bzrlib.textinv.read_text_inventory(sys.stdin) print 'loaded %d entries' % len(inv) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original', verbose=False): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone, verbose=verbose) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone', 'verbose'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ import locale enc = locale.getpreferredencoding() argv = [a.decode(enc) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') traceback.print_exc(None, bzrlib.trace._tracefile) log_error('(see $HOME/.bzr.log for debug information)\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error('(see $HOME/.bzr.log for debug information)\n') traceback.print_exc(None, bzrlib.trace._tracefile) ## traceback.print_exc(None, sys.stderr) return 1 ## TODO: Trap AssertionError # TODO: Maybe nicer handling of IOError especially for broken pipe. if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :246 committer 1113367403 +1000 data 54 - unicode decoding in getting email and userid strings from :245 M 644 inline bzrlib/osutils.py data 7924 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, types, re, time from stat import S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE from errors import bailout, BzrError from trace import mutter def make_readonly(filename): """Make a filename read-only.""" # TODO: probably needs to be fixed for windows mod = os.stat(filename).st_mode mod = mod & 0777555 os.chmod(filename, mod) def make_writable(filename): mod = os.stat(filename).st_mode mod = mod | 0200 os.chmod(filename, mod) _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])') def quotefn(f): """Return shell-quoted filename""" ## We could be a bit more terse by using double-quotes etc f = _QUOTE_RE.sub(r'\\\1', f) if f[0] == '~': f[0:1] = r'\~' return f def file_kind(f): mode = os.lstat(f)[ST_MODE] if S_ISREG(mode): return 'file' elif S_ISDIR(mode): return 'directory' elif S_ISLNK(mode): return 'symlink' else: raise BzrError("can't handle file kind with mode %o of %r" % (mode, f)) def isdir(f): """True if f is an accessible directory.""" try: return S_ISDIR(os.lstat(f)[ST_MODE]) except OSError: return False def isfile(f): """True if f is a regular file.""" try: return S_ISREG(os.lstat(f)[ST_MODE]) except OSError: return False def pumpfile(fromfile, tofile): """Copy contents of one file to another.""" tofile.write(fromfile.read()) def uuid(): """Return a new UUID""" ## XXX: Could alternatively read /proc/sys/kernel/random/uuid on ## Linux, but we need something portable for other systems; ## preferably an implementation in Python. try: return chomp(file('/proc/sys/kernel/random/uuid').readline()) except IOError: return chomp(os.popen('uuidgen').readline()) def chomp(s): if s and (s[-1] == '\n'): return s[:-1] else: return s def sha_file(f): import sha ## TODO: Maybe read in chunks to handle big files if hasattr(f, 'tell'): assert f.tell() == 0 s = sha.new() s.update(f.read()) return s.hexdigest() def sha_string(f): import sha s = sha.new() s.update(f) return s.hexdigest() def fingerprint_file(f): import sha s = sha.new() b = f.read() s.update(b) size = len(b) return {'size': size, 'sha1': s.hexdigest()} def auto_user_id(): """Calculate automatic user identification. Returns (realname, email). Only used when none is set in the environment. """ import socket, locale # XXX: Any good way to get real user name on win32? # XXX: can the FQDN be non-ascii? enc = locale.getpreferredencoding() try: import pwd uid = os.getuid() w = pwd.getpwuid(uid) gecos = w.pw_gecos.decode(enc) username = w.pw_name.decode(enc) comma = gecos.find(',') if comma == -1: realname = gecos else: realname = gecos[:comma] return realname, (username + '@' + socket.getfqdn()) except ImportError: import getpass return '', (getpass.getuser().decode(enc) + '@' + socket.getfqdn()) def username(): """Return email-style username. Something similar to 'Martin Pool ' :todo: Check it's reasonably well-formed. :todo: Allow taking it from a dotfile to help people on windows who can't easily set variables. """ import locale e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: return e.decode(locale.getpreferredencoding()) name, email = auto_user_id() if name: return '%s <%s>' % (name, email) else: return email _EMAIL_RE = re.compile(r'[\w+.-]+@[\w+.-]+') def user_email(): """Return just the email component of a username.""" e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') import locale e = e.decode(locale.getpreferredencoding()) if e: m = _EMAIL_RE.search(e) if not m: bailout('%r is not a reasonable email address' % e) return m.group(0) return auto_user_id()[1] def compare_files(a, b): """Returns true if equal in contents""" # TODO: don't read the whole thing in one go. BUFSIZE = 4096 while True: ai = a.read(BUFSIZE) bi = b.read(BUFSIZE) if ai != bi: return False if ai == '': return True def local_time_offset(t=None): """Return offset of local zone from GMT, either at present or at time t.""" # python2.3 localtime() can't take None if t == None: t = time.time() if time.localtime(t).tm_isdst and time.daylight: return -time.altzone else: return -time.timezone def format_date(t, offset=0, timezone='original'): ## TODO: Perhaps a global option to use either universal or local time? ## Or perhaps just let people set $TZ? assert isinstance(t, float) if timezone == 'utc': tt = time.gmtime(t) offset = 0 elif timezone == 'original': if offset == None: offset = 0 tt = time.gmtime(t + offset) elif timezone == 'local': tt = time.localtime(t) offset = local_time_offset(t) else: bailout("unsupported timezone format %r", ['options are "utc", "original", "local"']) return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt) + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)) def compact_date(when): return time.strftime('%Y%m%d%H%M%S', time.gmtime(when)) def filesize(f): """Return size of given open file.""" return os.fstat(f.fileno())[ST_SIZE] if hasattr(os, 'urandom'): # python 2.4 and later rand_bytes = os.urandom else: # FIXME: No good on non-Linux _rand_file = file('/dev/urandom', 'rb') rand_bytes = _rand_file.read ## TODO: We could later have path objects that remember their list ## decomposition (might be too tricksy though.) def splitpath(p): """Turn string into list of parts. >>> splitpath('a') ['a'] >>> splitpath('a/b') ['a', 'b'] >>> splitpath('a/./b') ['a', 'b'] >>> splitpath('a/.b') ['a', '.b'] >>> splitpath('a/../b') Traceback (most recent call last): ... BzrError: ("sorry, '..' not allowed in path", []) """ assert isinstance(p, types.StringTypes) ps = [f for f in p.split('/') if (f != '.' and f != '')] for f in ps: if f == '..': bailout("sorry, %r not allowed in path" % f) return ps def joinpath(p): assert isinstance(p, list) for f in p: if (f == '..') or (f == None) or (f == ''): bailout("sorry, %r not allowed in path" % f) return '/'.join(p) def appendpath(p1, p2): if p1 == '': return p2 else: return p1 + '/' + p2 def extern_command(cmd, ignore_errors = False): mutter('external command: %s' % `cmd`) if os.system(cmd): if not ignore_errors: bailout('command failed') commit refs/heads/master mark :247 committer 1113367955 +1000 data 3 doc from :246 M 644 inline bzrlib/branch.py data 34698 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. :todo: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. :todo: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. :todo: Keep the on-disk branch locked while the object exists. :todo: mkdir() method. """ def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. :param base: Base directory for the branch. :param init: If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. :param find_root: If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation import codecs return codecs.open(fn, mode + 'b', 'utf-8') else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. :todo: Perhaps have an option to add the ids even if the files do not (yet) exist. :todo: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. :todo: Option to specify file id. :todo: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on :todo: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True :todo: Do something useful with directories. :todo: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. :param timestamp: if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) self.append_revision(rev_id) if verbose: note("commited r%d" % self.revno()) def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. :todo: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history', 'r').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] else: return None def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original', verbose=False): """Write out human-readable log of commits to this branch :param utc: If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l if verbose == True and precursor != None: print 'changed files:' tree = self.revision_tree(p) prevtree = self.revision_tree(precursor) for file_state, fid, old_name, new_name, kind in \ diff_trees(prevtree, tree, ): if file_state == 'A' or file_state == 'M': show_status(file_state, kind, new_name) elif file_state == 'D': show_status(file_state, kind, old_name) elif file_state == 'R': show_status(file_state, kind, old_name + ' => ' + new_name) revno += 1 precursor = p def rename_one(self, from_rel, to_rel): tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(self, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo :todo: Get state for single files. :todo: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = self.basis_tree() new = self.working_tree() for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("wierd file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" try: shutil.rmtree(self.base) except OSError: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :248 committer 1113368205 +1000 data 61 - Better progress and completion indicator from check command from :247 M 644 inline bzrlib/check.py data 4297 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ###################################################################### # consistency checks import sys from sets import Set from trace import mutter from errors import bailout import osutils def check(branch, progress=True): out = sys.stdout # TODO: factor out if not (hasattr(out, 'isatty') and out.isatty()): progress=False if progress: def p(m): mutter('checking ' + m) out.write('\rchecking: %-50.50s' % m) out.flush() else: def p(m): mutter('checking ' + m) p('history of %r' % branch.base) last_ptr = None checked_revs = Set() history = branch.revision_history() revno = 0 revcount = len(history) checked_texts = {} for rid in history: revno += 1 p('revision %d/%d' % (revno, revcount)) mutter(' revision {%s}' % rid) rev = branch.get_revision(rid) if rev.revision_id != rid: bailout('wrong internal revision id in revision {%s}' % rid) if rev.precursor != last_ptr: bailout('mismatched precursor in revision {%s}' % rid) last_ptr = rid if rid in checked_revs: bailout('repeated revision {%s}' % rid) checked_revs.add(rid) ## TODO: Check all the required fields are present on the revision. inv = branch.get_inventory(rev.inventory_id) seen_ids = Set() seen_names = Set() p('revision %d/%d file ids' % (revno, revcount)) for file_id in inv: if file_id in seen_ids: bailout('duplicated file_id {%s} in inventory for revision {%s}' % (file_id, rid)) seen_ids.add(file_id) i = 0 len_inv = len(inv) for file_id in inv: i += 1 if (i % 100) == 0: p('revision %d/%d file text %d/%d' % (revno, revcount, i, len_inv)) ie = inv[file_id] if ie.parent_id != None: if ie.parent_id not in seen_ids: bailout('missing parent {%s} in inventory for revision {%s}' % (ie.parent_id, rid)) if ie.kind == 'file': if ie.text_id in checked_texts: fp = checked_texts[ie.text_id] else: if not ie.text_id in branch.text_store: bailout('text {%s} not in text_store' % ie.text_id) tf = branch.text_store[ie.text_id] fp = osutils.fingerprint_file(tf) checked_texts[ie.text_id] = fp if ie.text_size != fp['size']: bailout('text {%s} wrong size' % ie.text_id) if ie.text_sha1 != fp['sha1']: bailout('text {%s} wrong sha1' % ie.text_id) elif ie.kind == 'directory': if ie.text_sha1 != None or ie.text_size != None or ie.text_id != None: bailout('directory {%s} has text in revision {%s}' % (file_id, rid)) p('revision %d/%d file paths' % (revno, revcount)) for path, ie in inv.iter_entries(): if path in seen_names: bailout('duplicated path %r in inventory for revision {%s}' % (path, revid)) seen_names.add(path) p('done') if progress: print print 'checked %d revisions, %d file texts' % (revcount, len(checked_texts)) commit refs/heads/master mark :249 committer 1113368456 +1000 data 31 - Check repository from test.sh from :248 M 644 inline test.sh data 2368 #! /bin/sh -pe # Simple shell-based tests for bzr. # This is meant to exercise the external behaviour, command line # parsing and similar things and compliment the inwardly-turned # testing done by doctest. # This must already exist and be in the right place if ! [ -d bzr-test.tmp ] then echo "please create directory bzr-test.tmp" exit 1 fi echo "testing `which bzr`" bzr --version | head -n 1 echo rm -rf bzr-test.tmp mkdir bzr-test.tmp # save it for real errors exec 3>&2 exec > bzr-test.log exec 2>&1 set -x quitter() { echo "tests failed, look in bzr-test.log" >&3; exit 2; } trap quitter ERR cd bzr-test.tmp rm -rf .bzr mkdir branch1 cd branch1 # some information commands bzr help bzr version # invalid commands are detected ! bzr pants # some experiments with renames bzr init echo "hello world" > test.txt bzr unknowns # should be the only unknown file [ "`bzr unknowns`" = test.txt ] # can't rename unversioned files; use the regular unix rename command ! bzr rename test.txt new-test.txt # ok, so now add it and see what happens bzr add test.txt [ -z "`bzr unknowns`" ] # after adding even before committing you can rename files bzr rename test.txt newname.txt [ "`bzr status`" = "A newname.txt" ] [ `bzr revno` = 0 ] bzr commit -m "add first revision" [ `bzr revno` = 1 ] # now more complicated renames mkdir sub1 ! bzr rename newname.txt sub1 ! bzr rename newname.txt sub1/foo.txt bzr add sub1 ! bzr rename newname.txt sub1 bzr rename newname.txt sub1/foo.txt [ -f sub1/foo.txt ] [ ! -f newname.txt ] bzr rename sub1/foo.txt newname.txt [ -f newname.txt ] bzr rename newname.txt sub1/foo.txt bzr rename sub1/foo.txt sub1/bar.txt cd sub1 mkdir sub2 bzr add sub2 bzr rename bar.txt sub2/bar.txt cd sub2 bzr rename bar.txt ../../bar.txt cd ../../ bzr commit -m "more renames" [ `bzr revno` = 2 ] # now try pulling that file back out, checking it was stored properly [ "`bzr cat -r 1 newname.txt`" = "hello world" ] ! bzr rename sub1 sub1/knotted-up # now test hardlinked branches in subdirectories cd .. [ -d branch2 ] && rm -rf branch2 cp -al branch1 branch2 cd branch2 bzr log [ `bzr revno` = 2 ] echo "added in branch2" > new-in-2.txt bzr add new-in-2.txt bzr commit -m "add file to branch 2 only" [ `bzr revno` = 3 ] cd ../branch1 [ `bzr revno` = 2 ] bzr check echo "tests completed ok" >&3 commit refs/heads/master mark :250 committer 1113463895 +1000 data 40 Fix unicode bug in command line handler. from :249 M 644 inline bzrlib/commands.py data 30411 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, time, types, shutil, tempfile, traceback, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and new_name not in file_list: continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_dump_text_inventory(): import bzrlib.textinv inv = Branch('.').basis_tree().inventory bzrlib.textinv.write_text_inventory(inv, sys.stdout) def cmd_load_text_inventory(): import bzrlib.textinv inv = bzrlib.textinv.read_text_inventory(sys.stdin) print 'loaded %d entries' % len(inv) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original', verbose=False): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone, verbose=verbose) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone', 'verbose'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ import locale enc = locale.getpreferredencoding() argv = [a.decode(enc) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[str(k.replace('-', '_'))] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') traceback.print_exc(None, bzrlib.trace._tracefile) log_error('(see $HOME/.bzr.log for debug information)\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error('(see $HOME/.bzr.log for debug information)\n') traceback.print_exc(None, bzrlib.trace._tracefile) ## traceback.print_exc(None, sys.stderr) return 1 ## TODO: Trap AssertionError # TODO: Maybe nicer handling of IOError especially for broken pipe. if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :251 committer 1113522181 +1000 data 86 - factor out locale.getpreferredencoding() - fix problems with EMAIL not being defined from :250 M 644 inline bzrlib/__init__.py data 1588 # (C) 2005 Canonical Development Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr library""" from inventory import Inventory, InventoryEntry from branch import Branch, ScratchBranch from osutils import format_date from tree import Tree from diff import diff_trees from trace import mutter, warning import add BZRDIR = ".bzr" DEFAULT_IGNORE = ['.bzr.log', '*~', '#*#', '*$', '.#*', '*.tmp', '*.bak', '*.BAK', '*.orig', '*.o', '*.obj', '*.a', '*.py[oc]', '*.so', '*.exe', '*.elc', '{arch}', 'CVS', '.svn', '_darcs', 'SCCS', 'RCS', 'BitKeeper', 'TAGS', '.make.state', '.sconsign', '.tmp*'] IGNORE_FILENAME = ".bzrignore" import locale user_encoding = locale.getpreferredencoding() __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __version__ = '0.0.4pre' M 644 inline bzrlib/commands.py data 30370 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, time, types, shutil, tempfile, traceback, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and new_name not in file_list: continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_dump_text_inventory(): import bzrlib.textinv inv = Branch('.').basis_tree().inventory bzrlib.textinv.write_text_inventory(inv, sys.stdout) def cmd_load_text_inventory(): import bzrlib.textinv inv = bzrlib.textinv.read_text_inventory(sys.stdin) print 'loaded %d entries' % len(inv) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original', verbose=False): """Show log of this branch. :todo: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone, verbose=verbose) def cmd_ls(revision=None, verbose=False): """List files in a tree. :todo: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone', 'verbose'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[str(k.replace('-', '_'))] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') traceback.print_exc(None, bzrlib.trace._tracefile) log_error('(see $HOME/.bzr.log for debug information)\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error('(see $HOME/.bzr.log for debug information)\n') traceback.print_exc(None, bzrlib.trace._tracefile) ## traceback.print_exc(None, sys.stderr) return 1 ## TODO: Trap AssertionError # TODO: Maybe nicer handling of IOError especially for broken pipe. if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') M 644 inline bzrlib/osutils.py data 7912 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, types, re, time from stat import S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE from errors import bailout, BzrError from trace import mutter import bzrlib def make_readonly(filename): """Make a filename read-only.""" # TODO: probably needs to be fixed for windows mod = os.stat(filename).st_mode mod = mod & 0777555 os.chmod(filename, mod) def make_writable(filename): mod = os.stat(filename).st_mode mod = mod | 0200 os.chmod(filename, mod) _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])') def quotefn(f): """Return shell-quoted filename""" ## We could be a bit more terse by using double-quotes etc f = _QUOTE_RE.sub(r'\\\1', f) if f[0] == '~': f[0:1] = r'\~' return f def file_kind(f): mode = os.lstat(f)[ST_MODE] if S_ISREG(mode): return 'file' elif S_ISDIR(mode): return 'directory' elif S_ISLNK(mode): return 'symlink' else: raise BzrError("can't handle file kind with mode %o of %r" % (mode, f)) def isdir(f): """True if f is an accessible directory.""" try: return S_ISDIR(os.lstat(f)[ST_MODE]) except OSError: return False def isfile(f): """True if f is a regular file.""" try: return S_ISREG(os.lstat(f)[ST_MODE]) except OSError: return False def pumpfile(fromfile, tofile): """Copy contents of one file to another.""" tofile.write(fromfile.read()) def uuid(): """Return a new UUID""" ## XXX: Could alternatively read /proc/sys/kernel/random/uuid on ## Linux, but we need something portable for other systems; ## preferably an implementation in Python. try: return chomp(file('/proc/sys/kernel/random/uuid').readline()) except IOError: return chomp(os.popen('uuidgen').readline()) def chomp(s): if s and (s[-1] == '\n'): return s[:-1] else: return s def sha_file(f): import sha ## TODO: Maybe read in chunks to handle big files if hasattr(f, 'tell'): assert f.tell() == 0 s = sha.new() s.update(f.read()) return s.hexdigest() def sha_string(f): import sha s = sha.new() s.update(f) return s.hexdigest() def fingerprint_file(f): import sha s = sha.new() b = f.read() s.update(b) size = len(b) return {'size': size, 'sha1': s.hexdigest()} def auto_user_id(): """Calculate automatic user identification. Returns (realname, email). Only used when none is set in the environment. """ import socket # XXX: Any good way to get real user name on win32? # XXX: can the FQDN be non-ascii? try: import pwd uid = os.getuid() w = pwd.getpwuid(uid) gecos = w.pw_gecos.decode(bzrlib.user_encoding) username = w.pw_name.decode(bzrlib.user_encoding) comma = gecos.find(',') if comma == -1: realname = gecos else: realname = gecos[:comma] return realname, (username + '@' + socket.getfqdn()) except ImportError: import getpass getpass.getuser().decode(bzrlib.user_encoding) return '', (username + '@' + socket.getfqdn()) def username(): """Return email-style username. Something similar to 'Martin Pool ' :todo: Check it's reasonably well-formed. :todo: Allow taking it from a dotfile to help people on windows who can't easily set variables. """ e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: return e.decode(bzrlib.user_encoding) name, email = auto_user_id() if name: return '%s <%s>' % (name, email) else: return email _EMAIL_RE = re.compile(r'[\w+.-]+@[\w+.-]+') def user_email(): """Return just the email component of a username.""" e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL') if e: e = e.decode(bzrlib.user_encoding) m = _EMAIL_RE.search(e) if not m: bailout('%r is not a reasonable email address' % e) return m.group(0) return auto_user_id()[1] def compare_files(a, b): """Returns true if equal in contents""" # TODO: don't read the whole thing in one go. BUFSIZE = 4096 while True: ai = a.read(BUFSIZE) bi = b.read(BUFSIZE) if ai != bi: return False if ai == '': return True def local_time_offset(t=None): """Return offset of local zone from GMT, either at present or at time t.""" # python2.3 localtime() can't take None if t == None: t = time.time() if time.localtime(t).tm_isdst and time.daylight: return -time.altzone else: return -time.timezone def format_date(t, offset=0, timezone='original'): ## TODO: Perhaps a global option to use either universal or local time? ## Or perhaps just let people set $TZ? assert isinstance(t, float) if timezone == 'utc': tt = time.gmtime(t) offset = 0 elif timezone == 'original': if offset == None: offset = 0 tt = time.gmtime(t + offset) elif timezone == 'local': tt = time.localtime(t) offset = local_time_offset(t) else: bailout("unsupported timezone format %r", ['options are "utc", "original", "local"']) return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt) + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)) def compact_date(when): return time.strftime('%Y%m%d%H%M%S', time.gmtime(when)) def filesize(f): """Return size of given open file.""" return os.fstat(f.fileno())[ST_SIZE] if hasattr(os, 'urandom'): # python 2.4 and later rand_bytes = os.urandom else: # FIXME: No good on non-Linux _rand_file = file('/dev/urandom', 'rb') rand_bytes = _rand_file.read ## TODO: We could later have path objects that remember their list ## decomposition (might be too tricksy though.) def splitpath(p): """Turn string into list of parts. >>> splitpath('a') ['a'] >>> splitpath('a/b') ['a', 'b'] >>> splitpath('a/./b') ['a', 'b'] >>> splitpath('a/.b') ['a', '.b'] >>> splitpath('a/../b') Traceback (most recent call last): ... BzrError: ("sorry, '..' not allowed in path", []) """ assert isinstance(p, types.StringTypes) ps = [f for f in p.split('/') if (f != '.' and f != '')] for f in ps: if f == '..': bailout("sorry, %r not allowed in path" % f) return ps def joinpath(p): assert isinstance(p, list) for f in p: if (f == '..') or (f == None) or (f == ''): bailout("sorry, %r not allowed in path" % f) return '/'.join(p) def appendpath(p1, p2): if p1 == '': return p2 else: return p1 + '/' + p2 def extern_command(cmd, ignore_errors = False): mutter('external command: %s' % `cmd`) if os.system(cmd): if not ignore_errors: bailout('command failed') commit refs/heads/master mark :252 committer Martin Pool 1113526273 +1000 data 123 - Don't use host fqdn for default user name, because DNS tends to make it slow. - take email from ~/.bzr.email if present from :251 M 644 inline NEWS data 3493 bzr-0.0.4 NOT RELEASED YET ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/osutils.py data 8376 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, types, re, time, errno from stat import S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE from errors import bailout, BzrError from trace import mutter import bzrlib def make_readonly(filename): """Make a filename read-only.""" # TODO: probably needs to be fixed for windows mod = os.stat(filename).st_mode mod = mod & 0777555 os.chmod(filename, mod) def make_writable(filename): mod = os.stat(filename).st_mode mod = mod | 0200 os.chmod(filename, mod) _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])') def quotefn(f): """Return shell-quoted filename""" ## We could be a bit more terse by using double-quotes etc f = _QUOTE_RE.sub(r'\\\1', f) if f[0] == '~': f[0:1] = r'\~' return f def file_kind(f): mode = os.lstat(f)[ST_MODE] if S_ISREG(mode): return 'file' elif S_ISDIR(mode): return 'directory' elif S_ISLNK(mode): return 'symlink' else: raise BzrError("can't handle file kind with mode %o of %r" % (mode, f)) def isdir(f): """True if f is an accessible directory.""" try: return S_ISDIR(os.lstat(f)[ST_MODE]) except OSError: return False def isfile(f): """True if f is a regular file.""" try: return S_ISREG(os.lstat(f)[ST_MODE]) except OSError: return False def pumpfile(fromfile, tofile): """Copy contents of one file to another.""" tofile.write(fromfile.read()) def uuid(): """Return a new UUID""" ## XXX: Could alternatively read /proc/sys/kernel/random/uuid on ## Linux, but we need something portable for other systems; ## preferably an implementation in Python. try: return chomp(file('/proc/sys/kernel/random/uuid').readline()) except IOError: return chomp(os.popen('uuidgen').readline()) def chomp(s): if s and (s[-1] == '\n'): return s[:-1] else: return s def sha_file(f): import sha ## TODO: Maybe read in chunks to handle big files if hasattr(f, 'tell'): assert f.tell() == 0 s = sha.new() s.update(f.read()) return s.hexdigest() def sha_string(f): import sha s = sha.new() s.update(f) return s.hexdigest() def fingerprint_file(f): import sha s = sha.new() b = f.read() s.update(b) size = len(b) return {'size': size, 'sha1': s.hexdigest()} def _auto_user_id(): """Calculate automatic user identification. Returns (realname, email). Only used when none is set in the environment or the id file. This previously used the FQDN as the default domain, but that can be very slow on machines where DNS is broken. So now we simply use the hostname. """ import socket # XXX: Any good way to get real user name on win32? try: import pwd uid = os.getuid() w = pwd.getpwuid(uid) gecos = w.pw_gecos.decode(bzrlib.user_encoding) username = w.pw_name.decode(bzrlib.user_encoding) comma = gecos.find(',') if comma == -1: realname = gecos else: realname = gecos[:comma] except ImportError: realname = '' import getpass username = getpass.getuser().decode(bzrlib.user_encoding) return realname, (username + '@' + os.gethostname()) def _get_user_id(): v = os.environ.get('BZREMAIL') if v: return v.decode(bzrlib.user_encoding) try: return (open(os.path.expanduser("~/.bzr.email")) .read() .decode(bzrlib.user_encoding) .rstrip("\r\n")) except OSError, e: if e.errno != ENOENT: raise e v = os.environ.get('EMAIL') if v: return v.decode(bzrlib.user_encoding) else: return None def username(): """Return email-style username. Something similar to 'Martin Pool ' :todo: Check it's reasonably well-formed. :todo: Allow taking it from a dotfile to help people on windows who can't easily set variables. """ v = _get_user_id() if v: return v name, email = _auto_user_id() if name: return '%s <%s>' % (name, email) else: return email _EMAIL_RE = re.compile(r'[\w+.-]+@[\w+.-]+') def user_email(): """Return just the email component of a username.""" e = _get_user_id() if e: m = _EMAIL_RE.search(e) if not m: bailout("%r doesn't seem to contain a reasonable email address" % e) return m.group(0) return _auto_user_id()[1] def compare_files(a, b): """Returns true if equal in contents""" # TODO: don't read the whole thing in one go. BUFSIZE = 4096 while True: ai = a.read(BUFSIZE) bi = b.read(BUFSIZE) if ai != bi: return False if ai == '': return True def local_time_offset(t=None): """Return offset of local zone from GMT, either at present or at time t.""" # python2.3 localtime() can't take None if t == None: t = time.time() if time.localtime(t).tm_isdst and time.daylight: return -time.altzone else: return -time.timezone def format_date(t, offset=0, timezone='original'): ## TODO: Perhaps a global option to use either universal or local time? ## Or perhaps just let people set $TZ? assert isinstance(t, float) if timezone == 'utc': tt = time.gmtime(t) offset = 0 elif timezone == 'original': if offset == None: offset = 0 tt = time.gmtime(t + offset) elif timezone == 'local': tt = time.localtime(t) offset = local_time_offset(t) else: bailout("unsupported timezone format %r", ['options are "utc", "original", "local"']) return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt) + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)) def compact_date(when): return time.strftime('%Y%m%d%H%M%S', time.gmtime(when)) def filesize(f): """Return size of given open file.""" return os.fstat(f.fileno())[ST_SIZE] if hasattr(os, 'urandom'): # python 2.4 and later rand_bytes = os.urandom else: # FIXME: No good on non-Linux _rand_file = file('/dev/urandom', 'rb') rand_bytes = _rand_file.read ## TODO: We could later have path objects that remember their list ## decomposition (might be too tricksy though.) def splitpath(p): """Turn string into list of parts. >>> splitpath('a') ['a'] >>> splitpath('a/b') ['a', 'b'] >>> splitpath('a/./b') ['a', 'b'] >>> splitpath('a/.b') ['a', '.b'] >>> splitpath('a/../b') Traceback (most recent call last): ... BzrError: ("sorry, '..' not allowed in path", []) """ assert isinstance(p, types.StringTypes) ps = [f for f in p.split('/') if (f != '.' and f != '')] for f in ps: if f == '..': bailout("sorry, %r not allowed in path" % f) return ps def joinpath(p): assert isinstance(p, list) for f in p: if (f == '..') or (f == None) or (f == ''): bailout("sorry, %r not allowed in path" % f) return '/'.join(p) def appendpath(p1, p2): if p1 == '': return p2 else: return p1 + '/' + p2 def extern_command(cmd, ignore_errors = False): mutter('external command: %s' % `cmd`) if os.system(cmd): if not ignore_errors: bailout('command failed') commit refs/heads/master mark :253 committer Martin Pool 1113527304 +1000 data 51 - add test.sh for option parsing and status command from :252 M 644 inline test.sh data 2468 #! /bin/sh -pe # Simple shell-based tests for bzr. # This is meant to exercise the external behaviour, command line # parsing and similar things and compliment the inwardly-turned # testing done by doctest. # This must already exist and be in the right place if ! [ -d bzr-test.tmp ] then echo "please create directory bzr-test.tmp" exit 1 fi echo "testing `which bzr`" bzr --version | head -n 1 echo rm -rf bzr-test.tmp mkdir bzr-test.tmp # save it for real errors exec 3>&2 exec > bzr-test.log exec 2>&1 set -x quitter() { echo "tests failed, look in bzr-test.log" >&3; exit 2; } trap quitter ERR cd bzr-test.tmp rm -rf .bzr mkdir branch1 cd branch1 # some information commands bzr help bzr version # invalid commands are detected ! bzr pants # some experiments with renames bzr init echo "hello world" > test.txt bzr unknowns # should be the only unknown file [ "`bzr unknowns`" = test.txt ] bzr status --all > status.tmp ! diff -u - status.tmp < new-in-2.txt bzr add new-in-2.txt bzr commit -m "add file to branch 2 only" [ `bzr revno` = 3 ] cd ../branch1 [ `bzr revno` = 2 ] bzr check echo "tests completed ok" >&3 commit refs/heads/master mark :254 committer Martin Pool 1113528681 +1000 data 35 - Doc cleanups from Magnus Therning from :253 M 644 inline bzrlib/branch.py data 34659 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. TODO: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. TODO: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. TODO: Keep the on-disk branch locked while the object exists. TODO: mkdir() method. """ def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation import codecs return codecs.open(fn, mode + 'b', 'utf-8') else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) self.append_revision(rev_id) if verbose: note("commited r%d" % self.revno()) def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history', 'r').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] else: return None def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original', verbose=False): """Write out human-readable log of commits to this branch utc -- If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l if verbose == True and precursor != None: print 'changed files:' tree = self.revision_tree(p) prevtree = self.revision_tree(precursor) for file_state, fid, old_name, new_name, kind in \ diff_trees(prevtree, tree, ): if file_state == 'A' or file_state == 'M': show_status(file_state, kind, new_name) elif file_state == 'D': show_status(file_state, kind, old_name) elif file_state == 'R': show_status(file_state, kind, old_name + ' => ' + new_name) revno += 1 precursor = p def rename_one(self, from_rel, to_rel): tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(self, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo TODO: Get state for single files. TODO: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = self.basis_tree() new = self.working_tree() for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("weird file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" try: shutil.rmtree(self.base) except OSError: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/commands.py data 30368 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, time, types, shutil, tempfile, traceback, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and new_name not in file_list: continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_dump_text_inventory(): import bzrlib.textinv inv = Branch('.').basis_tree().inventory bzrlib.textinv.write_text_inventory(inv, sys.stdout) def cmd_load_text_inventory(): import bzrlib.textinv inv = bzrlib.textinv.read_text_inventory(sys.stdin) print 'loaded %d entries' % len(inv) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original', verbose=False): """Show log of this branch. TODO: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone, verbose=verbose) def cmd_ls(revision=None, verbose=False): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_username(): print bzrlib.osutils.username() def cmd_user_email(): print bzrlib.osutils.user_email() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone', 'verbose'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[str(k.replace('-', '_'))] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') traceback.print_exc(None, bzrlib.trace._tracefile) log_error('(see $HOME/.bzr.log for debug information)\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error('(see $HOME/.bzr.log for debug information)\n') traceback.print_exc(None, bzrlib.trace._tracefile) ## traceback.print_exc(None, sys.stderr) return 1 ## TODO: Trap AssertionError # TODO: Maybe nicer handling of IOError especially for broken pipe. if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') M 644 inline bzrlib/inventory.py data 19277 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # TODO: Maybe store inventory_id in the file? Not really needed. # This should really be an id randomly assigned when the tree is # created, but it's not for now. ROOT_ID = "TREE_ROOT" import sys, os.path, types, re from sets import Set try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from xml import XMLMixin from errors import bailout, BzrError import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') 'TREE_ROOT' >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID)) >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123')) >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' TODO: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ # TODO: split InventoryEntry into subclasses for files, # directories, etc etc. def __init__(self, file_id, name, kind, parent_id, text_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID) >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID) Traceback (most recent call last): BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", []) """ if len(splitpath(name)) != 1: bailout('InventoryEntry name is not a simple filename: %r' % name) self.file_id = file_id self.name = name self.kind = kind self.text_id = text_id self.parent_id = parent_id self.text_sha1 = None self.text_size = None if kind == 'directory': self.children = {} elif kind == 'file': pass else: raise BzrError("unhandled entry kind %r" % kind) def sorted_children(self): l = self.children.items() l.sort() return l def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.parent_id, text_id=self.text_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size != None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1']: v = getattr(self, f) if v != None: e.set(f, v) # to be conservative, we don't externalize the root pointers # for now, leaving them as null in the xml form. in a future # version it will be implied by nested elements. if self.parent_id != ROOT_ID: assert isinstance(self.parent_id, basestring) e.set('parent_id', self.parent_id) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' ## original format inventories don't have a parent_id for ## nodes in the root directory, but it's cleaner to use one ## internally. parent_id = elt.get('parent_id') if parent_id == None: parent_id = ROOT_ID self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __cmp__(self, other): if self is other: return 0 if not isinstance(other, InventoryEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.name, other.name) \ or cmp(self.text_sha1, other.text_sha1) \ or cmp(self.text_size, other.text_size) \ or cmp(self.text_id, other.text_id) \ or cmp(self.parent_id, other.parent_id) \ or cmp(self.kind, other.kind) class RootEntry(InventoryEntry): def __init__(self, file_id): self.file_id = file_id self.children = {} self.kind = 'root_directory' self.parent_id = None self.name = '' def __cmp__(self, other): if self is other: return 0 if not isinstance(other, RootEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.children, other.children) class Inventory(XMLMixin): """Inventory of versioned files in a tree. This describes which file_id is present at each point in the tree, and possibly the SHA-1 or other information about the file. Entries can be looked up either by path or by file_id. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted, other than through the Inventory API. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID)) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ ## TODO: Make sure only canonical filenames are stored. ## TODO: Do something sensible about the possible collisions on ## case-losing filesystems. Perhaps we should just always forbid ## such collisions. ## TODO: No special cases for root, rather just give it a file id ## like everything else. ## TODO: Probably change XML serialization to use nesting rather ## than parent_id pointers. ## TODO: Perhaps hold the ElementTree in memory and work directly ## on that rather than converting into Python objects every time? def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. The inventory is created with a default root directory, with an id of None. """ self.root = RootEntry(ROOT_ID) self._byid = {self.root.file_id: self.root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, from_dir=None): """Return (path, entry) pairs, in order by name.""" if from_dir == None: assert self.root from_dir = self.root elif isinstance(from_dir, basestring): from_dir = self._byid[from_dir] kids = from_dir.children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(from_dir=ie.file_id): yield '/'.join((name, cn)), cie def directories(self): """Return (path, entry) pairs for all directories. """ def descend(parent_ie): parent_name = parent_ie.name yield parent_name, parent_ie # directory children in sorted order dn = [] for ie in parent_ie.children.itervalues(): if ie.kind == 'directory': dn.append((ie.name, ie)) dn.sort() for name, child_ie in dn: for sub_name, sub_ie in descend(child_ie): yield appendpath(parent_name, sub_name), sub_ie for name, ie in descend(self.root): yield name, ie def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID)) >>> inv['123123'].name 'hello.c' """ if file_id == None: raise BzrError("can't look up file_id None") try: return self._byid[file_id] except KeyError: raise BzrError("file_id {%s} not in inventory" % file_id) def get_child(self, parent_id, filename): return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self._byid: bailout("inventory already contains entry with id {%s}" % entry.file_id) try: parent = self._byid[entry.parent_id] except KeyError: bailout("parent_id {%s} not in inventory" % entry.parent_id) if parent.children.has_key(entry.name): bailout("%s is already versioned" % appendpath(self.id2path(parent.file_id), entry.name)) self._byid[entry.file_id] = entry parent.children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: bailout("cannot re-add root of inventory") if file_id == None: file_id = bzrlib.branch.gen_file_id(relpath) parent_id = self.path2id(parts[:-1]) assert parent_id != None ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def id_set(self): return Set(self._byid) def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID)) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __cmp__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 True """ if self is other: return 0 if not isinstance(other, Inventory): return NotImplemented if self.id_set() ^ other.id_set(): return 1 for file_id in self._byid: c = cmp(self[file_id], other[file_id]) if c: return c return 0 def get_idpath(self, file_id): """Return a list of file_ids for the path to an entry. The list contains one element for each directory followed by the id of the file itself. So the length of the returned list is equal to the depth of the file in the tree, counting the root directory as depth 1. """ p = [] while file_id != None: try: ie = self._byid[file_id] except KeyError: bailout("file_id {%s} not found in inventory" % file_id) p.insert(0, ie.file_id) file_id = ie.parent_id return p def id2path(self, file_id): """Return as a list the path to file_id.""" # get all names, skipping root p = [self[fid].name for fid in self.get_idpath(file_id)[1:]] return '/'.join(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. Returns None iff the path is not found. """ if isinstance(name, types.StringTypes): name = splitpath(name) mutter("lookup path %r" % name) parent = self.root for f in name: try: cie = parent.children[f] assert cie.name == f assert cie.parent_id == parent.file_id parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): return self._byid.has_key(file_id) def rename(self, file_id, new_parent_id, new_name): """Move a file within the inventory. This can change either the name, or the parent, or both. This does not move the working file.""" if not is_valid_name(new_name): bailout("not an acceptable filename: %r" % new_name) new_parent = self._byid[new_parent_id] if new_name in new_parent.children: bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id))) new_parent_idpath = self.get_idpath(new_parent_id) if file_id in new_parent_idpath: bailout("cannot move directory %r into a subdirectory of itself, %r" % (self.id2path(file_id), self.id2path(new_parent_id))) file_ie = self._byid[file_id] old_parent = self._byid[file_ie.parent_id] # TODO: Don't leave things messed up if this fails del old_parent.children[file_ie.name] new_parent.children[new_name] = file_ie file_ie.name = new_name file_ie.parent_id = new_parent_id _NAME_RE = re.compile(r'^[^/\\]+$') def is_valid_name(name): return bool(_NAME_RE.match(name)) M 644 inline bzrlib/osutils.py data 8374 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, types, re, time, errno from stat import S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE from errors import bailout, BzrError from trace import mutter import bzrlib def make_readonly(filename): """Make a filename read-only.""" # TODO: probably needs to be fixed for windows mod = os.stat(filename).st_mode mod = mod & 0777555 os.chmod(filename, mod) def make_writable(filename): mod = os.stat(filename).st_mode mod = mod | 0200 os.chmod(filename, mod) _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])') def quotefn(f): """Return shell-quoted filename""" ## We could be a bit more terse by using double-quotes etc f = _QUOTE_RE.sub(r'\\\1', f) if f[0] == '~': f[0:1] = r'\~' return f def file_kind(f): mode = os.lstat(f)[ST_MODE] if S_ISREG(mode): return 'file' elif S_ISDIR(mode): return 'directory' elif S_ISLNK(mode): return 'symlink' else: raise BzrError("can't handle file kind with mode %o of %r" % (mode, f)) def isdir(f): """True if f is an accessible directory.""" try: return S_ISDIR(os.lstat(f)[ST_MODE]) except OSError: return False def isfile(f): """True if f is a regular file.""" try: return S_ISREG(os.lstat(f)[ST_MODE]) except OSError: return False def pumpfile(fromfile, tofile): """Copy contents of one file to another.""" tofile.write(fromfile.read()) def uuid(): """Return a new UUID""" ## XXX: Could alternatively read /proc/sys/kernel/random/uuid on ## Linux, but we need something portable for other systems; ## preferably an implementation in Python. try: return chomp(file('/proc/sys/kernel/random/uuid').readline()) except IOError: return chomp(os.popen('uuidgen').readline()) def chomp(s): if s and (s[-1] == '\n'): return s[:-1] else: return s def sha_file(f): import sha ## TODO: Maybe read in chunks to handle big files if hasattr(f, 'tell'): assert f.tell() == 0 s = sha.new() s.update(f.read()) return s.hexdigest() def sha_string(f): import sha s = sha.new() s.update(f) return s.hexdigest() def fingerprint_file(f): import sha s = sha.new() b = f.read() s.update(b) size = len(b) return {'size': size, 'sha1': s.hexdigest()} def _auto_user_id(): """Calculate automatic user identification. Returns (realname, email). Only used when none is set in the environment or the id file. This previously used the FQDN as the default domain, but that can be very slow on machines where DNS is broken. So now we simply use the hostname. """ import socket # XXX: Any good way to get real user name on win32? try: import pwd uid = os.getuid() w = pwd.getpwuid(uid) gecos = w.pw_gecos.decode(bzrlib.user_encoding) username = w.pw_name.decode(bzrlib.user_encoding) comma = gecos.find(',') if comma == -1: realname = gecos else: realname = gecos[:comma] except ImportError: realname = '' import getpass username = getpass.getuser().decode(bzrlib.user_encoding) return realname, (username + '@' + os.gethostname()) def _get_user_id(): v = os.environ.get('BZREMAIL') if v: return v.decode(bzrlib.user_encoding) try: return (open(os.path.expanduser("~/.bzr.email")) .read() .decode(bzrlib.user_encoding) .rstrip("\r\n")) except OSError, e: if e.errno != ENOENT: raise e v = os.environ.get('EMAIL') if v: return v.decode(bzrlib.user_encoding) else: return None def username(): """Return email-style username. Something similar to 'Martin Pool ' TODO: Check it's reasonably well-formed. TODO: Allow taking it from a dotfile to help people on windows who can't easily set variables. """ v = _get_user_id() if v: return v name, email = _auto_user_id() if name: return '%s <%s>' % (name, email) else: return email _EMAIL_RE = re.compile(r'[\w+.-]+@[\w+.-]+') def user_email(): """Return just the email component of a username.""" e = _get_user_id() if e: m = _EMAIL_RE.search(e) if not m: bailout("%r doesn't seem to contain a reasonable email address" % e) return m.group(0) return _auto_user_id()[1] def compare_files(a, b): """Returns true if equal in contents""" # TODO: don't read the whole thing in one go. BUFSIZE = 4096 while True: ai = a.read(BUFSIZE) bi = b.read(BUFSIZE) if ai != bi: return False if ai == '': return True def local_time_offset(t=None): """Return offset of local zone from GMT, either at present or at time t.""" # python2.3 localtime() can't take None if t == None: t = time.time() if time.localtime(t).tm_isdst and time.daylight: return -time.altzone else: return -time.timezone def format_date(t, offset=0, timezone='original'): ## TODO: Perhaps a global option to use either universal or local time? ## Or perhaps just let people set $TZ? assert isinstance(t, float) if timezone == 'utc': tt = time.gmtime(t) offset = 0 elif timezone == 'original': if offset == None: offset = 0 tt = time.gmtime(t + offset) elif timezone == 'local': tt = time.localtime(t) offset = local_time_offset(t) else: bailout("unsupported timezone format %r", ['options are "utc", "original", "local"']) return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt) + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)) def compact_date(when): return time.strftime('%Y%m%d%H%M%S', time.gmtime(when)) def filesize(f): """Return size of given open file.""" return os.fstat(f.fileno())[ST_SIZE] if hasattr(os, 'urandom'): # python 2.4 and later rand_bytes = os.urandom else: # FIXME: No good on non-Linux _rand_file = file('/dev/urandom', 'rb') rand_bytes = _rand_file.read ## TODO: We could later have path objects that remember their list ## decomposition (might be too tricksy though.) def splitpath(p): """Turn string into list of parts. >>> splitpath('a') ['a'] >>> splitpath('a/b') ['a', 'b'] >>> splitpath('a/./b') ['a', 'b'] >>> splitpath('a/.b') ['a', '.b'] >>> splitpath('a/../b') Traceback (most recent call last): ... BzrError: ("sorry, '..' not allowed in path", []) """ assert isinstance(p, types.StringTypes) ps = [f for f in p.split('/') if (f != '.' and f != '')] for f in ps: if f == '..': bailout("sorry, %r not allowed in path" % f) return ps def joinpath(p): assert isinstance(p, list) for f in p: if (f == '..') or (f == None) or (f == ''): bailout("sorry, %r not allowed in path" % f) return '/'.join(p) def appendpath(p1, p2): if p1 == '': return p2 else: return p1 + '/' + p2 def extern_command(cmd, ignore_errors = False): mutter('external command: %s' % `cmd`) if os.system(cmd): if not ignore_errors: bailout('command failed') M 644 inline bzrlib/revfile.py data 16487 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. This performs pretty well except when trying to calculate deltas of really large files. For that the main thing would be to plug in something faster than difflib, which is after all pure Python. Another approach is to just store the gzipped full text of big files, though perhaps that's too perverse? The iter method here will generally read through the whole index file in one go. With readahead in the kernel and python/libc (typically 128kB) this means that there should be no seeks and often only one read() call to get everything into memory. """ # TODO: Something like pread() would make this slightly simpler and # perhaps more efficient. # TODO: Could also try to mmap things... Might be faster for the # index in particular? # TODO: Some kind of faster lookup of SHAs? The bad thing is that probably means # rewriting existing records, which is not so nice. # TODO: Something to check that regions identified in the index file # completely butt up and do not overlap. Strictly it's not a problem # if there are gaps and that can happen if we're interrupted while # writing to the datafile. Overlapping would be very bad though. import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify, unhexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_RECORD = 0xFFFFFFFFL # fields in the index record I_SHA = 0 I_BASE = 1 I_FLAGS = 2 I_OFFSET = 3 I_LEN = 4 FL_GZIP = 1 # maximum number of patches in a row before recording a whole text. CHAIN_LIMIT = 50 class RevfileError(Exception): pass class LimitHitException(Exception): pass class Revfile: def __init__(self, basename, mode): # TODO: Lock file while open # TODO: advise of random access self.basename = basename if mode not in ['r', 'w']: raise RevfileError("invalid open mode %r" % mode) self.mode = mode idxname = basename + '.irev' dataname = basename + '.drev' idx_exists = os.path.exists(idxname) data_exists = os.path.exists(dataname) if idx_exists != data_exists: raise RevfileError("half-assed revfile") if not idx_exists: if mode == 'r': raise RevfileError("Revfile %r does not exist" % basename) self.idxfile = open(idxname, 'w+b') self.datafile = open(dataname, 'w+b') print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: if mode == 'r': diskmode = 'rb' else: diskmode = 'r+b' self.idxfile = open(idxname, diskmode) self.datafile = open(dataname, diskmode) h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def _check_index(self, idx): if idx < 0 or idx > len(self): raise RevfileError("invalid index %r" % idx) def _check_write(self): if self.mode != 'w': raise RevfileError("%r is open readonly" % self.basename) def find_sha(self, s): assert isinstance(s, str) assert len(s) == 20 for idx, idxrec in enumerate(self): if idxrec[I_SHA] == s: return idx else: return _NO_RECORD def _add_compressed(self, text_sha, data, base, compress): # well, maybe compress flags = 0 if compress: data_len = len(data) if data_len > 50: # don't do compression if it's too small; it's unlikely to win # enough to be worthwhile compr_data = zlib.compress(data) compr_len = len(compr_data) if compr_len < data_len: data = compr_data flags = FL_GZIP ##print '- compressed %d -> %d, %.1f%%' \ ## % (data_len, compr_len, float(compr_len)/float(data_len) * 100.0) return self._add_raw(text_sha, data, base, flags) def _add_raw(self, text_sha, data, base, flags): """Add pre-processed data, can be either full text or delta. This does the compression if that makes sense.""" idx = len(self) self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * (idx + 1) data_offset = self.datafile.tell() assert isinstance(data, str) # not unicode or anything weird self.datafile.write(data) self.datafile.flush() assert isinstance(text_sha, str) entry = text_sha entry += struct.pack(">IIII12x", base, flags, data_offset, len(data)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def _add_full_text(self, text, text_sha, compress): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" return self._add_compressed(text_sha, text, _NO_RECORD, compress) def _add_delta(self, text, text_sha, base, compress): """Add a text stored relative to a previous text.""" self._check_index(base) try: base_text = self.get(base, recursion_limit=CHAIN_LIMIT) except LimitHitException: return self._add_full_text(text, text_sha, compress) data = mdiff.bdiff(base_text, text) # If the delta is larger than the text, we might as well just # store the text. (OK, the delta might be more compressible, # but the overhead of applying it probably still makes it # bad, and I don't want to compress both of them to find out.) if len(data) >= len(text): return self._add_full_text(text, text_sha, compress) else: return self._add_compressed(text_sha, data, base, compress) def add(self, text, base=_NO_RECORD, compress=True): """Add a new text to the revfile. If the text is already present them its existing id is returned and the file is not changed. If compress is true then gzip compression will be used if it reduces the size. If a base index is specified, that text *may* be used for delta compression of the new text. Delta compression will only be used if it would be a size win and if the existing base is not at too long of a delta chain already. """ self._check_write() text_sha = sha.new(text).digest() idx = self.find_sha(text_sha) if idx != _NO_RECORD: # TODO: Optional paranoid mode where we read out that record and make sure # it's the same, in case someone ever breaks SHA-1. return idx # already present if base == _NO_RECORD: return self._add_full_text(text, text_sha, compress) else: return self._add_delta(text, text_sha, base, compress) def get(self, idx, recursion_limit=None): """Retrieve text of a previous revision. If recursion_limit is an integer then walk back at most that many revisions and then raise LimitHitException, indicating that we ought to record a new file text instead of another delta. Don't use this when trying to get out an existing revision.""" idxrec = self[idx] base = idxrec[I_BASE] if base == _NO_RECORD: text = self._get_full_text(idx, idxrec) else: text = self._get_patched(idx, idxrec, recursion_limit) if sha.new(text).digest() != idxrec[I_SHA]: raise RevfileError("corrupt SHA-1 digest on record %d" % idx) return text def _get_raw(self, idx, idxrec): flags = idxrec[I_FLAGS] if flags & ~FL_GZIP: raise RevfileError("unsupported index flags %#x on index %d" % (flags, idx)) l = idxrec[I_LEN] if l == 0: return '' self.datafile.seek(idxrec[I_OFFSET]) data = self.datafile.read(l) if len(data) != l: raise RevfileError("short read %d of %d " "getting text for record %d in %r" % (len(data), l, idx, self.basename)) if flags & FL_GZIP: data = zlib.decompress(data) return data def _get_full_text(self, idx, idxrec): assert idxrec[I_BASE] == _NO_RECORD text = self._get_raw(idx, idxrec) return text def _get_patched(self, idx, idxrec, recursion_limit): base = idxrec[I_BASE] assert base >= 0 assert base < idx # no loops! if recursion_limit == None: sub_limit = None else: sub_limit = recursion_limit - 1 if sub_limit < 0: raise LimitHitException() base_text = self.get(base, sub_limit) patch = self._get_raw(idx, idxrec) text = mdiff.bpatch(base_text, patch) return text def __len__(self): """Return number of revisions.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) if l < _RECORDSIZE: raise RevfileError("no header present in index of %r" % (self.basename)) return int(l / _RECORDSIZE) - 1 def __getitem__(self, idx): """Index by sequence id returns the index field""" ## TODO: Can avoid seek if we just moved there... self._seek_index(idx) idxrec = self._read_next_index() if idxrec == None: raise IndexError() else: return idxrec def _seek_index(self, idx): if idx < 0: raise RevfileError("invalid index %r" % idx) self.idxfile.seek((idx + 1) * _RECORDSIZE) def __iter__(self): """Read back all index records. Do not seek the index file while this is underway!""" sys.stderr.write(" ** iter called ** \n") self._seek_index(0) while True: idxrec = self._read_next_index() if not idxrec: break yield idxrec def _read_next_index(self): rec = self.idxfile.read(_RECORDSIZE) if not rec: return None elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_RECORD: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def total_text_size(self): """Return the sum of sizes of all file texts. This is how much space they would occupy if they were stored without delta and gzip compression. As a side effect this completely validates the Revfile, checking that all texts can be reproduced with the correct SHA-1.""" t = 0L for idx in range(len(self)): t += len(self.get(idx)) return t def main(argv): try: cmd = argv[1] except IndexError: sys.stderr.write("usage: revfile dump\n" " revfile add\n" " revfile add-delta BASE\n" " revfile get IDX\n" " revfile find-sha HEX\n" " revfile total-text-size\n" " revfile last\n") return 1 def rw(): return Revfile('testrev', 'w') def ro(): return Revfile('testrev', 'r') if cmd == 'add': print rw().add(sys.stdin.read()) elif cmd == 'add-delta': print rw().add(sys.stdin.read(), int(argv[2])) elif cmd == 'dump': ro().dump() elif cmd == 'get': try: idx = int(argv[2]) except IndexError: sys.stderr.write("usage: revfile get IDX\n") return 1 if idx < 0 or idx >= len(r): sys.stderr.write("invalid index %r\n" % idx) return 1 sys.stdout.write(ro().get(idx)) elif cmd == 'find-sha': try: s = unhexlify(argv[2]) except IndexError: sys.stderr.write("usage: revfile find-sha HEX\n") return 1 idx = ro().find_sha(s) if idx == _NO_RECORD: sys.stderr.write("no such record\n") return 1 else: print idx elif cmd == 'total-text-size': print ro().total_text_size() elif cmd == 'last': print len(ro())-1 else: sys.stderr.write("unknown command %r\n" % cmd) return 1 if __name__ == '__main__': import sys sys.exit(main(sys.argv) or 0) M 644 inline bzrlib/revision.py data 2866 # (C) 2005 Canonical # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from xml import XMLMixin try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from errors import BzrError class Revision(XMLMixin): """Single revision on a branch. Revisions may know their revision_hash, but only once they've been written out. This is not stored because you cannot write the hash into the file it describes. TODO: Perhaps make predecessor be a child element, not an attribute? """ def __init__(self, **args): self.inventory_id = None self.revision_id = None self.timestamp = None self.message = None self.timezone = None self.committer = None self.precursor = None self.__dict__.update(args) def __repr__(self): return "" % self.revision_id def to_element(self): root = Element('revision', committer = self.committer, timestamp = '%.9f' % self.timestamp, revision_id = self.revision_id, inventory_id = self.inventory_id, timezone = str(self.timezone)) if self.precursor: root.set('precursor', self.precursor) root.text = '\n' msg = SubElement(root, 'message') msg.text = self.message msg.tail = '\n' return root def from_element(cls, elt): # is deprecated... if elt.tag not in ('revision', 'changeset'): raise BzrError("unexpected tag in revision file: %r" % elt) cs = cls(committer = elt.get('committer'), timestamp = float(elt.get('timestamp')), precursor = elt.get('precursor'), revision_id = elt.get('revision_id'), inventory_id = elt.get('inventory_id')) v = elt.get('timezone') cs.timezone = v and int(v) cs.message = elt.findtext('message') # text of return cs from_element = classmethod(from_element) M 644 inline bzrlib/store.py data 5357 # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Stores are the main data-storage mechanism for Bazaar-NG. A store is a simple write-once container indexed by a universally unique ID, which is typically the SHA-1 of the content.""" __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import os, tempfile, types, osutils, gzip, errno from stat import ST_SIZE from StringIO import StringIO from trace import mutter ###################################################################### # stores class StoreError(Exception): pass class ImmutableStore: """Store that holds files indexed by unique names. Files can be added, but not modified once they are in. Typically the hash is used as the name, or something else known to be unique, such as a UUID. >>> st = ImmutableScratchStore() >>> st.add(StringIO('hello'), 'aa') >>> 'aa' in st True >>> 'foo' in st False You are not allowed to add an id that is already present. Entries can be retrieved as files, which may then be read. >>> st.add(StringIO('goodbye'), '123123') >>> st['123123'].read() 'goodbye' TODO: Atomic add by writing to a temporary file and renaming. TODO: Perhaps automatically transform to/from XML in a method? Would just need to tell the constructor what class to use... TODO: Even within a simple disk store like this, we could gzip the files. But since many are less than one disk block, that might not help a lot. """ def __init__(self, basedir): """ImmutableStore constructor.""" self._basedir = basedir def _path(self, id): return os.path.join(self._basedir, id) def __repr__(self): return "%s(%r)" % (self.__class__.__name__, self._basedir) def add(self, f, fileid, compressed=True): """Add contents of a file into the store. f -- An open file, or file-like object.""" # FIXME: Only works on smallish files # TODO: Can be optimized by copying at the same time as # computing the sum. mutter("add store entry %r" % (fileid)) if isinstance(f, types.StringTypes): content = f else: content = f.read() p = self._path(fileid) if os.access(p, os.F_OK) or os.access(p + '.gz', os.F_OK): bailout("store %r already contains id %r" % (self._basedir, fileid)) if compressed: f = gzip.GzipFile(p + '.gz', 'wb') os.chmod(p + '.gz', 0444) else: f = file(p, 'wb') os.chmod(p, 0444) f.write(content) f.close() def __contains__(self, fileid): """""" p = self._path(fileid) return (os.access(p, os.R_OK) or os.access(p + '.gz', os.R_OK)) # TODO: Guard against the same thing being stored twice, compressed and uncompresse def __iter__(self): for f in os.listdir(self._basedir): if f[-3:] == '.gz': # TODO: case-insensitive? yield f[:-3] else: yield f def __len__(self): return len(os.listdir(self._basedir)) def __getitem__(self, fileid): """Returns a file reading from a particular entry.""" p = self._path(fileid) try: return gzip.GzipFile(p + '.gz', 'rb') except IOError, e: if e.errno == errno.ENOENT: return file(p, 'rb') else: raise e def total_size(self): """Return (count, bytes) This is the (compressed) size stored on disk, not the size of the content.""" total = 0 count = 0 for fid in self: count += 1 p = self._path(fid) try: total += os.stat(p)[ST_SIZE] except OSError: total += os.stat(p + '.gz')[ST_SIZE] return count, total class ImmutableScratchStore(ImmutableStore): """Self-destructing test subclass of ImmutableStore. The Store only exists for the lifetime of the Python object. Obviously you should not put anything precious in it. """ def __init__(self): ImmutableStore.__init__(self, tempfile.mkdtemp()) def __del__(self): for f in os.listdir(self._basedir): fpath = os.path.join(self._basedir, f) # needed on windows, and maybe some other filesystems os.chmod(fpath, 0600) os.remove(fpath) os.rmdir(self._basedir) mutter("%r destroyed" % self) M 644 inline bzrlib/tree.py data 14239 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tree classes, representing directory at point in time. """ from sets import Set import os.path, os, fnmatch from osutils import pumpfile, compare_files, filesize, quotefn, sha_file, \ joinpath, splitpath, appendpath, isdir, isfile, file_kind, fingerprint_file import errno from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE from inventory import Inventory from trace import mutter, note from errors import bailout import branch import bzrlib class Tree: """Abstract file tree. There are several subclasses: * `WorkingTree` exists as files on disk editable by the user. * `RevisionTree` is a tree as recorded at some point in the past. * `EmptyTree` Trees contain an `Inventory` object, and also know how to retrieve file texts mentioned in the inventory, either from a working directory or from a store. It is possible for trees to contain files that are not described in their inventory or vice versa; for this use `filenames()`. Trees can be compared, etc, regardless of whether they are working trees or versioned trees. """ def has_filename(self, filename): """True if the tree has given filename.""" raise NotImplementedError() def has_id(self, file_id): return self.inventory.has_id(file_id) def id_set(self): """Return set of all ids in this tree.""" return self.inventory.id_set() def id2path(self, file_id): return self.inventory.id2path(file_id) def _get_inventory(self): return self._inventory inventory = property(_get_inventory, doc="Inventory of this Tree") def _check_retrieved(self, ie, f): fp = fingerprint_file(f) f.seek(0) if ie.text_size != None: if ie.text_size != fp['size']: bailout("mismatched size for file %r in %r" % (ie.file_id, self._store), ["inventory expects %d bytes" % ie.text_size, "file is actually %d bytes" % fp['size'], "store is probably damaged/corrupt"]) if ie.text_sha1 != fp['sha1']: bailout("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store), ["inventory expects %s" % ie.text_sha1, "file is actually %s" % fp['sha1'], "store is probably damaged/corrupt"]) def print_file(self, fileid): """Print file with id `fileid` to stdout.""" import sys pumpfile(self.get_file(fileid), sys.stdout) def export(self, dest): """Export this tree to a new directory. `dest` should not exist, and will be created holding the contents of this tree. TODO: To handle subdirectories we need to create the directories first. :note: If the export fails, the destination directory will be left in a half-assed state. """ os.mkdir(dest) mutter('export version %r' % self) inv = self.inventory for dp, ie in inv.iter_entries(): kind = ie.kind fullpath = appendpath(dest, dp) if kind == 'directory': os.mkdir(fullpath) elif kind == 'file': pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb')) else: bailout("don't know how to export {%s} of kind %r" % (fid, kind)) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) class WorkingTree(Tree): """Working copy tree. The inventory is held in the `Branch` working-inventory, and the files are in a directory on disk. It is possible for a `WorkingTree` to have a filename which is not listed in the Inventory and vice versa. """ def __init__(self, basedir, inv): self._inventory = inv self.basedir = basedir self.path2id = inv.path2id def __repr__(self): return "<%s of %s>" % (self.__class__.__name__, self.basedir) def abspath(self, filename): return os.path.join(self.basedir, filename) def has_filename(self, filename): return os.path.exists(self.abspath(filename)) def get_file(self, file_id): return self.get_file_byname(self.id2path(file_id)) def get_file_byname(self, filename): return file(self.abspath(filename), 'rb') def _get_store_filename(self, file_id): ## XXX: badly named; this isn't in the store at all return self.abspath(self.id2path(file_id)) def has_id(self, file_id): # files that have been deleted are excluded if not self.inventory.has_id(file_id): return False return os.access(self.abspath(self.inventory.id2path(file_id)), os.F_OK) def get_file_size(self, file_id): return os.stat(self._get_store_filename(file_id))[ST_SIZE] def get_file_sha1(self, file_id): f = self.get_file(file_id) return sha_file(f) def file_class(self, filename): if self.path2id(filename): return 'V' elif self.is_ignored(filename): return 'I' else: return '?' def list_files(self): """Recursively list all files as (path, class, kind, id). Lists, but does not descend into unversioned directories. This does not include files that have been deleted in this tree. Skips the control directory. """ inv = self.inventory def descend(from_dir_relpath, from_dir_id, dp): ls = os.listdir(dp) ls.sort() for f in ls: ## TODO: If we find a subdirectory with its own .bzr ## directory, then that is a separate tree and we ## should exclude it. if bzrlib.BZRDIR == f: continue # path within tree fp = appendpath(from_dir_relpath, f) # absolute path fap = appendpath(dp, f) f_ie = inv.get_child(from_dir_id, f) if f_ie: c = 'V' elif self.is_ignored(fp): c = 'I' else: c = '?' fk = file_kind(fap) if f_ie: if f_ie.kind != fk: bailout("file %r entered as kind %r id %r, now of kind %r" % (fap, f_ie.kind, f_ie.file_id, fk)) yield fp, c, fk, (f_ie and f_ie.file_id) if fk != 'directory': continue if c != 'V': # don't descend unversioned directories continue for ff in descend(fp, f_ie.file_id, fap): yield ff for f in descend('', inv.root.file_id, self.basedir): yield f def unknowns(self): for subp in self.extras(): if not self.is_ignored(subp): yield subp def extras(self): """Yield all unknown files in this WorkingTree. If there are any unknown directories then only the directory is returned, not all its children. But if there are unknown files under a versioned subdirectory, they are returned. Currently returned depth-first, sorted by name within directories. """ ## TODO: Work from given directory downwards for path, dir_entry in self.inventory.directories(): mutter("search for unknowns in %r" % path) dirabs = self.abspath(path) if not isdir(dirabs): # e.g. directory deleted continue fl = [] for subf in os.listdir(dirabs): if (subf != '.bzr' and (subf not in dir_entry.children)): fl.append(subf) fl.sort() for subf in fl: subp = appendpath(path, subf) yield subp def ignored_files(self): """Yield list of PATH, IGNORE_PATTERN""" for subp in self.extras(): pat = self.is_ignored(subp) if pat != None: yield subp, pat def get_ignore_list(self): """Return list of ignore patterns. Cached in the Tree object after the first call. """ if hasattr(self, '_ignorelist'): return self._ignorelist l = bzrlib.DEFAULT_IGNORE[:] if self.has_filename(bzrlib.IGNORE_FILENAME): f = self.get_file_byname(bzrlib.IGNORE_FILENAME) l.extend([line.rstrip("\n\r") for line in f.readlines()]) self._ignorelist = l return l def is_ignored(self, filename): """Check whether the filename matches an ignore pattern. Patterns containing '/' need to match the whole path; others match against only the last component. If the file is ignored, returns the pattern which caused it to be ignored, otherwise None. So this can simply be used as a boolean if desired.""" ## TODO: Use '**' to match directories, and other extended globbing stuff from cvs/rsync. for pat in self.get_ignore_list(): if '/' in pat: # as a special case, you can put ./ at the start of a pattern; # this is good to match in the top-level only; if pat[:2] == './': newpat = pat[2:] else: newpat = pat if fnmatch.fnmatchcase(filename, newpat): return pat else: if fnmatch.fnmatchcase(splitpath(filename)[-1], pat): return pat return None class RevisionTree(Tree): """Tree viewing a previous revision. File text can be retrieved from the text store. TODO: Some kind of `__repr__` method, but a good one probably means knowing the branch and revision number, or at least passing a description to the constructor. """ def __init__(self, store, inv): self._store = store self._inventory = inv def get_file(self, file_id): ie = self._inventory[file_id] f = self._store[ie.text_id] mutter(" get fileid{%s} from %r" % (file_id, self)) self._check_retrieved(ie, f) return f def get_file_size(self, file_id): return self._inventory[file_id].text_size def get_file_sha1(self, file_id): ie = self._inventory[file_id] return ie.text_sha1 def has_filename(self, filename): return bool(self.inventory.path2id(filename)) def list_files(self): # The only files returned by this are those from the version for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id class EmptyTree(Tree): def __init__(self): self._inventory = Inventory() def has_filename(self, filename): return False def list_files(self): if False: # just to make it a generator yield None ###################################################################### # diff # TODO: Merge these two functions into a single one that can operate # on either a whole tree or a set of files. # TODO: Return the diff in order by filename, not by category or in # random order. Can probably be done by lock-stepping through the # filenames from both trees. def file_status(filename, old_tree, new_tree): """Return single-letter status, old and new names for a file. The complexity here is in deciding how to represent renames; many complex cases are possible. """ old_inv = old_tree.inventory new_inv = new_tree.inventory new_id = new_inv.path2id(filename) old_id = old_inv.path2id(filename) if not new_id and not old_id: # easy: doesn't exist in either; not versioned at all if new_tree.is_ignored(filename): return 'I', None, None else: return '?', None, None elif new_id: # There is now a file of this name, great. pass else: # There is no longer a file of this name, but we can describe # what happened to the file that used to have # this name. There are two possibilities: either it was # deleted entirely, or renamed. assert old_id if new_inv.has_id(old_id): return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id) else: return 'D', old_inv.id2path(old_id), None # if the file_id is new in this revision, it is added if new_id and not old_inv.has_id(new_id): return 'A' # if there used to be a file of this name, but that ID has now # disappeared, it is deleted if old_id and not new_inv.has_id(old_id): return 'D' return 'wtf?' def find_renames(old_inv, new_inv): for file_id in old_inv: if file_id not in new_inv: continue old_name = old_inv.id2path(file_id) new_name = new_inv.id2path(file_id) if old_name != new_name: yield (old_name, new_name) M 644 inline doc/bitkeeper.txt data 898 Bitkeeper compared to Arch ========================== BK has a default GUI, which is good, but it's ugly and not all that friendly to the new user. It does pay attention to allowing quick keyboard navigation. The tool itself is also a bit unfriendly in terms of emitting a lot of noise messages and having lots of weird commands. Bitkeeper always requires both per-file and per-changeset commands. It seems bad to always require this; at most per-file messages should be optional. Are they really the best option? Claimed to have extremely space-efficient storage, keeping 19 years of history in slightly more than twice the size of the working copy. Hashes: does Baz always have these even without signing? It really should. Fine-grained event triggers, pre- and post-event. Can remotely find status of a tree, e.g. parent, number of comitters, versioned files, extras, modified, etc. M 644 inline doc/formats.txt data 8161 ***************** Bazaar-NG formats ***************** .. contents:: Since branches are working directories there is just a single directory format. There is one metadata directory called ``.bzr`` at the top of each tree. Control files inside ``.bzr`` are never touched by patches and should not normally be edited by the user. These files are designed so that repository-level operations are ACID without depending on atomic operations spanning multiple files. There are two particular cases: aborting a transaction in the middle, and contention from multiple processes. We also need to be careful to flush files to disk at appropriate points; even this may not be totally safe if the filesystem does not guarantee ordering between multiple file changes, so we need to be sure to roll back. The design must also be such that the directory can simply be copied and that hardlinked directories will work. (So we must always replace files, never just append.) A cache is kept under here of easily-accessible information about previous revisions. This should be under a single directory so that it can be easily identified, excluded from backups, removed, etc. This might contain pristine tree from previous revisions, manifests and inventories, etc. It might also contain working directories when building a commit, etc. Call this maybe ``cache`` or ``tmp``. I wonder if we should use .zip files for revisions and cacherevs rather than tar files so that random access is easier/more efficient. There is a Python library ``zipfile``. Signing XML files ***************** bzr relies on storing hashes or GPG signatures of various XML files. There can be multiple equivalent representations of the same XML tree, but these will have different byte-by-byte hashes. Once signed files are written out, they must be stored byte-for-byte and never re-encoded or renormalized, because that would break their hash or signature. Branch metadata *************** All inside ``.bzr`` ``README`` Tells people not to touch anything here. ``branch-format`` Identifies the parent as a Bazaar-NG branch; contains the overall branch metadata format as a string. ``pristine-directory`` Identifies that this is a pristine directory and may not be committed to. ``patches/`` Directory containing all patches applied to this branch, one per file. Patches are stored as compressed deltas. We also store the hash of the delta, hash of the before and after manifests, and optionally a GPG signature. ``cache/`` Contains various cached data that can be destroyed and will be recreated. (It should not be modified.) ``cache/pristine/`` Contains cached full trees for selected previous revisions, used when generating diffs, etc. ``cache/inventory/`` Contains cached inventories of previous revisions. ``cache/snapshot/`` Contains tarballs of cached revisions of the tree, named by their revision id. These can also be removed, but ``patch-history`` File containing the UUIDs of all patches taken in this branch, in the order they were taken. Each commit adds exactly one line to this file; lines are never removed or reordered. ``merged-patches`` List of foreign patches that have been merged into this branch. Must have no entries in common with ``patch-history``. Commits that include merges add to this file; lines are never removed or reordered. ``pending-merge-patches`` List of foreign patches that have been merged and are waiting to be committed. ``branch-name`` User-qualified name of the branch, for the purpose of describing the origin of patches, e.g. ``mbp@sourcefrog.net/distcc--main``. ``friends`` List of branches from which we have pulled; file containing a list of pairs of branch-name and location. ``parent`` Default pull/push target. ``pending-inventory`` Mapping from UUIDs to file name in the current working directory. ``branch-lock`` Lock held while modifying the branch, to protect against clashing updates. Locking ******* Is locking a good strategy? Perhaps somekind of read-copy-update or seq-lock based mechanism would work better? If we do use a locking algorithm, is it OK to rely on filesystem locking or do we need our own mechanism? I think most hosts should have reasonable ``flock()`` or equivalent, even on NFS. One risk is that on NFS it is easy to have broken locking and not know it, so it might be better to have something that will fail safe. Filesystem locks go away if the machine crashes or the process is terminated; this can be a feature in that we do not need to deal with stale locks but also a feature in that the lock itself does not indicate cleanup may be needed. robertc points out that tla converged on renaming a directory as a mechanism: this is one thing which is known to be atomic on almost all filesystems. Apparently renaming files, creating directories, making symlinks etc are not good enough. Delta ***** XML document plus a bag of patches, expressing the difference between two revisions. May be a partial delta. * list of entries * entry * parent directory (if any) * before-name or null if new * after-name or null if deleted * uuid * type (dir, file, symlink, ...) * patch type (patch, full-text, xdelta, ...) * patch filename (?) Inventory ********* XML document; series of entries. (Quite similar to the svn ``entries`` file; perhaps should even have that name.) Stored identified by its hash. An inventory is stored for recorded revisions, also a ``pending-inventory`` for a working directory. Revision ******** XML document. Stored identified by its hash. committer RFC-2822-style name of the committer. Should match the key used to sign the revision. comment multi-line free-form text; whitespace and line breaks preserved timestamp As floating-point seconds since epoch. precursor ID of the previous revision on this branch. May be absent (null) if this is the start of a new branch. branch name Name of the branch to which this was originally committed. (I'm not totally satisfied that this is the right way to do it; the results will be a bit weird when a series of revisions pass through variously named branches.) inventory_hash Acts as a pointer to the inventory for this revision. merged-branches Revision ids of complete branches merged into this revision. If a revision is listed, that revision and transitively its predecessor and all other merged-branches are merged. This is empty except where cherry-picks have occurred. merged-patches Revision ids of cherry-picked patches. Patches whose branches are merged need not be listed here. Listing a revision ID implies that only the change of that particular revision from its predecessor has been merged in. This is empty except where cherry-picks have occurred. The transitive closure avoids Arch's problem of needing to list a large number of previous revisions. As ddaa writes: Continuation revisions (created by tla tag or baz branch) are associated to a patchlog whose New-patches header lists the revisions associated to all the patchlogs present in the tree. That was introduced as an optimisation so the set of patchlogs in any revision could be determined solely by examining the patchlogs of ancestor revisions in the same branch. This behaves well as long as the total count of patchlog is reasonably small or new branches are not very frequent. A continuation revision on $tree currently creates a patchlog of about 500K. This patchlog is present in all descendent of the revision, and all revisions that merges it. It may be useful at some times to keep a cache of all the branches, or all the revisions, present in the history of a branch, so that we do need to walk the whole history of the branch to build this list. ---- Proposed changes **************** * Don't store parent-id in all revisions, but rather have nodes that contain entries for children? * Assign an id to the root of the tree, perhaps listed in the top of the inventory? M 644 inline doc/interrupted.txt data 4293 Interrupted operations ********************** Problem: interrupted operations =============================== Many version control systems tend to have trouble when operations are interrupted. This can happen in various ways: * user hits Ctrl-C * program hits a bug and aborts * machine crashes * network goes down * tree is naively copied (e.g. by cp/tar) while an operation is in progress We can reduce the window during which operations can be interrupted: most importantly, by receiving everything off the network into a staging area, so that network interruptions won't leave a job half complete. But it is not possible to totally avoid this, because the power can always fail. I think we can reasonably rely on flushing to stable storage at various points, and trust that such files will be accessible when we come back up. I think by using this and building from the bottom up there are never any broken pointers in the branch metadata: first we add the file versions, then the inventory, then the revision and signature, then link them into the revision history. The worst that can happen is that there will be some orphaned files if this is interrupted at any point. rsync is just impossible in the general case: it reads the files in a fairly unpredictable order, so what it copies may not be a tree that existed at any particular point in time. If people want to make backups or replicate using rsync they need to treat it like any other database and either * make a copy which will not be updated, and rsync from that * lock the database while rsyncing The operating system facilities are not sufficient to protect against all of these. We cannot satisfactorily commit a whole atomic transaction in one step. Operations might be updating either the metadata or the working copy. The working copy is in some ways more difficult: * Other processes are allowed to modify it from time to time in arbitrary ways. If they modify it while bazaar is working then they will lose, but we should at least try to make sure there is no corruption. * We can't atomically replace the whole working copy. We can (semi) atomically updated particular files. * If the working copy files are in a weird state it is hard to know whether that occurred because bzr's work was interrupted or because the user changed them. (A reasonable user might run ``bzr revert`` if they notice something like this has happened, but it would be nice to avoid it.) We don't want to leave things in a broken state. Solution: write-ahead journaling? ================================= One possibly solution might be write-ahead journaling: Before beginning a change, write and flush to disk a description of what change will be made. Every bzr operation checks this journal; if there are any pending operations waiting then they are completed first, before proceeding with whatever the user wanted. (Perhaps this should be in a separate ``bzr recover``, but I think it's better to just do it, perhaps with a warning.) The descriptions written into the journal need to be simple enough that they can safely be re-run in a totally different context. They must not depend on any external resources which might have gone away. If we can do anything without depending on journalling we should. It may be that the only case where we cannot get by with just ordering is in updating the working copy; the user might get into a difficult situation where they have pulled in a change and only half the working copy has been updated. One solution would be to remove the working copy files, or mark them readonly, while this is in progress. We don't want people accidentally writing to a file that needs to be overwritten. Or perhaps, in this particular case, it is OK to leave them in pointing to an old state, and let people revert if they're sure they want the new one? Sounds dangerous. Aaron points out that this basically sounds like changesets. So before updating the history, we first calculate the changeset and write it out to stable storage as a single file. We then apply the changeset, possibly updating several files. Each command should check whether such an application was in progress. M 644 inline doc/python.txt data 4230 Choice of Python ---------------- This will be written in Python, at least for the first cut, just for ease of development -- I think I am at least 2-3 times faster than in C or C++, and bugs may be less severe. I am open to the idea of switching to C at some time in the future, but because that is enormously expensive I want to avoid it until it's clearly necessary. Python is also a good platform to handle cross-platform portability. Possible reasons to go to C: Audience acceptance If Linus says "I'd use it if it were written in C" that would be persuasive. I think the good developers we want to do not consider implementation language as a dominant factor. A few related but separate questions are important to them: modest dependencies, easy installation, presence in distributions (or as packages), active support, etc. A few queries show that Python is seen as relatively safe and acceptable even by people who don't actually use it. Speed Having scalable designs is much more important. Secondly, we will do most of the heavy lifting in external C programs in the first cut, and perhaps move these into native libraries later. (Subversion people had trouble in relying on GNU diff on legacy platforms and they had to integrate the code eventually.) Bindings to other languages If we have only a Python interpretation then it can be run as a shell script from emacs or similar tools. It can also be natively called from Python scripts, which would allow GUI bindings to almost every toolkit, and it can possibly be called from Java and .NET/Mono. By the time this is mature, it's possible that Python code will be able to cross-call Perl and other languages through Parrot. There should be enough options there to support a good infrastructure there of additional tools. If it was necessary to provide a C API that can perhaps be wrapped around a Python library. Reuse of tla code That may be useful, if there are substantial sections that approach or meet our goals for both design and implementation (e.g. being good to use from a library.) This does not necessarily mean doing the whole thing in C; we could call out to tla or could wrap particular bits into libraries. ---- Erik Bågfors: However, I think it's very important that a VCS can be wrapped in other languages so that it can be integrated in IDE's and have tools written for them. A library written in c would be simple to wrap in other languages and therefore could be used from for example monodevelop and friends. I really believe this is important for a VCS. I agree; this is a more important argument against Python than speed, where I think we can be entirely adequate just using smart design. But there are some partial answers: We can design bzr to be easily called as an external process -- not depending on interactive input, having systematically parsed output, --format=xml output, etc. This is the only mode CVS supports, and people have built many interesting tools on top of it, and it's still popular for svn and tla. For things like editor integration this is often the easiest way. Secondly, there is a good chance of calling into Python from other languages. There are projects like Jython, IronPython, Parrot and so on that may well fix this. Thirdly, we can present a Python library through a C interface; this might seem a bit weird but I think it will work fine. Python is easily embeddable; this might be the best way for Windows IDE integration. Finally, if none of these work, then we can always recode in C, treating Python only as a prototype. I think working in Python I can develop it at least twice as fast as in C, particularly in this early phase where the design is still being worked out. Although all other things being equal it might be nice to be in pure C, but I don't think it's worth paying that price. One of the problems with darcs is that it's such a mess wrapping it. Yes. ---- Experiments to date on large trees show that even with little optimization, bzr is mostly disk-bound, and the CPU time usage is only a few seconds. That supports the position that Python performance will be adequate. M 644 inline doc/todo-from-arch.txt data 13588 ***************************************** Opportunities for improvement on GNU Arch ***************************************** Bazaar-NG is based on the GNU Arch system, and inherits a lot of its design from Arch. However, there are several things we will change in Baz to (we hope) improve the user experience. The core design of Arch is good, brilliant even. It can scale from small projects too large ones, and is a good foundation for building tools on top. However, the design is far too complex, both in concepts and execution. So the plan is to cut out as many things as we can, add a few other good concepts from other systems, and try to make it into a whole that is consistent and understandable. Good bits to keep ----------------- * Roll-up changesets No other system is able to express this valuable idea: "I merged all these changes from other people; here is the result." However, it should *also* be possible to bring in perfect-fit patches without creating a new commit. * Star-merge Find a common ancestor on diverged and cross-merged branches. * Apply isolated changesets. We should extend this by having a good way to send changesets by email, preferably readable even by people who are not using Arch. * GPG signing of commits. Open source hackers almost all have GPG keys already, and GPG deals with a lot of PKI functions to do with propagating, signing and revoking keys. Signed commits are interesting in many ways, not least of which in detecting intrusion to code servers. * Anonymous downloads can be done without an active server. Good for security; also very good for people who do not have a permnanently-connected machine on which they can install their own software, or which is very tightly secured. It's neat that you can upload over only sftp/ftp, but I'm not sure it's really worth the hassle; getting properly atomic operations over remote-file protocols is hard. * Clean and transparent storage format. This is a neat hack, and gives people assurance that they can get their data back out again even if the tool disappears. Very nice. (Bazaar-NG won't keep the exact same format, but the ideas will be similar.) * Relatively easily parseable/scriptable shell interface. Good for people writing web/emacs/editor/IDE interfaces, or scripts based it. * Automatically build (and hardlink) revision libraries, with consistency checks. I don't know how many people want *every* revision in a library, but it can be handy to have a few key ones. In general making use of hardlinks when they are available and safe is nice. * Rely on ssh for remote access, authentication, and confidentiality. * Patch headers separate from patch bodies. (Sometimes you only want one.) * Autogeneration of Changelogs -- but should be in GNU format, at least optionally. I'm not convinced auto-updating them in the tree is worthwhile; it makes merges weird. * Sealing branches. It seems useful to prevent accidental commits to things that are meant to be stable. However, the set-once nature of sealing is undesirable, because people can make mistakes or want to seal more than once. One possibility is to have a voluntary write-protect flag set on branches that should not normally be updated. One can remove the flag if it turns out it was set wrongly. * ``resolved`` command in Bazaar-1.1 Good for preventing accidental breakage. * Multi-level undo -- though could perhaps be more understandable, perhaps through ``undo-history``. Bits to cut out --------------- One lesson from usability design is that it does not always work to have a complex model and then try to hide complexity in the user interface. If you want something to be a joy to use, that must be designed in from the bottom up. (Some developers may react to tla by thinking "eww, how gross" on particular points. As much as possible we might like to fix these.) * General impression that the tool is telling you how to run your life. * Non-standard terminology Arch uses terms like "version" and "category" in ways that are confusing to people accustomed to other version control systems. This is not helpful. Therefore: development proceeds on a *branch*, which is a series of *revisions*. Simple and obvious. * Too many commands. * Command-line options are wierdly inconsistent with both other systems, with each others, and with what people would like to do. For example, I would think the obvious usage is ``bzr diff [FILE]``, but ``tla diff`` does not let you specify a file at all. Most commands should take filenames as their argument: log, diff, add, commit, etc. * Despite having too many commands, there are massive and glaring gaps, such reverting a single file or a tree. * Commands are too different from what people are used to in CVS, and often not for a good reason. * Identifiers are too long. In part this is because Arch tries to have identifiers which are both human-assigned and universally unique. * Archive names are probably unnecessary. * Part of the reason for complexity in archives is that the Arch design wants to be able to go and find patches on other branches at a later time. (This is not really implemented or used at the moment.) I think the complexity is unjustified: changesets and revisions have universally unique names so they can simply be archived, either on the machine of the person who wants them or on a central site like supermirror. * The tool is *unforgiving*; if people create a branch with the wrong name it will be around forever. * Branches are heaviweight; a record always persists in the archive. Sometimes it is good to create micro-branches, try something out, and then discard them. If nobody wants the changes, there is no reason for the tool to keep them. * Working offline requires creating a new branch and merging back and forth. This is both more work than it should be, and also polutes the "story" told by branching. As much as possible, the *accidental* difference of the location of the repository should not effect the *semantics* of branches. (However, some merging may obviously be necessary when there is divergence.) * Archive registration. This causes confusion and is unnecessary. Proposed solutions such as archive aliases or an additional command to register-and-get make it worse. * Wierd file names (``++`` and ``,,``, which persist in user directories and cause breakage of many tools. Gives a bad impression, and it's even worse when people have to interact with them. * Overly-long identifiers. (One advantage of pointing to branches using filenames or URLs is that the length of the path depends on how close it is to the users location, and they can more easily use * Too slow by default. Arch can be made fast, but in the hands of a nonexpert user it is often slow. For most users, disk is cheaper than CPU time, which is cheaper than network roundtrips. The performance model should be transparent -- users should not be surprised that something is slow. * Tagging onto branches. Unifying tags and commits is interesting, but the result is hard to mentally model; even Arch maintainers can't say exactly how it is supposed to work in some cases. * Reinventing the world from scratch in libhackerlab/frob/pika/xl. Those are all fine projects and may be useful in the future, but they are totally unnecessary to write a great version control system. It is not an enormous project; it is not CPU-cycle critical; something like Python will be fine. * Lack (for the moment) of an active server. Given that network traffic is the most expensive thing, we can possibly get a better solution by having intelligence on both sides of the link. Suppose we want to get just one file from a previous revision... * Poor Windows/Mac support. Even though many developers only work on Linux, this still holds a tool back. The reason is this: at least some projects have some developers on Windows some of the time. Those projects can't switch to Arch. Most people want to only learn one tool deeply, so it won't be Arch. Don't make any overly Unixy assumptions. Avoid too-cute filesystem dependencies. Being in Python should help with portability: people do need to install it, but many developers will already have it and the total burden is possibly less than that of installing C requisite libraries. * Quirky filename support. Files with non-ascii names, or names containing whitespace tend to be handled poorly, perhaps partly because of arch's shell heritage. By swallowing XML we do at least get automatic quoting of wierd strings, and we will always use UTF-8 for internal storage. * Complex file-id-tagging Nobody should be expected to understand this. There are two basic cases: people want to auto-add everything, and want to add by hand. Both can be reasonably accomodated in a simpler system. * Complex naming-convention regexps in ``.arch-inventory`` and ``{arch}/id-tagging-method``. (The fact that there are two overlapping mechanisms with very different names is also bad.) All this complexity basically just comes down to versioned, ignored, unknown, the same as in every other system. So we might as well just have that. There are relatively few cases where regexps help more than globs, and people do find them more complex. Even experienced users can forget to escape ``\.``. We can have a bit of flexibility with (say) zsh-style extended globs like ``*.(pyo|pyc)``. * Some files inside ``{arch}`` are meant to be edited by the user, and some are not. This is a flaw common to other systems, including Bitkeeper. The user should be clear on whether they should touch things in a directory or not. * Source-librarian function works poorly. It is not the place of a tool to force people to stay organized; it should just facilitate it. In any case, a library without descriptive text is of little use. So bazaar-ng does not force three-level naming but rather lets people arrange their own trees, and put on their own descriptions (either within the tree, or by e.g. having a wiki page listing branches, descriptions and URLs.) * Whining about inode mismatches on pristines/revlibs. It's fine that there is validation, but the tool should not show off its limitations. Just do the right thing. * More generally, not quite enough consistency/safety checking. * Unclear what commands work on subdirs and what works on the whole tree. * Hard to share work on a single branch -- though still not really too bad. * Lack of partial commits of added/deleted files. * Separate id tags for each file; simple implementation but probably costs too much disk space. * Way too many deeply-nested directories; should be just one. * ``.listing`` files are ugly and a point of failure. They can cause trouble on some servers which limit access to dot files. Isn't it possible to have the top-level file be predictable and find everything else needed from there? * Summary separate from log message. Simpler to just have one message, and let people extract the first line/sentence if they wish. Rather than 'keywords', let arbitrary properties be attached to the revision at the time of commit. Simpler disconnected operation ------------------------------ A basic distributed VCS operation is to make it easy to work on an offline laptop. Arch can do this in a few ways, but none of them are really simple. http://wiki.gnuarch.org/moin.cgi/mini_5fTravellingOftenWithArch Yaron Minsky writes (2005-01-18): I was wondering what people considered to be a good setup for using Arch on a laptop. Here's the basic situation. I have a few projects that reside in arch repositories on my desktop computer. Basically, I'd like to be able to do commits from my laptop, and have those commits eventually migrate up to the main repository. I understand that the right way of doing this is to set up archives on the laptop. But what's the cleanest way of doing this? And is there some way of making the commits I do on the laptop show up cleanly and individually on the desktop once they are merged in? Tagging-method -------------- baz default is much less strict. Much of tla depends on being able to categorize files. Some hangovers from larch -- eg precious and backup are essentially the same. junk is never deleted today. Automatic version control with 'untagged-source source'. But this is deprecated for baz? Annoyed by - defaults - having the feature at all - complex way to define it Default of 166 lines. Remove id-tagging-method command or at most make it read-only. If people really want to use deprecated methods they can just edit the file. So we can ship a default id-tagging which works the same as CVS/Svn: give warnings for files that are not known to be junk. This is the default in baz right now. Also we have .arch-inventory, which is per-directory. Why not have 'baz ignore FILENAME'? To remove ignores, perhaps you have to edit the .arch-inventory. Print "FILTER added to PATH/.arch-inventory"; create and baz-add this file if it doesn't. Docs should perhaps emphasize .arch-inventory as the basic method and only mention =tagging-method as an advanced topic. Should this really be regexps, or just file globs? commit refs/heads/master mark :255 committer Martin Pool 1113528737 +1000 data 20 - New whoami command from :254 M 644 inline NEWS data 3557 bzr-0.0.4 NOT RELEASED YET ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 30407 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, time, types, shutil, tempfile, traceback, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and new_name not in file_list: continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_dump_text_inventory(): import bzrlib.textinv inv = Branch('.').basis_tree().inventory bzrlib.textinv.write_text_inventory(inv, sys.stdout) def cmd_load_text_inventory(): import bzrlib.textinv inv = bzrlib.textinv.read_text_inventory(sys.stdin) print 'loaded %d entries' % len(inv) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original', verbose=False): """Show log of this branch. TODO: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone, verbose=verbose) def cmd_ls(revision=None, verbose=False): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_whoami(): """Show bzr user id. TODO: Command to show only the email-address part as parsed out. """ print bzrlib.osutils.username() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone', 'verbose'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND\n') log_error(' try "bzr help"\n') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[str(k.replace('-', '_'))] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0] + '\n') if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h + '\n') traceback.print_exc(None, bzrlib.trace._tracefile) log_error('(see $HOME/.bzr.log for debug information)\n') return 1 except Exception, e: log_error('bzr: exception: %s\n' % e) log_error('(see $HOME/.bzr.log for debug information)\n') traceback.print_exc(None, bzrlib.trace._tracefile) ## traceback.print_exc(None, sys.stderr) return 1 ## TODO: Trap AssertionError # TODO: Maybe nicer handling of IOError especially for broken pipe. if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :256 committer Martin Pool 1113529013 +1000 data 37 - More handling of auto-username case from :255 M 644 inline bzrlib/osutils.py data 8430 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, types, re, time, errno from stat import S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE from errors import bailout, BzrError from trace import mutter import bzrlib def make_readonly(filename): """Make a filename read-only.""" # TODO: probably needs to be fixed for windows mod = os.stat(filename).st_mode mod = mod & 0777555 os.chmod(filename, mod) def make_writable(filename): mod = os.stat(filename).st_mode mod = mod | 0200 os.chmod(filename, mod) _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])') def quotefn(f): """Return shell-quoted filename""" ## We could be a bit more terse by using double-quotes etc f = _QUOTE_RE.sub(r'\\\1', f) if f[0] == '~': f[0:1] = r'\~' return f def file_kind(f): mode = os.lstat(f)[ST_MODE] if S_ISREG(mode): return 'file' elif S_ISDIR(mode): return 'directory' elif S_ISLNK(mode): return 'symlink' else: raise BzrError("can't handle file kind with mode %o of %r" % (mode, f)) def isdir(f): """True if f is an accessible directory.""" try: return S_ISDIR(os.lstat(f)[ST_MODE]) except OSError: return False def isfile(f): """True if f is a regular file.""" try: return S_ISREG(os.lstat(f)[ST_MODE]) except OSError: return False def pumpfile(fromfile, tofile): """Copy contents of one file to another.""" tofile.write(fromfile.read()) def uuid(): """Return a new UUID""" ## XXX: Could alternatively read /proc/sys/kernel/random/uuid on ## Linux, but we need something portable for other systems; ## preferably an implementation in Python. try: return chomp(file('/proc/sys/kernel/random/uuid').readline()) except IOError: return chomp(os.popen('uuidgen').readline()) def chomp(s): if s and (s[-1] == '\n'): return s[:-1] else: return s def sha_file(f): import sha ## TODO: Maybe read in chunks to handle big files if hasattr(f, 'tell'): assert f.tell() == 0 s = sha.new() s.update(f.read()) return s.hexdigest() def sha_string(f): import sha s = sha.new() s.update(f) return s.hexdigest() def fingerprint_file(f): import sha s = sha.new() b = f.read() s.update(b) size = len(b) return {'size': size, 'sha1': s.hexdigest()} def _auto_user_id(): """Calculate automatic user identification. Returns (realname, email). Only used when none is set in the environment or the id file. This previously used the FQDN as the default domain, but that can be very slow on machines where DNS is broken. So now we simply use the hostname. """ import socket # XXX: Any good way to get real user name on win32? try: import pwd uid = os.getuid() w = pwd.getpwuid(uid) gecos = w.pw_gecos.decode(bzrlib.user_encoding) username = w.pw_name.decode(bzrlib.user_encoding) comma = gecos.find(',') if comma == -1: realname = gecos else: realname = gecos[:comma] if not realname: realname = username except ImportError: import getpass realname = username = getpass.getuser().decode(bzrlib.user_encoding) return realname, (username + '@' + socket.gethostname()) def _get_user_id(): v = os.environ.get('BZREMAIL') if v: return v.decode(bzrlib.user_encoding) try: return (open(os.path.expanduser("~/.bzr.email")) .read() .decode(bzrlib.user_encoding) .rstrip("\r\n")) except IOError, e: if e.errno != errno.ENOENT: raise e v = os.environ.get('EMAIL') if v: return v.decode(bzrlib.user_encoding) else: return None def username(): """Return email-style username. Something similar to 'Martin Pool ' TODO: Check it's reasonably well-formed. TODO: Allow taking it from a dotfile to help people on windows who can't easily set variables. """ v = _get_user_id() if v: return v name, email = _auto_user_id() if name: return '%s <%s>' % (name, email) else: return email _EMAIL_RE = re.compile(r'[\w+.-]+@[\w+.-]+') def user_email(): """Return just the email component of a username.""" e = _get_user_id() if e: m = _EMAIL_RE.search(e) if not m: bailout("%r doesn't seem to contain a reasonable email address" % e) return m.group(0) return _auto_user_id()[1] def compare_files(a, b): """Returns true if equal in contents""" # TODO: don't read the whole thing in one go. BUFSIZE = 4096 while True: ai = a.read(BUFSIZE) bi = b.read(BUFSIZE) if ai != bi: return False if ai == '': return True def local_time_offset(t=None): """Return offset of local zone from GMT, either at present or at time t.""" # python2.3 localtime() can't take None if t == None: t = time.time() if time.localtime(t).tm_isdst and time.daylight: return -time.altzone else: return -time.timezone def format_date(t, offset=0, timezone='original'): ## TODO: Perhaps a global option to use either universal or local time? ## Or perhaps just let people set $TZ? assert isinstance(t, float) if timezone == 'utc': tt = time.gmtime(t) offset = 0 elif timezone == 'original': if offset == None: offset = 0 tt = time.gmtime(t + offset) elif timezone == 'local': tt = time.localtime(t) offset = local_time_offset(t) else: bailout("unsupported timezone format %r", ['options are "utc", "original", "local"']) return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt) + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)) def compact_date(when): return time.strftime('%Y%m%d%H%M%S', time.gmtime(when)) def filesize(f): """Return size of given open file.""" return os.fstat(f.fileno())[ST_SIZE] if hasattr(os, 'urandom'): # python 2.4 and later rand_bytes = os.urandom else: # FIXME: No good on non-Linux _rand_file = file('/dev/urandom', 'rb') rand_bytes = _rand_file.read ## TODO: We could later have path objects that remember their list ## decomposition (might be too tricksy though.) def splitpath(p): """Turn string into list of parts. >>> splitpath('a') ['a'] >>> splitpath('a/b') ['a', 'b'] >>> splitpath('a/./b') ['a', 'b'] >>> splitpath('a/.b') ['a', '.b'] >>> splitpath('a/../b') Traceback (most recent call last): ... BzrError: ("sorry, '..' not allowed in path", []) """ assert isinstance(p, types.StringTypes) ps = [f for f in p.split('/') if (f != '.' and f != '')] for f in ps: if f == '..': bailout("sorry, %r not allowed in path" % f) return ps def joinpath(p): assert isinstance(p, list) for f in p: if (f == '..') or (f == None) or (f == ''): bailout("sorry, %r not allowed in path" % f) return '/'.join(p) def appendpath(p1, p2): if p1 == '': return p2 else: return p1 + '/' + p2 def extern_command(cmd, ignore_errors = False): mutter('external command: %s' % `cmd`) if os.system(cmd): if not ignore_errors: bailout('command failed') commit refs/heads/master mark :257 committer Martin Pool 1113532143 +1000 data 121 - Write less startup junk to .bzr.log - Clean up trace.py code. - Write pids into log to help with concurrent execution. from :256 M 644 inline NEWS data 3599 bzr-0.0.4 NOT RELEASED YET ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 30397 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, time, types, shutil, tempfile, traceback, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and new_name not in file_list: continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_dump_text_inventory(): import bzrlib.textinv inv = Branch('.').basis_tree().inventory bzrlib.textinv.write_text_inventory(inv, sys.stdout) def cmd_load_text_inventory(): import bzrlib.textinv inv = bzrlib.textinv.read_text_inventory(sys.stdin) print 'loaded %d entries' % len(inv) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original', verbose=False): """Show log of this branch. TODO: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone, verbose=verbose) def cmd_ls(revision=None, verbose=False): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_whoami(): """Show bzr user id. TODO: Command to show only the email-address part as parsed out. """ print bzrlib.osutils.username() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone', 'verbose'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[str(k.replace('-', '_'))] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0]) if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h) traceback.print_exc(None, bzrlib.trace._tracefile) log_error('(see ~/.bzr.log for debug information)') return 1 except Exception, e: log_error('bzr: exception: %s' % str(e).rstrip('\n')) log_error('(see $HOME/.bzr.log for debug information)') traceback.print_exc(None, bzrlib.trace._tracefile) ## traceback.print_exc(None, sys.stderr) return 1 ## TODO: Trap AssertionError # TODO: Maybe nicer handling of IOError especially for broken pipe. if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') M 644 inline bzrlib/trace.py data 3745 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os, time, socket, stat, codecs import bzrlib ###################################################################### # messages and logging ## TODO: If --verbose is given then write to both stderr and ## _tracefile; perhaps replace _tracefile with a tee thing. global _tracefile, _starttime _tracefile = None # used to have % (os.environ['USER'], time.time(), os.getpid()), 'w') _starttime = None # If false, notes also go to stdout; should replace this with --silent # at some point. silent = False # TODO: Somehow tie this to the --verbose option? verbose = False # fix this if we ever fork within python _mypid = os.getpid() _logprefix = '[%d] ' % _mypid def _write_trace(msg): _tracefile.write(_logprefix + msg + '\n') def warning(msg): sys.stderr.write('bzr: warning: ' + msg + '\n') _write_trace('warning: ' + msg) mutter = _write_trace def note(msg): b = '* ' + str(msg) + '\n' if not silent: sys.stderr.write(b) _write_trace('note: ' + msg) def log_error(msg): sys.stderr.write(msg + '\n') _write_trace(msg) # TODO: Something to log exceptions in here. def create_tracefile(argv): # TODO: Also show contents of /etc/lsb-release, if it can be parsed. # Perhaps that should eventually go into the platform library? # TODO: If the file doesn't exist, add a note describing it. # Messages are always written to here, so that we have some # information if something goes wrong. In a future version this # file will be removed on successful completion. global _starttime, _tracefile _starttime = os.times()[4] # TODO: If the file exists and is too large, rename it to .old; # must handle failures of this because we can't rename an open # file on Windows. trace_fname = os.path.join(os.path.expanduser('~/.bzr.log')) # buffering=1 means line buffered _tracefile = codecs.open(trace_fname, 'at', 'utf8', buffering=1) t = _tracefile if os.fstat(t.fileno())[stat.ST_SIZE] == 0: t.write("\nthis is a debug log for diagnosing/reporting problems in bzr\n") t.write("you can delete or truncate this file, or include sections in\n") t.write("bug reports to bazaar-ng@lists.canonical.com\n\n") # TODO: If we failed to create the file, perhaps give a warning # but don't abort; send things to /dev/null instead? _write_trace('bzr %s invoked on python %s (%s)' % (bzrlib.__version__, '.'.join(map(str, sys.version_info)), sys.platform)) _write_trace(' arguments: %r' % argv) _write_trace(' working dir: ' + os.getcwdu()) import atexit atexit.register(_close_trace) def _close_trace(): times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum, %.3f elapsed" % (times[:4] + ((times[4] - _starttime),))) commit refs/heads/master mark :258 committer Martin Pool 1113532447 +1000 data 35 - Take email from ~/.bzr.conf/email from :257 M 644 inline bzrlib/osutils.py data 8702 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, types, re, time, errno from stat import S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE from errors import bailout, BzrError from trace import mutter import bzrlib def make_readonly(filename): """Make a filename read-only.""" # TODO: probably needs to be fixed for windows mod = os.stat(filename).st_mode mod = mod & 0777555 os.chmod(filename, mod) def make_writable(filename): mod = os.stat(filename).st_mode mod = mod | 0200 os.chmod(filename, mod) _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])') def quotefn(f): """Return shell-quoted filename""" ## We could be a bit more terse by using double-quotes etc f = _QUOTE_RE.sub(r'\\\1', f) if f[0] == '~': f[0:1] = r'\~' return f def file_kind(f): mode = os.lstat(f)[ST_MODE] if S_ISREG(mode): return 'file' elif S_ISDIR(mode): return 'directory' elif S_ISLNK(mode): return 'symlink' else: raise BzrError("can't handle file kind with mode %o of %r" % (mode, f)) def isdir(f): """True if f is an accessible directory.""" try: return S_ISDIR(os.lstat(f)[ST_MODE]) except OSError: return False def isfile(f): """True if f is a regular file.""" try: return S_ISREG(os.lstat(f)[ST_MODE]) except OSError: return False def pumpfile(fromfile, tofile): """Copy contents of one file to another.""" tofile.write(fromfile.read()) def uuid(): """Return a new UUID""" ## XXX: Could alternatively read /proc/sys/kernel/random/uuid on ## Linux, but we need something portable for other systems; ## preferably an implementation in Python. try: return chomp(file('/proc/sys/kernel/random/uuid').readline()) except IOError: return chomp(os.popen('uuidgen').readline()) def chomp(s): if s and (s[-1] == '\n'): return s[:-1] else: return s def sha_file(f): import sha ## TODO: Maybe read in chunks to handle big files if hasattr(f, 'tell'): assert f.tell() == 0 s = sha.new() s.update(f.read()) return s.hexdigest() def sha_string(f): import sha s = sha.new() s.update(f) return s.hexdigest() def fingerprint_file(f): import sha s = sha.new() b = f.read() s.update(b) size = len(b) return {'size': size, 'sha1': s.hexdigest()} def config_dir(): """Return per-user configuration directory. By default this is ~/.bzr.conf/ TODO: Global option --config-dir to override this. """ return os.path.expanduser("~/.bzr.conf") def _auto_user_id(): """Calculate automatic user identification. Returns (realname, email). Only used when none is set in the environment or the id file. This previously used the FQDN as the default domain, but that can be very slow on machines where DNS is broken. So now we simply use the hostname. """ import socket # XXX: Any good way to get real user name on win32? try: import pwd uid = os.getuid() w = pwd.getpwuid(uid) gecos = w.pw_gecos.decode(bzrlib.user_encoding) username = w.pw_name.decode(bzrlib.user_encoding) comma = gecos.find(',') if comma == -1: realname = gecos else: realname = gecos[:comma] if not realname: realname = username except ImportError: import getpass realname = username = getpass.getuser().decode(bzrlib.user_encoding) return realname, (username + '@' + socket.gethostname()) def _get_user_id(): """Return the full user id from a file or environment variable. TODO: Allow taking this from a file in the branch directory too for per-branch ids.""" v = os.environ.get('BZREMAIL') if v: return v.decode(bzrlib.user_encoding) try: return (open(os.path.join(config_dir(), "email")) .read() .decode(bzrlib.user_encoding) .rstrip("\r\n")) except IOError, e: if e.errno != errno.ENOENT: raise e v = os.environ.get('EMAIL') if v: return v.decode(bzrlib.user_encoding) else: return None def username(): """Return email-style username. Something similar to 'Martin Pool ' TODO: Check it's reasonably well-formed. """ v = _get_user_id() if v: return v name, email = _auto_user_id() if name: return '%s <%s>' % (name, email) else: return email _EMAIL_RE = re.compile(r'[\w+.-]+@[\w+.-]+') def user_email(): """Return just the email component of a username.""" e = _get_user_id() if e: m = _EMAIL_RE.search(e) if not m: bailout("%r doesn't seem to contain a reasonable email address" % e) return m.group(0) return _auto_user_id()[1] def compare_files(a, b): """Returns true if equal in contents""" # TODO: don't read the whole thing in one go. BUFSIZE = 4096 while True: ai = a.read(BUFSIZE) bi = b.read(BUFSIZE) if ai != bi: return False if ai == '': return True def local_time_offset(t=None): """Return offset of local zone from GMT, either at present or at time t.""" # python2.3 localtime() can't take None if t == None: t = time.time() if time.localtime(t).tm_isdst and time.daylight: return -time.altzone else: return -time.timezone def format_date(t, offset=0, timezone='original'): ## TODO: Perhaps a global option to use either universal or local time? ## Or perhaps just let people set $TZ? assert isinstance(t, float) if timezone == 'utc': tt = time.gmtime(t) offset = 0 elif timezone == 'original': if offset == None: offset = 0 tt = time.gmtime(t + offset) elif timezone == 'local': tt = time.localtime(t) offset = local_time_offset(t) else: bailout("unsupported timezone format %r", ['options are "utc", "original", "local"']) return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt) + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)) def compact_date(when): return time.strftime('%Y%m%d%H%M%S', time.gmtime(when)) def filesize(f): """Return size of given open file.""" return os.fstat(f.fileno())[ST_SIZE] if hasattr(os, 'urandom'): # python 2.4 and later rand_bytes = os.urandom else: # FIXME: No good on non-Linux _rand_file = file('/dev/urandom', 'rb') rand_bytes = _rand_file.read ## TODO: We could later have path objects that remember their list ## decomposition (might be too tricksy though.) def splitpath(p): """Turn string into list of parts. >>> splitpath('a') ['a'] >>> splitpath('a/b') ['a', 'b'] >>> splitpath('a/./b') ['a', 'b'] >>> splitpath('a/.b') ['a', '.b'] >>> splitpath('a/../b') Traceback (most recent call last): ... BzrError: ("sorry, '..' not allowed in path", []) """ assert isinstance(p, types.StringTypes) ps = [f for f in p.split('/') if (f != '.' and f != '')] for f in ps: if f == '..': bailout("sorry, %r not allowed in path" % f) return ps def joinpath(p): assert isinstance(p, list) for f in p: if (f == '..') or (f == None) or (f == ''): bailout("sorry, %r not allowed in path" % f) return '/'.join(p) def appendpath(p1, p2): if p1 == '': return p2 else: return p1 + '/' + p2 def extern_command(cmd, ignore_errors = False): mutter('external command: %s' % `cmd`) if os.system(cmd): if not ignore_errors: bailout('command failed') commit refs/heads/master mark :259 committer Martin Pool 1113532707 +1000 data 58 - use larger file buffers when opening branch control file from :258 M 644 inline bzrlib/branch.py data 34778 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. TODO: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. TODO: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. TODO: Keep the on-disk branch locked while the object exists. TODO: mkdir() method. """ def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) self.append_revision(rev_id) if verbose: note("commited r%d" % self.revno()) def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history', 'r').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] else: return None def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original', verbose=False): """Write out human-readable log of commits to this branch utc -- If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l if verbose == True and precursor != None: print 'changed files:' tree = self.revision_tree(p) prevtree = self.revision_tree(precursor) for file_state, fid, old_name, new_name, kind in \ diff_trees(prevtree, tree, ): if file_state == 'A' or file_state == 'M': show_status(file_state, kind, new_name) elif file_state == 'D': show_status(file_state, kind, old_name) elif file_state == 'R': show_status(file_state, kind, old_name + ' => ' + new_name) revno += 1 precursor = p def rename_one(self, from_rel, to_rel): tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(self, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo TODO: Get state for single files. TODO: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = self.basis_tree() new = self.working_tree() for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("weird file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" try: shutil.rmtree(self.base) except OSError: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :260 committer Martin Pool 1113532912 +1000 data 60 - remove atexit() dependency for writing out execution times from :259 M 644 inline bzrlib/commands.py data 30518 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, time, types, shutil, tempfile, traceback, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and new_name not in file_list: continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_dump_text_inventory(): import bzrlib.textinv inv = Branch('.').basis_tree().inventory bzrlib.textinv.write_text_inventory(inv, sys.stdout) def cmd_load_text_inventory(): import bzrlib.textinv inv = bzrlib.textinv.read_text_inventory(sys.stdin) print 'loaded %d entries' % len(inv) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original', verbose=False): """Show log of this branch. TODO: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone, verbose=verbose) def cmd_ls(revision=None, verbose=False): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_whoami(): """Show bzr user id. TODO: Command to show only the email-address part as parsed out. """ print bzrlib.osutils.username() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone', 'verbose'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[str(k.replace('-', '_'))] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: return cmd_handler(**cmdargs) or 0 def main(argv): ## TODO: Handle command-line options; probably know what options are valid for ## each command ## TODO: If the arguments are wrong, give a usage message rather ## than just a backtrace. bzrlib.trace.create_tracefile(argv) try: try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0]) if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h) traceback.print_exc(None, bzrlib.trace._tracefile) log_error('(see ~/.bzr.log for debug information)') return 1 except Exception, e: log_error('bzr: exception: %s' % str(e).rstrip('\n')) log_error('(see $HOME/.bzr.log for debug information)') traceback.print_exc(None, bzrlib.trace._tracefile) ## traceback.print_exc(None, sys.stderr) return 1 finally: bzrlib.trace.close_trace() ## TODO: Trap AssertionError # TODO: Maybe nicer handling of IOError especially for broken pipe. if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') M 644 inline bzrlib/trace.py data 3691 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os, time, socket, stat, codecs import bzrlib ###################################################################### # messages and logging ## TODO: If --verbose is given then write to both stderr and ## _tracefile; perhaps replace _tracefile with a tee thing. global _tracefile, _starttime _tracefile = None # used to have % (os.environ['USER'], time.time(), os.getpid()), 'w') _starttime = None # If false, notes also go to stdout; should replace this with --silent # at some point. silent = False # TODO: Somehow tie this to the --verbose option? verbose = False # fix this if we ever fork within python _mypid = os.getpid() _logprefix = '[%d] ' % _mypid def _write_trace(msg): _tracefile.write(_logprefix + msg + '\n') def warning(msg): sys.stderr.write('bzr: warning: ' + msg + '\n') _write_trace('warning: ' + msg) mutter = _write_trace def note(msg): b = '* ' + str(msg) + '\n' if not silent: sys.stderr.write(b) _write_trace('note: ' + msg) def log_error(msg): sys.stderr.write(msg + '\n') _write_trace(msg) # TODO: Something to log exceptions in here. def create_tracefile(argv): # TODO: Also show contents of /etc/lsb-release, if it can be parsed. # Perhaps that should eventually go into the platform library? # TODO: If the file doesn't exist, add a note describing it. # Messages are always written to here, so that we have some # information if something goes wrong. In a future version this # file will be removed on successful completion. global _starttime, _tracefile _starttime = os.times()[4] # TODO: If the file exists and is too large, rename it to .old; # must handle failures of this because we can't rename an open # file on Windows. trace_fname = os.path.join(os.path.expanduser('~/.bzr.log')) # buffering=1 means line buffered _tracefile = codecs.open(trace_fname, 'at', 'utf8', buffering=1) t = _tracefile if os.fstat(t.fileno())[stat.ST_SIZE] == 0: t.write("\nthis is a debug log for diagnosing/reporting problems in bzr\n") t.write("you can delete or truncate this file, or include sections in\n") t.write("bug reports to bazaar-ng@lists.canonical.com\n\n") # TODO: If we failed to create the file, perhaps give a warning # but don't abort; send things to /dev/null instead? _write_trace('bzr %s invoked on python %s (%s)' % (bzrlib.__version__, '.'.join(map(str, sys.version_info)), sys.platform)) _write_trace(' arguments: %r' % argv) _write_trace(' working dir: ' + os.getcwdu()) def close_trace(): times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum, %.3f elapsed" % (times[:4] + ((times[4] - _starttime),))) commit refs/heads/master mark :261 committer Martin Pool 1113533412 +1000 data 27 - auto-rollover of .bzr.log from :260 M 644 inline NEWS data 3655 bzr-0.0.4 NOT RELEASED YET ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/trace.py data 4081 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os, time, socket, stat, codecs import bzrlib ###################################################################### # messages and logging ## TODO: If --verbose is given then write to both stderr and ## _tracefile; perhaps replace _tracefile with a tee thing. global _tracefile, _starttime _tracefile = None # used to have % (os.environ['USER'], time.time(), os.getpid()), 'w') _starttime = None # If false, notes also go to stdout; should replace this with --silent # at some point. silent = False # TODO: Somehow tie this to the --verbose option? verbose = False # fix this if we ever fork within python _mypid = os.getpid() _logprefix = '[%d] ' % _mypid def _write_trace(msg): _tracefile.write(_logprefix + msg + '\n') def warning(msg): sys.stderr.write('bzr: warning: ' + msg + '\n') _write_trace('warning: ' + msg) mutter = _write_trace def note(msg): b = '* ' + str(msg) + '\n' if not silent: sys.stderr.write(b) _write_trace('note: ' + msg) def log_error(msg): sys.stderr.write(msg + '\n') _write_trace(msg) # TODO: Something to log exceptions in here. def _rollover_trace_maybe(trace_fname): try: size = os.stat(trace_fname)[stat.ST_SIZE] if size <= 100000: return old_fname = trace_fname + '.old' try: # must remove before rename on windows os.remove(old_fname) except OSError: pass try: # might fail if in use on windows os.rename(trace_fname, old_fname) except OSError: pass except OSError: return def create_tracefile(argv): # TODO: Also show contents of /etc/lsb-release, if it can be parsed. # Perhaps that should eventually go into the platform library? # TODO: If the file doesn't exist, add a note describing it. # Messages are always written to here, so that we have some # information if something goes wrong. In a future version this # file will be removed on successful completion. global _starttime, _tracefile _starttime = os.times()[4] trace_fname = os.path.join(os.path.expanduser('~/.bzr.log')) _rollover_trace_maybe(trace_fname) # buffering=1 means line buffered _tracefile = codecs.open(trace_fname, 'at', 'utf8', buffering=1) t = _tracefile if os.fstat(t.fileno())[stat.ST_SIZE] == 0: t.write("\nthis is a debug log for diagnosing/reporting problems in bzr\n") t.write("you can delete or truncate this file, or include sections in\n") t.write("bug reports to bazaar-ng@lists.canonical.com\n\n") # TODO: If we failed to create the file, perhaps give a warning # but don't abort; send things to /dev/null instead? _write_trace('bzr %s invoked on python %s (%s)' % (bzrlib.__version__, '.'.join(map(str, sys.version_info)), sys.platform)) _write_trace(' arguments: %r' % argv) _write_trace(' working dir: ' + os.getcwdu()) def close_trace(): times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum, %.3f elapsed" % (times[:4] + ((times[4] - _starttime),))) commit refs/heads/master mark :262 committer Martin Pool 1113533453 +1000 data 91 - gen_file_id: break the file on either / or \ when looking for a safe prefix of the name from :261 M 644 inline NEWS data 3748 bzr-0.0.4 NOT RELEASED YET ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/branch.py data 34853 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. TODO: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. TODO: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. TODO: Keep the on-disk branch locked while the object exists. TODO: mkdir() method. """ def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) self.append_revision(rev_id) if verbose: note("commited r%d" % self.revno()) def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history', 'r').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] else: return None def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original', verbose=False): """Write out human-readable log of commits to this branch utc -- If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l if verbose == True and precursor != None: print 'changed files:' tree = self.revision_tree(p) prevtree = self.revision_tree(precursor) for file_state, fid, old_name, new_name, kind in \ diff_trees(prevtree, tree, ): if file_state == 'A' or file_state == 'M': show_status(file_state, kind, new_name) elif file_state == 'D': show_status(file_state, kind, old_name) elif file_state == 'R': show_status(file_state, kind, old_name + ' => ' + new_name) revno += 1 precursor = p def rename_one(self, from_rel, to_rel): tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(self, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo TODO: Get state for single files. TODO: Perhaps show a slash at the end of directory names. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = self.basis_tree() new = self.working_tree() for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("weird file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" try: shutil.rmtree(self.base) except OSError: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :263 committer Martin Pool 1113533700 +1000 data 33 factor out code to log exceptions from :262 M 644 inline bzrlib/commands.py data 30226 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, time, types, shutil, tempfile, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and new_name not in file_list: continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_dump_text_inventory(): import bzrlib.textinv inv = Branch('.').basis_tree().inventory bzrlib.textinv.write_text_inventory(inv, sys.stdout) def cmd_load_text_inventory(): import bzrlib.textinv inv = bzrlib.textinv.read_text_inventory(sys.stdin) print 'loaded %d entries' % len(inv) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original', verbose=False): """Show log of this branch. TODO: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone, verbose=verbose) def cmd_ls(revision=None, verbose=False): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_whoami(): """Show bzr user id. TODO: Command to show only the email-address part as parsed out. """ print bzrlib.osutils.username() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone', 'verbose'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[str(k.replace('-', '_'))] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: return cmd_handler(**cmdargs) or 0 def main(argv): bzrlib.trace.create_tracefile(argv) try: try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0]) if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h) bzrlib.trace.log_exception(e) sys.stderr.write('(see ~/.bzr.log for debug information)\n') return 1 except Exception, e: log_error('bzr: exception: %s' % str(e).rstrip('\n')) sys.stderr.write('(see $HOME/.bzr.log for debug information)\n') bzrlib.trace.log_exception(e) return 1 finally: bzrlib.trace.close_trace() ## TODO: Trap AssertionError # TODO: Maybe nicer handling of IOError especially for broken pipe. if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') M 644 inline bzrlib/trace.py data 4171 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os, time, socket, stat, codecs import bzrlib ###################################################################### # messages and logging ## TODO: If --verbose is given then write to both stderr and ## _tracefile; perhaps replace _tracefile with a tee thing. global _tracefile, _starttime _tracefile = None # used to have % (os.environ['USER'], time.time(), os.getpid()), 'w') _starttime = None # If false, notes also go to stdout; should replace this with --silent # at some point. silent = False # TODO: Somehow tie this to the --verbose option? verbose = False # fix this if we ever fork within python _mypid = os.getpid() _logprefix = '[%d] ' % _mypid def _write_trace(msg): _tracefile.write(_logprefix + msg + '\n') def warning(msg): sys.stderr.write('bzr: warning: ' + msg + '\n') _write_trace('warning: ' + msg) mutter = _write_trace def note(msg): b = '* ' + str(msg) + '\n' if not silent: sys.stderr.write(b) _write_trace('note: ' + msg) def log_error(msg): sys.stderr.write(msg + '\n') _write_trace(msg) # TODO: Something to log exceptions in here. def _rollover_trace_maybe(trace_fname): try: size = os.stat(trace_fname)[stat.ST_SIZE] if size <= 100000: return old_fname = trace_fname + '.old' try: # must remove before rename on windows os.remove(old_fname) except OSError: pass try: # might fail if in use on windows os.rename(trace_fname, old_fname) except OSError: pass except OSError: return def create_tracefile(argv): # TODO: Also show contents of /etc/lsb-release, if it can be parsed. # Perhaps that should eventually go into the platform library? # TODO: If the file doesn't exist, add a note describing it. # Messages are always written to here, so that we have some # information if something goes wrong. In a future version this # file will be removed on successful completion. global _starttime, _tracefile _starttime = os.times()[4] trace_fname = os.path.join(os.path.expanduser('~/.bzr.log')) _rollover_trace_maybe(trace_fname) # buffering=1 means line buffered _tracefile = codecs.open(trace_fname, 'at', 'utf8', buffering=1) t = _tracefile if os.fstat(t.fileno())[stat.ST_SIZE] == 0: t.write("\nthis is a debug log for diagnosing/reporting problems in bzr\n") t.write("you can delete or truncate this file, or include sections in\n") t.write("bug reports to bazaar-ng@lists.canonical.com\n\n") # TODO: If we failed to create the file, perhaps give a warning # but don't abort; send things to /dev/null instead? _write_trace('bzr %s invoked on python %s (%s)' % (bzrlib.__version__, '.'.join(map(str, sys.version_info)), sys.platform)) _write_trace(' arguments: %r' % argv) _write_trace(' working dir: ' + os.getcwdu()) def close_trace(): times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum, %.3f elapsed" % (times[:4] + ((times[4] - _starttime),))) def log_exception(e): import traceback traceback.print_exc(None, _tracefile) commit refs/heads/master mark :264 committer Martin Pool 1113533846 +1000 data 38 parse_args: option names must be ascii from :263 M 644 inline bzrlib/commands.py data 30296 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, time, types, shutil, tempfile, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and new_name not in file_list: continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_dump_text_inventory(): import bzrlib.textinv inv = Branch('.').basis_tree().inventory bzrlib.textinv.write_text_inventory(inv, sys.stdout) def cmd_load_text_inventory(): import bzrlib.textinv inv = bzrlib.textinv.read_text_inventory(sys.stdin) print 'loaded %d entries' % len(inv) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original', verbose=False): """Show log of this branch. TODO: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone, verbose=verbose) def cmd_ls(revision=None, verbose=False): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_whoami(): """Show bzr user id. TODO: Command to show only the email-address part as parsed out. """ print bzrlib.osutils.username() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone', 'verbose'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = args.pop(0) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[str(k.replace('-', '_'))] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: return cmd_handler(**cmdargs) or 0 def main(argv): bzrlib.trace.create_tracefile(argv) try: try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0]) if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h) bzrlib.trace.log_exception(e) sys.stderr.write('(see ~/.bzr.log for debug information)\n') return 1 except Exception, e: log_error('bzr: exception: %s' % str(e).rstrip('\n')) sys.stderr.write('(see $HOME/.bzr.log for debug information)\n') bzrlib.trace.log_exception(e) return 1 finally: bzrlib.trace.close_trace() ## TODO: Trap AssertionError # TODO: Maybe nicer handling of IOError especially for broken pipe. if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :265 committer Martin Pool 1113533913 +1000 data 44 parse_args: command names must also be ascii from :264 M 644 inline bzrlib/commands.py data 30296 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, time, types, shutil, tempfile, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and new_name not in file_list: continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_dump_text_inventory(): import bzrlib.textinv inv = Branch('.').basis_tree().inventory bzrlib.textinv.write_text_inventory(inv, sys.stdout) def cmd_load_text_inventory(): import bzrlib.textinv inv = bzrlib.textinv.read_text_inventory(sys.stdin) print 'loaded %d entries' % len(inv) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original', verbose=False): """Show log of this branch. TODO: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone, verbose=verbose) def cmd_ls(revision=None, verbose=False): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_whoami(): """Show bzr user id. TODO: Command to show only the email-address part as parsed out. """ print bzrlib.osutils.username() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone', 'verbose'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: return cmd_handler(**cmdargs) or 0 def main(argv): bzrlib.trace.create_tracefile(argv) try: try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0]) if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h) bzrlib.trace.log_exception(e) sys.stderr.write('(see ~/.bzr.log for debug information)\n') return 1 except Exception, e: log_error('bzr: exception: %s' % str(e).rstrip('\n')) sys.stderr.write('(see $HOME/.bzr.log for debug information)\n') bzrlib.trace.log_exception(e) return 1 finally: bzrlib.trace.close_trace() ## TODO: Trap AssertionError # TODO: Maybe nicer handling of IOError especially for broken pipe. if __name__ == '__main__': sys.exit(main(sys.argv)) ##import profile ##profile.run('main(sys.argv)') commit refs/heads/master mark :266 committer Martin Pool 1113534093 +1000 data 9 - cleanup from :265 M 644 inline bzrlib/commands.py data 30239 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, time, types, shutil, tempfile, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and new_name not in file_list: continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_dump_text_inventory(): import bzrlib.textinv inv = Branch('.').basis_tree().inventory bzrlib.textinv.write_text_inventory(inv, sys.stdout) def cmd_load_text_inventory(): import bzrlib.textinv inv = bzrlib.textinv.read_text_inventory(sys.stdin) print 'loaded %d entries' % len(inv) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original', verbose=False): """Show log of this branch. TODO: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone, verbose=verbose) def cmd_ls(revision=None, verbose=False): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_whoami(): """Show bzr user id. TODO: Command to show only the email-address part as parsed out. """ print bzrlib.osutils.username() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone', 'verbose'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: return cmd_handler(**cmdargs) or 0 def main(argv): bzrlib.trace.create_tracefile(argv) try: try: ret = run_bzr(argv) return ret except BzrError, e: log_error('bzr: error: ' + e.args[0]) if len(e.args) > 1: for h in e.args[1]: log_error(' ' + h) bzrlib.trace.log_exception(e) sys.stderr.write('(see ~/.bzr.log for debug information)\n') return 1 except Exception, e: log_error('bzr: exception: %s' % str(e).rstrip('\n')) sys.stderr.write('(see $HOME/.bzr.log for debug information)\n') bzrlib.trace.log_exception(e) return 1 finally: bzrlib.trace.close_trace() ## TODO: Trap AssertionError # TODO: Maybe nicer handling of IOError especially for broken pipe. if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :267 committer Martin Pool 1113535709 +1000 data 58 - better reporting of errors - handle assertions specially from :266 M 644 inline bzrlib/commands.py data 30614 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, time, types, shutil, tempfile, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and new_name not in file_list: continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_dump_text_inventory(): import bzrlib.textinv inv = Branch('.').basis_tree().inventory bzrlib.textinv.write_text_inventory(inv, sys.stdout) def cmd_load_text_inventory(): import bzrlib.textinv inv = bzrlib.textinv.read_text_inventory(sys.stdin) print 'loaded %d entries' % len(inv) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original', verbose=False): """Show log of this branch. TODO: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone, verbose=verbose) def cmd_ls(revision=None, verbose=False): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_whoami(): """Show bzr user id. TODO: Command to show only the email-address part as parsed out. """ print bzrlib.osutils.username() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone', 'verbose'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: return cmd_handler(**cmdargs) or 0 def _report_exception(e, summary): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception(e) tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb, 1) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[0][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def cmd_assert_fail(): assert False def main(argv): bzrlib.trace.create_tracefile(argv) try: try: ret = run_bzr(argv) return ret except BzrError, e: _report_exception(e, 'error: ' + e.args[0]) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(e, msg) except Exception, e: _report_exception(e, 'exception: %s' % str(e).rstrip('\n')) return 1 finally: bzrlib.trace.close_trace() ## TODO: Trap AssertionError # TODO: Maybe nicer handling of IOError especially for broken pipe. if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :268 committer Martin Pool 1113535739 +1000 data 35 - more tests for assertion failures from :267 M 644 inline bzrlib/commands.py data 30630 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, time, types, shutil, tempfile, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and new_name not in file_list: continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_dump_text_inventory(): import bzrlib.textinv inv = Branch('.').basis_tree().inventory bzrlib.textinv.write_text_inventory(inv, sys.stdout) def cmd_load_text_inventory(): import bzrlib.textinv inv = bzrlib.textinv.read_text_inventory(sys.stdin) print 'loaded %d entries' % len(inv) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original', verbose=False): """Show log of this branch. TODO: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone, verbose=verbose) def cmd_ls(revision=None, verbose=False): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_whoami(): """Show bzr user id. TODO: Command to show only the email-address part as parsed out. """ print bzrlib.osutils.username() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone', 'verbose'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: return cmd_handler(**cmdargs) or 0 def _report_exception(e, summary): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception(e) tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb, 1) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[0][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def cmd_assert_fail(): assert False, "always fails" def main(argv): bzrlib.trace.create_tracefile(argv) try: try: ret = run_bzr(argv) return ret except BzrError, e: _report_exception(e, 'error: ' + e.args[0]) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(e, msg) except Exception, e: _report_exception(e, 'exception: %s' % str(e).rstrip('\n')) return 1 finally: bzrlib.trace.close_trace() ## TODO: Trap AssertionError # TODO: Maybe nicer handling of IOError especially for broken pipe. if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :269 committer Martin Pool 1113535952 +1000 data 56 - Put proper prefix at start of exceptions in trace file from :268 M 644 inline bzrlib/trace.py data 4274 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os, time, socket, stat, codecs import bzrlib ###################################################################### # messages and logging ## TODO: If --verbose is given then write to both stderr and ## _tracefile; perhaps replace _tracefile with a tee thing. global _tracefile, _starttime _tracefile = None # used to have % (os.environ['USER'], time.time(), os.getpid()), 'w') _starttime = None # If false, notes also go to stdout; should replace this with --silent # at some point. silent = False # TODO: Somehow tie this to the --verbose option? verbose = False # fix this if we ever fork within python _mypid = os.getpid() _logprefix = '[%d] ' % _mypid def _write_trace(msg): _tracefile.write(_logprefix + msg + '\n') def warning(msg): sys.stderr.write('bzr: warning: ' + msg + '\n') _write_trace('warning: ' + msg) mutter = _write_trace def note(msg): b = '* ' + str(msg) + '\n' if not silent: sys.stderr.write(b) _write_trace('note: ' + msg) def log_error(msg): sys.stderr.write(msg + '\n') _write_trace(msg) # TODO: Something to log exceptions in here. def _rollover_trace_maybe(trace_fname): try: size = os.stat(trace_fname)[stat.ST_SIZE] if size <= 100000: return old_fname = trace_fname + '.old' try: # must remove before rename on windows os.remove(old_fname) except OSError: pass try: # might fail if in use on windows os.rename(trace_fname, old_fname) except OSError: pass except OSError: return def create_tracefile(argv): # TODO: Also show contents of /etc/lsb-release, if it can be parsed. # Perhaps that should eventually go into the platform library? # TODO: If the file doesn't exist, add a note describing it. # Messages are always written to here, so that we have some # information if something goes wrong. In a future version this # file will be removed on successful completion. global _starttime, _tracefile _starttime = os.times()[4] trace_fname = os.path.join(os.path.expanduser('~/.bzr.log')) _rollover_trace_maybe(trace_fname) # buffering=1 means line buffered _tracefile = codecs.open(trace_fname, 'at', 'utf8', buffering=1) t = _tracefile if os.fstat(t.fileno())[stat.ST_SIZE] == 0: t.write("\nthis is a debug log for diagnosing/reporting problems in bzr\n") t.write("you can delete or truncate this file, or include sections in\n") t.write("bug reports to bazaar-ng@lists.canonical.com\n\n") # TODO: If we failed to create the file, perhaps give a warning # but don't abort; send things to /dev/null instead? _write_trace('bzr %s invoked on python %s (%s)' % (bzrlib.__version__, '.'.join(map(str, sys.version_info)), sys.platform)) _write_trace(' arguments: %r' % argv) _write_trace(' working dir: ' + os.getcwdu()) def close_trace(): times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum, %.3f elapsed" % (times[:4] + ((times[4] - _starttime),))) def log_exception(e): import traceback, cStringIO s = cStringIO.StringIO() traceback.print_exc(None, s) for l in s.getvalue().split('\n'): _write_trace(l) commit refs/heads/master mark :270 committer Martin Pool 1113535998 +1000 data 27 - Allow log files up to 1MB from :269 M 644 inline bzrlib/trace.py data 4275 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os, time, socket, stat, codecs import bzrlib ###################################################################### # messages and logging ## TODO: If --verbose is given then write to both stderr and ## _tracefile; perhaps replace _tracefile with a tee thing. global _tracefile, _starttime _tracefile = None # used to have % (os.environ['USER'], time.time(), os.getpid()), 'w') _starttime = None # If false, notes also go to stdout; should replace this with --silent # at some point. silent = False # TODO: Somehow tie this to the --verbose option? verbose = False # fix this if we ever fork within python _mypid = os.getpid() _logprefix = '[%d] ' % _mypid def _write_trace(msg): _tracefile.write(_logprefix + msg + '\n') def warning(msg): sys.stderr.write('bzr: warning: ' + msg + '\n') _write_trace('warning: ' + msg) mutter = _write_trace def note(msg): b = '* ' + str(msg) + '\n' if not silent: sys.stderr.write(b) _write_trace('note: ' + msg) def log_error(msg): sys.stderr.write(msg + '\n') _write_trace(msg) # TODO: Something to log exceptions in here. def _rollover_trace_maybe(trace_fname): try: size = os.stat(trace_fname)[stat.ST_SIZE] if size <= 1 << 20: return old_fname = trace_fname + '.old' try: # must remove before rename on windows os.remove(old_fname) except OSError: pass try: # might fail if in use on windows os.rename(trace_fname, old_fname) except OSError: pass except OSError: return def create_tracefile(argv): # TODO: Also show contents of /etc/lsb-release, if it can be parsed. # Perhaps that should eventually go into the platform library? # TODO: If the file doesn't exist, add a note describing it. # Messages are always written to here, so that we have some # information if something goes wrong. In a future version this # file will be removed on successful completion. global _starttime, _tracefile _starttime = os.times()[4] trace_fname = os.path.join(os.path.expanduser('~/.bzr.log')) _rollover_trace_maybe(trace_fname) # buffering=1 means line buffered _tracefile = codecs.open(trace_fname, 'at', 'utf8', buffering=1) t = _tracefile if os.fstat(t.fileno())[stat.ST_SIZE] == 0: t.write("\nthis is a debug log for diagnosing/reporting problems in bzr\n") t.write("you can delete or truncate this file, or include sections in\n") t.write("bug reports to bazaar-ng@lists.canonical.com\n\n") # TODO: If we failed to create the file, perhaps give a warning # but don't abort; send things to /dev/null instead? _write_trace('bzr %s invoked on python %s (%s)' % (bzrlib.__version__, '.'.join(map(str, sys.version_info)), sys.platform)) _write_trace(' arguments: %r' % argv) _write_trace(' working dir: ' + os.getcwdu()) def close_trace(): times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum, %.3f elapsed" % (times[:4] + ((times[4] - _starttime),))) def log_exception(e): import traceback, cStringIO s = cStringIO.StringIO() traceback.print_exc(None, s) for l in s.getvalue().split('\n'): _write_trace(l) commit refs/heads/master mark :271 committer Martin Pool 1113551639 +1000 data 20 - Windows path fixes from :270 M 644 inline bzrlib/commands.py data 30705 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, time, types, shutil, tempfile, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. if file_list: file_list = [b.relpath(f) for f in file_list] # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and (new_name not in file_list): continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_dump_text_inventory(): import bzrlib.textinv inv = Branch('.').basis_tree().inventory bzrlib.textinv.write_text_inventory(inv, sys.stdout) def cmd_load_text_inventory(): import bzrlib.textinv inv = bzrlib.textinv.read_text_inventory(sys.stdin) print 'loaded %d entries' % len(inv) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original', verbose=False): """Show log of this branch. TODO: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone, verbose=verbose) def cmd_ls(revision=None, verbose=False): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_whoami(): """Show bzr user id. TODO: Command to show only the email-address part as parsed out. """ print bzrlib.osutils.username() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? try: cmdfn = globals()['cmd_' + topic.replace('-', '_')] except KeyError: bailout("no help for %r" % topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone', 'verbose'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: bailout("unknown command " + `cmd`) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: return cmd_handler(**cmdargs) or 0 def _report_exception(e, summary): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception(e) tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb, 1) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[0][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def cmd_assert_fail(): assert False, "always fails" def main(argv): bzrlib.trace.create_tracefile(argv) try: try: ret = run_bzr(argv) return ret except BzrError, e: _report_exception(e, 'error: ' + e.args[0]) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(e, msg) except Exception, e: _report_exception(e, 'exception: %s' % str(e).rstrip('\n')) return 1 finally: bzrlib.trace.close_trace() ## TODO: Trap AssertionError # TODO: Maybe nicer handling of IOError especially for broken pipe. if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/inventory.py data 19282 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # TODO: Maybe store inventory_id in the file? Not really needed. # This should really be an id randomly assigned when the tree is # created, but it's not for now. ROOT_ID = "TREE_ROOT" import sys, os.path, types, re from sets import Set try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from xml import XMLMixin from errors import bailout, BzrError import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') 'TREE_ROOT' >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID)) >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123')) >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' TODO: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ # TODO: split InventoryEntry into subclasses for files, # directories, etc etc. def __init__(self, file_id, name, kind, parent_id, text_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID) >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID) Traceback (most recent call last): BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", []) """ if len(splitpath(name)) != 1: bailout('InventoryEntry name is not a simple filename: %r' % name) self.file_id = file_id self.name = name self.kind = kind self.text_id = text_id self.parent_id = parent_id self.text_sha1 = None self.text_size = None if kind == 'directory': self.children = {} elif kind == 'file': pass else: raise BzrError("unhandled entry kind %r" % kind) def sorted_children(self): l = self.children.items() l.sort() return l def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.parent_id, text_id=self.text_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size != None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1']: v = getattr(self, f) if v != None: e.set(f, v) # to be conservative, we don't externalize the root pointers # for now, leaving them as null in the xml form. in a future # version it will be implied by nested elements. if self.parent_id != ROOT_ID: assert isinstance(self.parent_id, basestring) e.set('parent_id', self.parent_id) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' ## original format inventories don't have a parent_id for ## nodes in the root directory, but it's cleaner to use one ## internally. parent_id = elt.get('parent_id') if parent_id == None: parent_id = ROOT_ID self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __cmp__(self, other): if self is other: return 0 if not isinstance(other, InventoryEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.name, other.name) \ or cmp(self.text_sha1, other.text_sha1) \ or cmp(self.text_size, other.text_size) \ or cmp(self.text_id, other.text_id) \ or cmp(self.parent_id, other.parent_id) \ or cmp(self.kind, other.kind) class RootEntry(InventoryEntry): def __init__(self, file_id): self.file_id = file_id self.children = {} self.kind = 'root_directory' self.parent_id = None self.name = '' def __cmp__(self, other): if self is other: return 0 if not isinstance(other, RootEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.children, other.children) class Inventory(XMLMixin): """Inventory of versioned files in a tree. This describes which file_id is present at each point in the tree, and possibly the SHA-1 or other information about the file. Entries can be looked up either by path or by file_id. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted, other than through the Inventory API. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID)) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ ## TODO: Make sure only canonical filenames are stored. ## TODO: Do something sensible about the possible collisions on ## case-losing filesystems. Perhaps we should just always forbid ## such collisions. ## TODO: No special cases for root, rather just give it a file id ## like everything else. ## TODO: Probably change XML serialization to use nesting rather ## than parent_id pointers. ## TODO: Perhaps hold the ElementTree in memory and work directly ## on that rather than converting into Python objects every time? def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. The inventory is created with a default root directory, with an id of None. """ self.root = RootEntry(ROOT_ID) self._byid = {self.root.file_id: self.root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, from_dir=None): """Return (path, entry) pairs, in order by name.""" if from_dir == None: assert self.root from_dir = self.root elif isinstance(from_dir, basestring): from_dir = self._byid[from_dir] kids = from_dir.children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(from_dir=ie.file_id): yield os.path.join(name, cn), cie def directories(self): """Return (path, entry) pairs for all directories. """ def descend(parent_ie): parent_name = parent_ie.name yield parent_name, parent_ie # directory children in sorted order dn = [] for ie in parent_ie.children.itervalues(): if ie.kind == 'directory': dn.append((ie.name, ie)) dn.sort() for name, child_ie in dn: for sub_name, sub_ie in descend(child_ie): yield appendpath(parent_name, sub_name), sub_ie for name, ie in descend(self.root): yield name, ie def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID)) >>> inv['123123'].name 'hello.c' """ if file_id == None: raise BzrError("can't look up file_id None") try: return self._byid[file_id] except KeyError: raise BzrError("file_id {%s} not in inventory" % file_id) def get_child(self, parent_id, filename): return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self._byid: bailout("inventory already contains entry with id {%s}" % entry.file_id) try: parent = self._byid[entry.parent_id] except KeyError: bailout("parent_id {%s} not in inventory" % entry.parent_id) if parent.children.has_key(entry.name): bailout("%s is already versioned" % appendpath(self.id2path(parent.file_id), entry.name)) self._byid[entry.file_id] = entry parent.children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: bailout("cannot re-add root of inventory") if file_id == None: file_id = bzrlib.branch.gen_file_id(relpath) parent_id = self.path2id(parts[:-1]) assert parent_id != None ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def id_set(self): return Set(self._byid) def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID)) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __cmp__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 True """ if self is other: return 0 if not isinstance(other, Inventory): return NotImplemented if self.id_set() ^ other.id_set(): return 1 for file_id in self._byid: c = cmp(self[file_id], other[file_id]) if c: return c return 0 def get_idpath(self, file_id): """Return a list of file_ids for the path to an entry. The list contains one element for each directory followed by the id of the file itself. So the length of the returned list is equal to the depth of the file in the tree, counting the root directory as depth 1. """ p = [] while file_id != None: try: ie = self._byid[file_id] except KeyError: bailout("file_id {%s} not found in inventory" % file_id) p.insert(0, ie.file_id) file_id = ie.parent_id return p def id2path(self, file_id): """Return as a list the path to file_id.""" # get all names, skipping root p = [self[fid].name for fid in self.get_idpath(file_id)[1:]] return os.sep.join(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. Returns None iff the path is not found. """ if isinstance(name, types.StringTypes): name = splitpath(name) mutter("lookup path %r" % name) parent = self.root for f in name: try: cie = parent.children[f] assert cie.name == f assert cie.parent_id == parent.file_id parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): return self._byid.has_key(file_id) def rename(self, file_id, new_parent_id, new_name): """Move a file within the inventory. This can change either the name, or the parent, or both. This does not move the working file.""" if not is_valid_name(new_name): bailout("not an acceptable filename: %r" % new_name) new_parent = self._byid[new_parent_id] if new_name in new_parent.children: bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id))) new_parent_idpath = self.get_idpath(new_parent_id) if file_id in new_parent_idpath: bailout("cannot move directory %r into a subdirectory of itself, %r" % (self.id2path(file_id), self.id2path(new_parent_id))) file_ie = self._byid[file_id] old_parent = self._byid[file_ie.parent_id] # TODO: Don't leave things messed up if this fails del old_parent.children[file_ie.name] new_parent.children[new_name] = file_ie file_ie.name = new_name file_ie.parent_id = new_parent_id _NAME_RE = re.compile(r'^[^/\\]+$') def is_valid_name(name): return bool(_NAME_RE.match(name)) M 644 inline bzrlib/osutils.py data 8876 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, types, re, time, errno from stat import S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE from errors import bailout, BzrError from trace import mutter import bzrlib def make_readonly(filename): """Make a filename read-only.""" # TODO: probably needs to be fixed for windows mod = os.stat(filename).st_mode mod = mod & 0777555 os.chmod(filename, mod) def make_writable(filename): mod = os.stat(filename).st_mode mod = mod | 0200 os.chmod(filename, mod) _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])') def quotefn(f): """Return shell-quoted filename""" ## We could be a bit more terse by using double-quotes etc f = _QUOTE_RE.sub(r'\\\1', f) if f[0] == '~': f[0:1] = r'\~' return f def file_kind(f): mode = os.lstat(f)[ST_MODE] if S_ISREG(mode): return 'file' elif S_ISDIR(mode): return 'directory' elif S_ISLNK(mode): return 'symlink' else: raise BzrError("can't handle file kind with mode %o of %r" % (mode, f)) def isdir(f): """True if f is an accessible directory.""" try: return S_ISDIR(os.lstat(f)[ST_MODE]) except OSError: return False def isfile(f): """True if f is a regular file.""" try: return S_ISREG(os.lstat(f)[ST_MODE]) except OSError: return False def pumpfile(fromfile, tofile): """Copy contents of one file to another.""" tofile.write(fromfile.read()) def uuid(): """Return a new UUID""" ## XXX: Could alternatively read /proc/sys/kernel/random/uuid on ## Linux, but we need something portable for other systems; ## preferably an implementation in Python. try: return chomp(file('/proc/sys/kernel/random/uuid').readline()) except IOError: return chomp(os.popen('uuidgen').readline()) def chomp(s): if s and (s[-1] == '\n'): return s[:-1] else: return s def sha_file(f): import sha ## TODO: Maybe read in chunks to handle big files if hasattr(f, 'tell'): assert f.tell() == 0 s = sha.new() s.update(f.read()) return s.hexdigest() def sha_string(f): import sha s = sha.new() s.update(f) return s.hexdigest() def fingerprint_file(f): import sha s = sha.new() b = f.read() s.update(b) size = len(b) return {'size': size, 'sha1': s.hexdigest()} def config_dir(): """Return per-user configuration directory. By default this is ~/.bzr.conf/ TODO: Global option --config-dir to override this. """ return os.path.expanduser("~/.bzr.conf") def _auto_user_id(): """Calculate automatic user identification. Returns (realname, email). Only used when none is set in the environment or the id file. This previously used the FQDN as the default domain, but that can be very slow on machines where DNS is broken. So now we simply use the hostname. """ import socket # XXX: Any good way to get real user name on win32? try: import pwd uid = os.getuid() w = pwd.getpwuid(uid) gecos = w.pw_gecos.decode(bzrlib.user_encoding) username = w.pw_name.decode(bzrlib.user_encoding) comma = gecos.find(',') if comma == -1: realname = gecos else: realname = gecos[:comma] if not realname: realname = username except ImportError: import getpass realname = username = getpass.getuser().decode(bzrlib.user_encoding) return realname, (username + '@' + socket.gethostname()) def _get_user_id(): """Return the full user id from a file or environment variable. TODO: Allow taking this from a file in the branch directory too for per-branch ids.""" v = os.environ.get('BZREMAIL') if v: return v.decode(bzrlib.user_encoding) try: return (open(os.path.join(config_dir(), "email")) .read() .decode(bzrlib.user_encoding) .rstrip("\r\n")) except IOError, e: if e.errno != errno.ENOENT: raise e v = os.environ.get('EMAIL') if v: return v.decode(bzrlib.user_encoding) else: return None def username(): """Return email-style username. Something similar to 'Martin Pool ' TODO: Check it's reasonably well-formed. """ v = _get_user_id() if v: return v name, email = _auto_user_id() if name: return '%s <%s>' % (name, email) else: return email _EMAIL_RE = re.compile(r'[\w+.-]+@[\w+.-]+') def user_email(): """Return just the email component of a username.""" e = _get_user_id() if e: m = _EMAIL_RE.search(e) if not m: bailout("%r doesn't seem to contain a reasonable email address" % e) return m.group(0) return _auto_user_id()[1] def compare_files(a, b): """Returns true if equal in contents""" # TODO: don't read the whole thing in one go. BUFSIZE = 4096 while True: ai = a.read(BUFSIZE) bi = b.read(BUFSIZE) if ai != bi: return False if ai == '': return True def local_time_offset(t=None): """Return offset of local zone from GMT, either at present or at time t.""" # python2.3 localtime() can't take None if t == None: t = time.time() if time.localtime(t).tm_isdst and time.daylight: return -time.altzone else: return -time.timezone def format_date(t, offset=0, timezone='original'): ## TODO: Perhaps a global option to use either universal or local time? ## Or perhaps just let people set $TZ? assert isinstance(t, float) if timezone == 'utc': tt = time.gmtime(t) offset = 0 elif timezone == 'original': if offset == None: offset = 0 tt = time.gmtime(t + offset) elif timezone == 'local': tt = time.localtime(t) offset = local_time_offset(t) else: bailout("unsupported timezone format %r", ['options are "utc", "original", "local"']) return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt) + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)) def compact_date(when): return time.strftime('%Y%m%d%H%M%S', time.gmtime(when)) def filesize(f): """Return size of given open file.""" return os.fstat(f.fileno())[ST_SIZE] if hasattr(os, 'urandom'): # python 2.4 and later rand_bytes = os.urandom else: # FIXME: No good on non-Linux _rand_file = file('/dev/urandom', 'rb') rand_bytes = _rand_file.read ## TODO: We could later have path objects that remember their list ## decomposition (might be too tricksy though.) def splitpath(p): """Turn string into list of parts. >>> splitpath('a') ['a'] >>> splitpath('a/b') ['a', 'b'] >>> splitpath('a/./b') ['a', 'b'] >>> splitpath('a/.b') ['a', '.b'] >>> splitpath('a/../b') Traceback (most recent call last): ... BzrError: ("sorry, '..' not allowed in path", []) """ assert isinstance(p, types.StringTypes) # split on either delimiter because people might use either on # Windows ps = re.split(r'[\\/]', p) rps = [] for f in ps: if f == '..': bailout("sorry, %r not allowed in path" % f) elif (f == '.') or (f == ''): pass else: rps.append(f) return rps def joinpath(p): assert isinstance(p, list) for f in p: if (f == '..') or (f == None) or (f == ''): bailout("sorry, %r not allowed in path" % f) return os.path.join(*p) def appendpath(p1, p2): if p1 == '': return p2 else: return os.path.join(p1, p2) def extern_command(cmd, ignore_errors = False): mutter('external command: %s' % `cmd`) if os.system(cmd): if not ignore_errors: bailout('command failed') M 644 inline bzrlib/textui.py data 1053 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def show_status(state, kind, name): if kind == 'directory': # use this even on windows? kind_ch = '/' else: assert kind == 'file' kind_ch = '' assert len(state) == 1 print state + ' ' + name + kind_ch M 644 inline bzrlib/tree.py data 14627 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tree classes, representing directory at point in time. """ from sets import Set import os.path, os, fnmatch from osutils import pumpfile, compare_files, filesize, quotefn, sha_file, \ joinpath, splitpath, appendpath, isdir, isfile, file_kind, fingerprint_file import errno from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE from inventory import Inventory from trace import mutter, note from errors import bailout import branch import bzrlib class Tree: """Abstract file tree. There are several subclasses: * `WorkingTree` exists as files on disk editable by the user. * `RevisionTree` is a tree as recorded at some point in the past. * `EmptyTree` Trees contain an `Inventory` object, and also know how to retrieve file texts mentioned in the inventory, either from a working directory or from a store. It is possible for trees to contain files that are not described in their inventory or vice versa; for this use `filenames()`. Trees can be compared, etc, regardless of whether they are working trees or versioned trees. """ def has_filename(self, filename): """True if the tree has given filename.""" raise NotImplementedError() def has_id(self, file_id): return self.inventory.has_id(file_id) def id_set(self): """Return set of all ids in this tree.""" return self.inventory.id_set() def id2path(self, file_id): return self.inventory.id2path(file_id) def _get_inventory(self): return self._inventory inventory = property(_get_inventory, doc="Inventory of this Tree") def _check_retrieved(self, ie, f): fp = fingerprint_file(f) f.seek(0) if ie.text_size != None: if ie.text_size != fp['size']: bailout("mismatched size for file %r in %r" % (ie.file_id, self._store), ["inventory expects %d bytes" % ie.text_size, "file is actually %d bytes" % fp['size'], "store is probably damaged/corrupt"]) if ie.text_sha1 != fp['sha1']: bailout("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store), ["inventory expects %s" % ie.text_sha1, "file is actually %s" % fp['sha1'], "store is probably damaged/corrupt"]) def print_file(self, fileid): """Print file with id `fileid` to stdout.""" import sys pumpfile(self.get_file(fileid), sys.stdout) def export(self, dest): """Export this tree to a new directory. `dest` should not exist, and will be created holding the contents of this tree. TODO: To handle subdirectories we need to create the directories first. :note: If the export fails, the destination directory will be left in a half-assed state. """ os.mkdir(dest) mutter('export version %r' % self) inv = self.inventory for dp, ie in inv.iter_entries(): kind = ie.kind fullpath = appendpath(dest, dp) if kind == 'directory': os.mkdir(fullpath) elif kind == 'file': pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb')) else: bailout("don't know how to export {%s} of kind %r" % (fid, kind)) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) class WorkingTree(Tree): """Working copy tree. The inventory is held in the `Branch` working-inventory, and the files are in a directory on disk. It is possible for a `WorkingTree` to have a filename which is not listed in the Inventory and vice versa. """ def __init__(self, basedir, inv): self._inventory = inv self.basedir = basedir self.path2id = inv.path2id def __repr__(self): return "<%s of %s>" % (self.__class__.__name__, self.basedir) def abspath(self, filename): return os.path.join(self.basedir, filename) def has_filename(self, filename): return os.path.exists(self.abspath(filename)) def get_file(self, file_id): return self.get_file_byname(self.id2path(file_id)) def get_file_byname(self, filename): return file(self.abspath(filename), 'rb') def _get_store_filename(self, file_id): ## XXX: badly named; this isn't in the store at all return self.abspath(self.id2path(file_id)) def has_id(self, file_id): # files that have been deleted are excluded if not self.inventory.has_id(file_id): return False return os.access(self.abspath(self.inventory.id2path(file_id)), os.F_OK) def get_file_size(self, file_id): return os.stat(self._get_store_filename(file_id))[ST_SIZE] def get_file_sha1(self, file_id): f = self.get_file(file_id) return sha_file(f) def file_class(self, filename): if self.path2id(filename): return 'V' elif self.is_ignored(filename): return 'I' else: return '?' def list_files(self): """Recursively list all files as (path, class, kind, id). Lists, but does not descend into unversioned directories. This does not include files that have been deleted in this tree. Skips the control directory. """ inv = self.inventory def descend(from_dir_relpath, from_dir_id, dp): ls = os.listdir(dp) ls.sort() for f in ls: ## TODO: If we find a subdirectory with its own .bzr ## directory, then that is a separate tree and we ## should exclude it. if bzrlib.BZRDIR == f: continue # path within tree fp = appendpath(from_dir_relpath, f) # absolute path fap = appendpath(dp, f) f_ie = inv.get_child(from_dir_id, f) if f_ie: c = 'V' elif self.is_ignored(fp): c = 'I' else: c = '?' fk = file_kind(fap) if f_ie: if f_ie.kind != fk: bailout("file %r entered as kind %r id %r, now of kind %r" % (fap, f_ie.kind, f_ie.file_id, fk)) yield fp, c, fk, (f_ie and f_ie.file_id) if fk != 'directory': continue if c != 'V': # don't descend unversioned directories continue for ff in descend(fp, f_ie.file_id, fap): yield ff for f in descend('', inv.root.file_id, self.basedir): yield f def unknowns(self): for subp in self.extras(): if not self.is_ignored(subp): yield subp def extras(self): """Yield all unknown files in this WorkingTree. If there are any unknown directories then only the directory is returned, not all its children. But if there are unknown files under a versioned subdirectory, they are returned. Currently returned depth-first, sorted by name within directories. """ ## TODO: Work from given directory downwards for path, dir_entry in self.inventory.directories(): mutter("search for unknowns in %r" % path) dirabs = self.abspath(path) if not isdir(dirabs): # e.g. directory deleted continue fl = [] for subf in os.listdir(dirabs): if (subf != '.bzr' and (subf not in dir_entry.children)): fl.append(subf) fl.sort() for subf in fl: subp = appendpath(path, subf) yield subp def ignored_files(self): """Yield list of PATH, IGNORE_PATTERN""" for subp in self.extras(): pat = self.is_ignored(subp) if pat != None: yield subp, pat def get_ignore_list(self): """Return list of ignore patterns. Cached in the Tree object after the first call. """ if hasattr(self, '_ignorelist'): return self._ignorelist l = bzrlib.DEFAULT_IGNORE[:] if self.has_filename(bzrlib.IGNORE_FILENAME): f = self.get_file_byname(bzrlib.IGNORE_FILENAME) l.extend([line.rstrip("\n\r") for line in f.readlines()]) self._ignorelist = l return l def is_ignored(self, filename): r"""Check whether the filename matches an ignore pattern. Patterns containing '/' or '\' need to match the whole path; others match against only the last component. If the file is ignored, returns the pattern which caused it to be ignored, otherwise None. So this can simply be used as a boolean if desired.""" # TODO: Use '**' to match directories, and other extended # globbing stuff from cvs/rsync. # XXX: fnmatch is actually not quite what we want: it's only # approximately the same as real Unix fnmatch, and doesn't # treat dotfiles correctly and allows * to match /. # Eventually it should be replaced with something more # accurate. for pat in self.get_ignore_list(): if '/' in pat or '\\' in pat: # as a special case, you can put ./ at the start of a # pattern; this is good to match in the top-level # only; if (pat[:2] == './') or (pat[:2] == '.\\'): newpat = pat[2:] else: newpat = pat if fnmatch.fnmatchcase(filename, newpat): return pat else: if fnmatch.fnmatchcase(splitpath(filename)[-1], pat): return pat return None class RevisionTree(Tree): """Tree viewing a previous revision. File text can be retrieved from the text store. TODO: Some kind of `__repr__` method, but a good one probably means knowing the branch and revision number, or at least passing a description to the constructor. """ def __init__(self, store, inv): self._store = store self._inventory = inv def get_file(self, file_id): ie = self._inventory[file_id] f = self._store[ie.text_id] mutter(" get fileid{%s} from %r" % (file_id, self)) self._check_retrieved(ie, f) return f def get_file_size(self, file_id): return self._inventory[file_id].text_size def get_file_sha1(self, file_id): ie = self._inventory[file_id] return ie.text_sha1 def has_filename(self, filename): return bool(self.inventory.path2id(filename)) def list_files(self): # The only files returned by this are those from the version for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id class EmptyTree(Tree): def __init__(self): self._inventory = Inventory() def has_filename(self, filename): return False def list_files(self): if False: # just to make it a generator yield None ###################################################################### # diff # TODO: Merge these two functions into a single one that can operate # on either a whole tree or a set of files. # TODO: Return the diff in order by filename, not by category or in # random order. Can probably be done by lock-stepping through the # filenames from both trees. def file_status(filename, old_tree, new_tree): """Return single-letter status, old and new names for a file. The complexity here is in deciding how to represent renames; many complex cases are possible. """ old_inv = old_tree.inventory new_inv = new_tree.inventory new_id = new_inv.path2id(filename) old_id = old_inv.path2id(filename) if not new_id and not old_id: # easy: doesn't exist in either; not versioned at all if new_tree.is_ignored(filename): return 'I', None, None else: return '?', None, None elif new_id: # There is now a file of this name, great. pass else: # There is no longer a file of this name, but we can describe # what happened to the file that used to have # this name. There are two possibilities: either it was # deleted entirely, or renamed. assert old_id if new_inv.has_id(old_id): return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id) else: return 'D', old_inv.id2path(old_id), None # if the file_id is new in this revision, it is added if new_id and not old_inv.has_id(new_id): return 'A' # if there used to be a file of this name, but that ID has now # disappeared, it is deleted if old_id and not new_inv.has_id(old_id): return 'D' return 'wtf?' def find_renames(old_inv, new_inv): for file_id in old_inv: if file_id not in new_inv: continue old_name = old_inv.id2path(file_id) new_name = new_inv.id2path(file_id) if old_name != new_name: yield (old_name, new_name) commit refs/heads/master mark :272 committer Martin Pool 1113553612 +1000 data 21 - Add command aliases from :271 M 644 inline NEWS data 3841 bzr-0.0.4 NOT RELEASED YET ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 31013 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** Current limitation include: * Metadata format is not stable yet -- you may need to discard history in the future. * Insufficient error handling. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands:: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. """ import sys, os, time, types, shutil, tempfile, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from cmd_aliases = { '?': 'help', 'ci': 'commit', 'checkin': 'commit', 'di': 'diff', 'st': 'status', 'stat': 'status', } def get_cmd_handler(cmd): assert isinstance(cmd, str) cmd = cmd_aliases.get(cmd, cmd) try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: raise BzrError("unknown command %r" % cmd) return cmd, cmd_handler def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. if file_list: file_list = [b.relpath(f) for f in file_list] # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and (new_name not in file_list): continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_dump_text_inventory(): import bzrlib.textinv inv = Branch('.').basis_tree().inventory bzrlib.textinv.write_text_inventory(inv, sys.stdout) def cmd_load_text_inventory(): import bzrlib.textinv inv = bzrlib.textinv.read_text_inventory(sys.stdin) print 'loaded %d entries' % len(inv) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original', verbose=False): """Show log of this branch. TODO: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone, verbose=verbose) def cmd_ls(revision=None, verbose=False): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_whoami(): """Show bzr user id. TODO: Command to show only the email-address part as parsed out. """ print bzrlib.osutils.username() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ return # otherwise, maybe the name of a command? topic, cmdfn = get_cmd_handler(topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone', 'verbose'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_handler = get_cmd_handler(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(canonical_cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(canonical_cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: return cmd_handler(**cmdargs) or 0 def _report_exception(e, summary): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception(e) tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb, 1) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[0][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def cmd_assert_fail(): assert False, "always fails" def main(argv): bzrlib.trace.create_tracefile(argv) try: try: ret = run_bzr(argv) return ret except BzrError, e: _report_exception(e, 'error: ' + e.args[0]) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(e, msg) except Exception, e: _report_exception(e, 'exception: %s' % str(e).rstrip('\n')) return 1 finally: bzrlib.trace.close_trace() ## TODO: Trap AssertionError # TODO: Maybe nicer handling of IOError especially for broken pipe. if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline test.sh data 2521 #! /bin/sh -pe # Simple shell-based tests for bzr. # This is meant to exercise the external behaviour, command line # parsing and similar things and compliment the inwardly-turned # testing done by doctest. # This must already exist and be in the right place if ! [ -d bzr-test.tmp ] then echo "please create directory bzr-test.tmp" exit 1 fi echo "testing `which bzr`" bzr --version | head -n 1 echo rm -rf bzr-test.tmp mkdir bzr-test.tmp # save it for real errors exec 3>&2 exec > bzr-test.log exec 2>&1 set -x quitter() { echo "tests failed, look in bzr-test.log" >&3; exit 2; } trap quitter ERR cd bzr-test.tmp rm -rf .bzr mkdir branch1 cd branch1 # some information commands bzr help bzr version # invalid commands are detected ! bzr pants # some experiments with renames bzr init echo "hello world" > test.txt bzr unknowns # should be the only unknown file [ "`bzr unknowns`" = test.txt ] bzr status --all > status.tmp ! diff -u - status.tmp < new-in-2.txt bzr add new-in-2.txt bzr commit -m "add file to branch 2 only" [ `bzr revno` = 3 ] cd ../branch1 [ `bzr revno` = 2 ] bzr check echo "tests completed ok" >&3 commit refs/heads/master mark :273 committer Martin Pool 1113555071 +1000 data 25 - New 'bzr help commands' from :272 M 644 inline NEWS data 3909 bzr-0.0.4 NOT RELEASED YET ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 31565 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool http://bazaar-ng.org/ **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** * Metadata format is not stable yet -- you may need to discard history in the future. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. bzr check Verify history is stored safely. (for more type 'bzr help commands') """ import sys, os, time, types, shutil, tempfile, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from cmd_aliases = { '?': 'help', 'ci': 'commit', 'checkin': 'commit', 'di': 'diff', 'st': 'status', 'stat': 'status', } def get_cmd_handler(cmd): assert isinstance(cmd, str) cmd = cmd_aliases.get(cmd, cmd) try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: raise BzrError("unknown command %r" % cmd) return cmd, cmd_handler def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. if file_list: file_list = [b.relpath(f) for f in file_list] # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and (new_name not in file_list): continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_dump_text_inventory(): import bzrlib.textinv inv = Branch('.').basis_tree().inventory bzrlib.textinv.write_text_inventory(inv, sys.stdout) def cmd_load_text_inventory(): import bzrlib.textinv inv = bzrlib.textinv.read_text_inventory(sys.stdin) print 'loaded %d entries' % len(inv) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original', verbose=False): """Show log of this branch. TODO: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone, verbose=verbose) def cmd_ls(revision=None, verbose=False): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_whoami(): """Show bzr user id. usage: bzr whoami TODO: Command to show only the email-address part as parsed out. """ print bzrlib.osutils.username() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ elif topic == 'commands': help_commands() else: # otherwise, maybe the name of a command? topic, cmdfn = get_cmd_handler(topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def help_commands(): """List all commands""" accu = [] for k in globals().keys(): if k.startswith('cmd_'): accu.append(k[4:].replace('_','-')) accu.sort() print "bzr commands: " for x in accu: print " " + x print "note: some of these commands are internal-use or obsolete" # TODO: Some kind of marker for internal-use commands? # TODO: Show aliases? def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone', 'verbose'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_handler = get_cmd_handler(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(canonical_cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(canonical_cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: return cmd_handler(**cmdargs) or 0 def _report_exception(e, summary): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception(e) tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb, 1) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[0][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def cmd_assert_fail(): assert False, "always fails" def main(argv): bzrlib.trace.create_tracefile(argv) try: try: ret = run_bzr(argv) return ret except BzrError, e: _report_exception(e, 'error: ' + e.args[0]) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(e, msg) except Exception, e: _report_exception(e, 'exception: %s' % str(e).rstrip('\n')) return 1 finally: bzrlib.trace.close_trace() ## TODO: Trap AssertionError # TODO: Maybe nicer handling of IOError especially for broken pipe. if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline test.sh data 2562 #! /bin/sh -pe # Simple shell-based tests for bzr. # This is meant to exercise the external behaviour, command line # parsing and similar things and compliment the inwardly-turned # testing done by doctest. # This must already exist and be in the right place if ! [ -d bzr-test.tmp ] then echo "please create directory bzr-test.tmp" exit 1 fi echo "testing `which bzr`" bzr --version | head -n 1 echo rm -rf bzr-test.tmp mkdir bzr-test.tmp # save it for real errors exec 3>&2 exec > bzr-test.log exec 2>&1 set -x quitter() { echo "tests failed, look in bzr-test.log" >&3; exit 2; } trap quitter ERR cd bzr-test.tmp rm -rf .bzr mkdir branch1 cd branch1 # some information commands bzr help bzr version [ $(bzr help commands | wc -l) -gt 20 ] # invalid commands are detected ! bzr pants # some experiments with renames bzr init echo "hello world" > test.txt bzr unknowns # should be the only unknown file [ "`bzr unknowns`" = test.txt ] bzr status --all > status.tmp ! diff -u - status.tmp < new-in-2.txt bzr add new-in-2.txt bzr commit -m "add file to branch 2 only" [ `bzr revno` = 3 ] cd ../branch1 [ `bzr revno` = 2 ] bzr check echo "tests completed ok" >&3 commit refs/heads/master mark :274 committer Martin Pool 1113555552 +1000 data 44 - Fix 'bzr help COMMAND' for Unicode changes from :273 M 644 inline bzrlib/commands.py data 31552 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool http://bazaar-ng.org/ **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** * Metadata format is not stable yet -- you may need to discard history in the future. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. bzr check Verify history is stored safely. (for more type 'bzr help commands') """ import sys, os, time, types, shutil, tempfile, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from cmd_aliases = { '?': 'help', 'ci': 'commit', 'checkin': 'commit', 'di': 'diff', 'st': 'status', 'stat': 'status', } def get_cmd_handler(cmd): cmd = str(cmd) cmd = cmd_aliases.get(cmd, cmd) try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: raise BzrError("unknown command %r" % cmd) return cmd, cmd_handler def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. if file_list: file_list = [b.relpath(f) for f in file_list] # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and (new_name not in file_list): continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_dump_text_inventory(): import bzrlib.textinv inv = Branch('.').basis_tree().inventory bzrlib.textinv.write_text_inventory(inv, sys.stdout) def cmd_load_text_inventory(): import bzrlib.textinv inv = bzrlib.textinv.read_text_inventory(sys.stdin) print 'loaded %d entries' % len(inv) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original', verbose=False): """Show log of this branch. TODO: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone, verbose=verbose) def cmd_ls(revision=None, verbose=False): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_whoami(): """Show bzr user id. usage: bzr whoami TODO: Command to show only the email-address part as parsed out. """ print bzrlib.osutils.username() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ elif topic == 'commands': help_commands() else: # otherwise, maybe the name of a command? topic, cmdfn = get_cmd_handler(topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def help_commands(): """List all commands""" accu = [] for k in globals().keys(): if k.startswith('cmd_'): accu.append(k[4:].replace('_','-')) accu.sort() print "bzr commands: " for x in accu: print " " + x print "note: some of these commands are internal-use or obsolete" # TODO: Some kind of marker for internal-use commands? # TODO: Show aliases? def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone', 'verbose'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_handler = get_cmd_handler(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(canonical_cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(canonical_cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: return cmd_handler(**cmdargs) or 0 def _report_exception(e, summary): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception(e) tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb, 1) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[0][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def cmd_assert_fail(): assert False, "always fails" def main(argv): bzrlib.trace.create_tracefile(argv) try: try: ret = run_bzr(argv) return ret except BzrError, e: _report_exception(e, 'error: ' + e.args[0]) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(e, msg) except Exception, e: _report_exception(e, 'exception: %s' % str(e).rstrip('\n')) return 1 finally: bzrlib.trace.close_trace() ## TODO: Trap AssertionError # TODO: Maybe nicer handling of IOError especially for broken pipe. if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :275 committer Martin Pool 1113555618 +1000 data 75 - Show innermost not outermost frame location when reporting an exception from :274 M 644 inline bzrlib/commands.py data 31550 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool http://bazaar-ng.org/ **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** * Metadata format is not stable yet -- you may need to discard history in the future. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. bzr check Verify history is stored safely. (for more type 'bzr help commands') """ import sys, os, time, types, shutil, tempfile, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from cmd_aliases = { '?': 'help', 'ci': 'commit', 'checkin': 'commit', 'di': 'diff', 'st': 'status', 'stat': 'status', } def get_cmd_handler(cmd): cmd = str(cmd) cmd = cmd_aliases.get(cmd, cmd) try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: raise BzrError("unknown command %r" % cmd) return cmd, cmd_handler def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. if file_list: file_list = [b.relpath(f) for f in file_list] # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and (new_name not in file_list): continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_dump_text_inventory(): import bzrlib.textinv inv = Branch('.').basis_tree().inventory bzrlib.textinv.write_text_inventory(inv, sys.stdout) def cmd_load_text_inventory(): import bzrlib.textinv inv = bzrlib.textinv.read_text_inventory(sys.stdin) print 'loaded %d entries' % len(inv) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original', verbose=False): """Show log of this branch. TODO: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone, verbose=verbose) def cmd_ls(revision=None, verbose=False): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_whoami(): """Show bzr user id. usage: bzr whoami TODO: Command to show only the email-address part as parsed out. """ print bzrlib.osutils.username() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ elif topic == 'commands': help_commands() else: # otherwise, maybe the name of a command? topic, cmdfn = get_cmd_handler(topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def help_commands(): """List all commands""" accu = [] for k in globals().keys(): if k.startswith('cmd_'): accu.append(k[4:].replace('_','-')) accu.sort() print "bzr commands: " for x in accu: print " " + x print "note: some of these commands are internal-use or obsolete" # TODO: Some kind of marker for internal-use commands? # TODO: Show aliases? def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone', 'verbose'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_handler = get_cmd_handler(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(canonical_cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(canonical_cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: return cmd_handler(**cmdargs) or 0 def _report_exception(e, summary): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception(e) tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def cmd_assert_fail(): assert False, "always fails" def main(argv): bzrlib.trace.create_tracefile(argv) try: try: ret = run_bzr(argv) return ret except BzrError, e: _report_exception(e, 'error: ' + e.args[0]) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(e, msg) except Exception, e: _report_exception(e, 'exception: %s' % str(e).rstrip('\n')) return 1 finally: bzrlib.trace.close_trace() ## TODO: Trap AssertionError # TODO: Maybe nicer handling of IOError especially for broken pipe. if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :276 committer Martin Pool 1113555738 +1000 data 3 Doc from :275 M 644 inline bzrlib/commands.py data 31691 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool http://bazaar-ng.org/ **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** * Metadata format is not stable yet -- you may need to discard history in the future. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. bzr check Verify history is stored safely. (for more type 'bzr help commands') """ import sys, os, time, types, shutil, tempfile, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from cmd_aliases = { '?': 'help', 'ci': 'commit', 'checkin': 'commit', 'di': 'diff', 'st': 'status', 'stat': 'status', } def get_cmd_handler(cmd): cmd = str(cmd) cmd = cmd_aliases.get(cmd, cmd) try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: raise BzrError("unknown command %r" % cmd) return cmd, cmd_handler def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. if file_list: file_list = [b.relpath(f) for f in file_list] # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and (new_name not in file_list): continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if oldlines or newlines: sys.stdout.writelines(difflib.unified_diff(oldlines, newlines, **kw)) print if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_dump_text_inventory(): import bzrlib.textinv inv = Branch('.').basis_tree().inventory bzrlib.textinv.write_text_inventory(inv, sys.stdout) def cmd_load_text_inventory(): import bzrlib.textinv inv = bzrlib.textinv.read_text_inventory(sys.stdin) print 'loaded %d entries' % len(inv) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original', verbose=False): """Show log of this branch. TODO: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone, verbose=verbose) def cmd_ls(revision=None, verbose=False): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_whoami(): """Show bzr user id. usage: bzr whoami TODO: Command to show only the email-address part as parsed out. """ print bzrlib.osutils.username() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ elif topic == 'commands': help_commands() else: # otherwise, maybe the name of a command? topic, cmdfn = get_cmd_handler(topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def help_commands(): """List all commands""" accu = [] for k in globals().keys(): if k.startswith('cmd_'): accu.append(k[4:].replace('_','-')) accu.sort() print "bzr commands: " for x in accu: print " " + x print "note: some of these commands are internal-use or obsolete" # TODO: Some kind of marker for internal-use commands? # TODO: Show aliases? def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone', 'verbose'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_handler = get_cmd_handler(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(canonical_cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(canonical_cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: return cmd_handler(**cmdargs) or 0 def _report_exception(e, summary): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception(e) tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def cmd_assert_fail(): assert False, "always fails" def main(argv): bzrlib.trace.create_tracefile(argv) try: try: ret = run_bzr(argv) return ret except BzrError, e: _report_exception(e, 'error: ' + e.args[0]) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(e, msg) except Exception, e: _report_exception(e, 'exception: %s' % str(e).rstrip('\n')) return 1 finally: bzrlib.trace.close_trace() ## TODO: Trap AssertionError # TODO: Maybe nicer handling of IOError especially for broken pipe. if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :277 committer Martin Pool 1113557268 +1000 data 3 Doc from :276 M 644 inline test.sh data 2605 #! /bin/sh -pe # Simple shell-based tests for bzr. # This is meant to exercise the external behaviour, command line # parsing and similar things and compliment the inwardly-turned # testing done by doctest. # This must already exist and be in the right place if ! [ -d bzr-test.tmp ] then echo "please create directory bzr-test.tmp" exit 1 fi echo "testing `which bzr`" bzr --version | head -n 1 echo rm -rf bzr-test.tmp mkdir bzr-test.tmp # save it for real errors exec 3>&2 exec > bzr-test.log exec 2>&1 set -x quitter() { echo "tests failed, look in bzr-test.log" >&3; exit 2; } trap quitter ERR cd bzr-test.tmp rm -rf .bzr mkdir branch1 cd branch1 # some information commands bzr help bzr version [ $(bzr help commands | wc -l) -gt 20 ] # invalid commands are detected ! bzr pants # TODO: test unicode user names bzr help # some experiments with renames bzr init echo "hello world" > test.txt bzr unknowns # should be the only unknown file [ "`bzr unknowns`" = test.txt ] bzr status --all > status.tmp ! diff -u - status.tmp < new-in-2.txt bzr add new-in-2.txt bzr commit -m "add file to branch 2 only" [ `bzr revno` = 3 ] cd ../branch1 [ `bzr revno` = 2 ] bzr check echo "tests completed ok" >&3 commit refs/heads/master mark :278 committer Martin Pool 1113557608 +1000 data 50 - Better workaround for trailing newlines in diffs from :277 M 644 inline NEWS data 4007 bzr-0.0.4 NOT RELEASED YET ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 32580 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool http://bazaar-ng.org/ **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** * Metadata format is not stable yet -- you may need to discard history in the future. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. bzr check Verify history is stored safely. (for more type 'bzr help commands') """ import sys, os, time, types, shutil, tempfile, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from cmd_aliases = { '?': 'help', 'ci': 'commit', 'checkin': 'commit', 'di': 'diff', 'st': 'status', 'stat': 'status', } def get_cmd_handler(cmd): cmd = str(cmd) cmd = cmd_aliases.get(cmd, cmd) try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: raise BzrError("unknown command %r" % cmd) return cmd, cmd_handler def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. if file_list: file_list = [b.relpath(f) for f in file_list] # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and (new_name not in file_list): continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) sys.stdout.writelines(ud) if nonl: print "\\ No newline at end of file" sys.stdout.write('\n') if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_dump_text_inventory(): import bzrlib.textinv inv = Branch('.').basis_tree().inventory bzrlib.textinv.write_text_inventory(inv, sys.stdout) def cmd_load_text_inventory(): import bzrlib.textinv inv = bzrlib.textinv.read_text_inventory(sys.stdin) print 'loaded %d entries' % len(inv) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original', verbose=False): """Show log of this branch. TODO: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone, verbose=verbose) def cmd_ls(revision=None, verbose=False): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_whoami(): """Show bzr user id. usage: bzr whoami TODO: Command to show only the email-address part as parsed out. """ print bzrlib.osutils.username() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ elif topic == 'commands': help_commands() else: # otherwise, maybe the name of a command? topic, cmdfn = get_cmd_handler(topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def help_commands(): """List all commands""" accu = [] for k in globals().keys(): if k.startswith('cmd_'): accu.append(k[4:].replace('_','-')) accu.sort() print "bzr commands: " for x in accu: print " " + x print "note: some of these commands are internal-use or obsolete" # TODO: Some kind of marker for internal-use commands? # TODO: Show aliases? def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone', 'verbose'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_handler = get_cmd_handler(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(canonical_cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(canonical_cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: return cmd_handler(**cmdargs) or 0 def _report_exception(e, summary): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception(e) tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def cmd_assert_fail(): assert False, "always fails" def main(argv): bzrlib.trace.create_tracefile(argv) try: try: ret = run_bzr(argv) return ret except BzrError, e: _report_exception(e, 'error: ' + e.args[0]) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(e, msg) except Exception, e: _report_exception(e, 'exception: %s' % str(e).rstrip('\n')) return 1 finally: bzrlib.trace.close_trace() ## TODO: Trap AssertionError # TODO: Maybe nicer handling of IOError especially for broken pipe. if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :279 committer Martin Pool 1114047022 +1000 data 4 todo from :278 M 644 inline bzrlib/commands.py data 32714 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool http://bazaar-ng.org/ **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** * Metadata format is not stable yet -- you may need to discard history in the future. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. bzr check Verify history is stored safely. (for more type 'bzr help commands') """ import sys, os, time, types, shutil, tempfile, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from cmd_aliases = { '?': 'help', 'ci': 'commit', 'checkin': 'commit', 'di': 'diff', 'st': 'status', 'stat': 'status', } def get_cmd_handler(cmd): cmd = str(cmd) cmd = cmd_aliases.get(cmd, cmd) try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: raise BzrError("unknown command %r" % cmd) return cmd, cmd_handler def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. if file_list: file_list = [b.relpath(f) for f in file_list] # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and (new_name not in file_list): continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) sys.stdout.writelines(ud) if nonl: print "\\ No newline at end of file" sys.stdout.write('\n') if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_dump_text_inventory(): import bzrlib.textinv inv = Branch('.').basis_tree().inventory bzrlib.textinv.write_text_inventory(inv, sys.stdout) def cmd_load_text_inventory(): import bzrlib.textinv inv = bzrlib.textinv.read_text_inventory(sys.stdin) print 'loaded %d entries' % len(inv) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original', verbose=False): """Show log of this branch. TODO: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone, verbose=verbose) def cmd_ls(revision=None, verbose=False): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_whoami(): """Show bzr user id. usage: bzr whoami TODO: Command to show only the email-address part as parsed out. """ print bzrlib.osutils.username() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ elif topic == 'commands': help_commands() else: # otherwise, maybe the name of a command? topic, cmdfn = get_cmd_handler(topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def help_commands(): """List all commands""" accu = [] for k in globals().keys(): if k.startswith('cmd_'): accu.append(k[4:].replace('_','-')) accu.sort() print "bzr commands: " for x in accu: print " " + x print "note: some of these commands are internal-use or obsolete" # TODO: Some kind of marker for internal-use commands? # TODO: Show aliases? def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone', 'verbose'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_handler = get_cmd_handler(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(canonical_cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(canonical_cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: return cmd_handler(**cmdargs) or 0 def _report_exception(e, summary): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception(e) tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def cmd_assert_fail(): assert False, "always fails" def main(argv): bzrlib.trace.create_tracefile(argv) try: try: ret = run_bzr(argv) return ret except BzrError, e: _report_exception(e, 'error: ' + e.args[0]) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(e, msg) except Exception, e: _report_exception(e, 'exception: %s' % str(e).rstrip('\n')) return 1 finally: bzrlib.trace.close_trace() ## TODO: Trap AssertionError # TODO: Maybe nicer handling of IOError especially for broken pipe. if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :280 committer Martin Pool 1114134026 +1000 data 25 Prepare for 0.0.4 release from :279 M 644 inline NEWS data 4001 bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/__init__.py data 1585 # (C) 2005 Canonical Development Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr library""" from inventory import Inventory, InventoryEntry from branch import Branch, ScratchBranch from osutils import format_date from tree import Tree from diff import diff_trees from trace import mutter, warning import add BZRDIR = ".bzr" DEFAULT_IGNORE = ['.bzr.log', '*~', '#*#', '*$', '.#*', '*.tmp', '*.bak', '*.BAK', '*.orig', '*.o', '*.obj', '*.a', '*.py[oc]', '*.so', '*.exe', '*.elc', '{arch}', 'CVS', '.svn', '_darcs', 'SCCS', 'RCS', 'BitKeeper', 'TAGS', '.make.state', '.sconsign', '.tmp*'] IGNORE_FILENAME = ".bzrignore" import locale user_encoding = locale.getpreferredencoding() __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __version__ = '0.0.4' commit refs/heads/master mark :281 committer Martin Pool 1114424280 +1000 data 30 - List rename and move in help from :280 M 644 inline bzrlib/commands.py data 32840 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool http://bazaar-ng.org/ **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** * Metadata format is not stable yet -- you may need to discard history in the future. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr rename FROM TO Rename one file. bzr move FROM... DESTDIR Move one or more files to a different directory. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. bzr check Verify history is stored safely. (for more type 'bzr help commands') """ import sys, os, time, types, shutil, tempfile, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. ## TODO: rename command, needed soon: check destination doesn't exist ## either in working copy or tree; move working copy; update ## inventory; write out ## TODO: move command; check destination is a directory and will not ## clash; move it. ## TODO: command to show renames, one per line, as to->from cmd_aliases = { '?': 'help', 'ci': 'commit', 'checkin': 'commit', 'di': 'diff', 'st': 'status', 'stat': 'status', } def get_cmd_handler(cmd): cmd = str(cmd) cmd = cmd_aliases.get(cmd, cmd) try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: raise BzrError("unknown command %r" % cmd) return cmd, cmd_handler def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. if file_list: file_list = [b.relpath(f) for f in file_list] # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and (new_name not in file_list): continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) sys.stdout.writelines(ud) if nonl: print "\\ No newline at end of file" sys.stdout.write('\n') if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_dump_text_inventory(): import bzrlib.textinv inv = Branch('.').basis_tree().inventory bzrlib.textinv.write_text_inventory(inv, sys.stdout) def cmd_load_text_inventory(): import bzrlib.textinv inv = bzrlib.textinv.read_text_inventory(sys.stdin) print 'loaded %d entries' % len(inv) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original', verbose=False): """Show log of this branch. TODO: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone, verbose=verbose) def cmd_ls(revision=None, verbose=False): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_whoami(): """Show bzr user id. usage: bzr whoami TODO: Command to show only the email-address part as parsed out. """ print bzrlib.osutils.username() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ elif topic == 'commands': help_commands() else: # otherwise, maybe the name of a command? topic, cmdfn = get_cmd_handler(topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def help_commands(): """List all commands""" accu = [] for k in globals().keys(): if k.startswith('cmd_'): accu.append(k[4:].replace('_','-')) accu.sort() print "bzr commands: " for x in accu: print " " + x print "note: some of these commands are internal-use or obsolete" # TODO: Some kind of marker for internal-use commands? # TODO: Show aliases? def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone', 'verbose'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_handler = get_cmd_handler(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(canonical_cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(canonical_cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: return cmd_handler(**cmdargs) or 0 def _report_exception(e, summary): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception(e) tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def cmd_assert_fail(): assert False, "always fails" def main(argv): bzrlib.trace.create_tracefile(argv) try: try: ret = run_bzr(argv) return ret except BzrError, e: _report_exception(e, 'error: ' + e.args[0]) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(e, msg) except Exception, e: _report_exception(e, 'exception: %s' % str(e).rstrip('\n')) return 1 finally: bzrlib.trace.close_trace() ## TODO: Trap AssertionError # TODO: Maybe nicer handling of IOError especially for broken pipe. if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :282 committer Martin Pool 1114492817 +1000 data 33 - move all TODO items into ./TODO from :281 D doc/roadmap.txt D doc/testing.txt D doc/work-order.txt M 644 inline TODO data 1973 (See also various low-level TODOs in the source code.) Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. Medium things ------------- * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * mv command? * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. Large things ------------ * Web interface * GUI (maybe in Python GTK+?) * C library interface M 644 inline doc/index.txt data 5586 Bazaar-NG ********* .. These documents are formatted as ReStructuredText. You can .. .. convert them to HTML, PDF, etc using the ``python-docutils`` .. .. package. .. *Bazaar-NG* (``bzr``) is a project of `Canonical Ltd`__ to develop an open source distributed version control system that is powerful, friendly, and scalable. The project is at an early stage of development. __ http://canonical.com/ **Note:** These documents are in a very preliminary state, and so may be internally or externally inconsistent or redundant. Comments are still very welcome. Please send them to . For more information, see the homepage at http://bazaar-ng.org/ User documentation ------------------ * `Project overview/introduction `__ * `Command reference `__ -- intended to be user documentation, and gives the best overview at the moment of what the system will feel like to use. Fairly complete. * `Quick reference `__ -- single page description of how to use, intended to check it's adequately simple. Incomplete. * `FAQ `__ -- mostly user-oriented FAQ. Requirements and general design ------------------------------- * `Various purposes of a VCS `__ -- taking snapshots and helping with merges is not the whole story. * `Requirements `__ * `Costs `__ of various factors: time, disk, network, etc. * `Deadly sins `__ that gcc maintainers suggest we avoid. * `Overview of the whole design `__ and miscellaneous small design points. * `File formats `__ * `Random observations `__ that don't fit anywhere else yet. Design of particular features ----------------------------- * `Automatic generation of ChangeLogs `__ * `Cherry picking `__ -- merge just selected non-contiguous changes from a branch. * `Common changeset format `__ for interchange format between VCS. * `Compression `__ of file text for more efficient storage. * `Config specs `__ assemble a tree from several places. * `Conflicts `_ that can occur during merge-like operations. * `Ignored files `__ * `Recovering from interrupted operations `__ * `Inventory command `__ * `Branch joins `__ represent that all the changes from one branch are integrated into another. * `Kill a version `__ to fix a broken commit or wrong message, or to remove confidential information from the history. * `Hash collisions `__ and weaknesses, and the security implications thereof. * `Layers `__ within the design * `Library interface `__ for Python. * `Merge `__ * `Mirroring `__ * `Optional edit command `__: sometimes people want to make the working copy read-only, or not present at all. * `Partial commits `__ * `Patch pools `__ to efficiently store related branches. * `Revision syntax `__ -- ``hello.c@12``, etc. * `Roll-up commits `__ -- a single revision incorporates the changes from several others. * `Scalability `__ * `Security `__ * `Shared branches `__ maintained by more than one person * `Supportability `__ -- how to handle any bugs or problems in the field. * `Place tags on revisions for easy reference `__ * `Detecting unchanged files `__ * `Merging previously-unrelated branches `__ * `Usability principles `__ (very small at the moment) * ``__ * ``__ * ``__ Modelling/controlling flow of patches. * ``__ -- Discussion of using YAML_ as a storage or transmission format. .. _YAML: http://www.yaml.org/ Comparisons to other systems ---------------------------- * `Taxonomy `__: basic questions a VCS must answer. * `Bitkeeper `__, the proprietary system used by some kernel developers. * `Aegis `__, a tool focussed on enforcing process and workflow. * `Codeville `__ has an intruiging but scarcely-documented merge algorithm. * `CVSNT `__, with more Windows support and some merge enhancements. * `OpenCM `__, another hash-based tool with a good whitepaper. * `PRCS `__, a non-distributed inventory-based tool. * `GNU Arch `__, with many pros and cons. * `Darcs `__, a merge-focussed tool with good usability. * `Quilt `__ -- Andrew Morton's patch scripts, popular with kernel maintainers. * `Monotone `__, Graydon Hoare's hash-based distributed system. * `SVK `__ -- distributed operation stacked on Subversion. * `Sun Teamware `__ Project management and organization ----------------------------------- * `Notes on how to get a VCS adopted `__ * `Thanks `__ to various people * `Extra commands `__ for internal/developer/debugger use. * `Choice of Python as a development language `__ M 644 inline doc/random.txt data 8930 I think Ruby's point is right: we need to think about how a tool *feels* as you're using it. Making regular commits gives a nice rhythm to to working; in some ways it's nicer to just commit single files with C-x v v than to build complex changesets. (See gmane.c.v-c.arch.devel post 19 Nov, Tom Lord.) * Would like to generate an activity report, to e.g. mail to your boss or post to your blog. "What did I change today, across all these specified branches?" * It is possibly nice that tla by default forbids you from committing if emacs autosave or lock files exist -- I find it confusing to commit somethin other than what is shown in the editor window because there are unsaved changes. However, grumbling about unknown files is annoying, and requiring people to edit regexps in the id-tagging-method file to fix it is totally unreasonable. Perhaps there should be a preference to abort on unknown files, or perhaps it should be possible to specify forbidden files. Perhaps this is related to a mechanism to detect conflicted files: should refuse to commit if there are any .rej files lying around. *Those who lose history are doomed to recreate it.* -- broked (on #gnu.arch.users) *A universal convention supplies all of maintainability, clarity, consistency, and a foundation for good programming habits too. What it doesn't do is insist that you follow it against your will. That's Python!* -- Tim Peters on comp.lang.python, 2001-06-16 (Bazaar provides mechanism and convention, but it is up to you whether you wish to follow or enforce that convention.) ---- jblack asks for A way to subtract merges, so that you can see the work you've done to a branch since conception. ---- :: now that is a neat idea: advertise branches over zeroconf should make lca fun :-) ---- http://thedailywtf.com/ShowPost.aspx?PostID=24281 Source control is necessary and useful, but in a team of one (or even two) people the setup overhead isn't always worth it--especially if you're going to join source control in a month, and you don't want to have to migrate everything out of your existing (in my case, skunkworks) system before you can use it. At least that was my experience--I putzed with CVS a bit and knew other source control systems pretty well, but in the day-to-day it wasn't worth the bother (granted, I was a bit offended at having to wait to use the mainline source control, but that's another matter). I think Bazaar-NG will have such low setup overhead (just ``init``, ``add``) that it can be easily used for even tiny projects. The ability to merge previously-unrelated trees means they can fold their project in later. ---- From tridge: * cope without $EMAIL better * notes at start of .bzr.log: * you can delete this * or include it in bug reports * should you be able to remove things from the default ignore list? * headers at start of diff, giving some comments, perhaps dates * is diff against /dev/null really OK? I think so. * separate remove/delete commands? * detect files which were removed and now in 'missing' state * should we actually compare files for 'status', or check mtime and size; reading every file in the samba source tree can take a long time. without this, doing a status on a large tree can be very slow. but relying on mtime/size is a bit dangerous. people really do work on trees which take a large chunk of memory and which will not stay in memory * status up-to-date files: not 'U', and don't list without --all * if status does compare file text, then it should be quick when checking just a single file * wrapper for svn that every time run logs - command - all inputs - time it took - sufficient to replay everything - record all files * status crashes if a file is missing * option for -p1 level on diff, etc. perhaps * commit without message should start $EDITOR * don't duplicate all files on commit * start importing tridge-junkcode * perhaps need xdelta storage sooner rather than later, to handle very large file ---- The first operation most people do with a new version-control system is *not* making their own project, but rather getting a checkout of an existing project, building it, and possibly submitting a patch. So those operations should be *extremely* easy. ---- * Way to check that a branch is fully merged, and no longer needed: should mean all its changes have been integrated upstream, no uncommitted changes or rejects or unknown files. * Filter revisions by containing a particular word (as for log). Perhaps have key-value fields that might be used for e.g. line-of-development or bug nr? * List difference in the revisions on one branch vs another. * Perhaps use a partially-readable but still hopefully unique ID for revisions/inventories? * Preview what will happen in a merge before it is applied * When a changeset deletes a file, should have the option to just make it unknown/ignored. Perhaps this is best handled by an interactive merge. If the file is unchanged locally and deleted remotely, it will by default be deleted (but the user has the option to reject the delete, or to make it just unversioned, or to save a copy.) If it is modified locall then the user still needs to choose between those options but there is no default (or perhaps the default is to reject the delete.) * interactive commit, prompting whether each hunk should be sent (as for darcs) * Write up something about detection of unmodified files * Preview a merge so as to get some idea what will happen: * What revisions will be merged (log entries, etc) * What files will be affected? * Are those simple updates, or have they been updated locally as well. * Any renames or metadata clashes? * Show diffs or conflict markers. * Do the merge, but write into a second directory. * "Show me all changesets that touch this file" Can be done by walking back through all revisions, and filtering out those where the file-id either gets a new name or a new text. * Way to commit backdated revisions or pretend to be something by someone else, for the benefit of import tools; in general allow everything taken from the current environment to be overridden. * Cope well when trying to checkout or update over a flaky connection. Passive HTTP possibly helps with this: we can fetch all the file texts first, then the inventory, and can even retry interrupted connections. * Use readline for reading log messages, and store a history of previous commit messages! * Warn when adding huge files(?) - more than say 10MB? On the other hand, why not just cope? * Perhaps allow people to specify a revision-id, much as people have unique but human-assigned names for patches at the moment? ---- 20050218090900.GA2071@opteron.random Subject: Re: [darcs-users] Re: [BK] upgrade will be needed From: Andrea Arcangeli Newsgroups: gmane.linux.kernel Date: Fri, 18 Feb 2005 10:09:00 +0100 On Thu, Feb 17, 2005 at 06:24:53PM -0800, Tupshin Harper wrote: > small to medium sized ones). Last I checked, Arch was still too slow in > some areas, though that might have changed in recent months. Also, many IMHO someone needs to rewrite ARCH using the RCS or SCCS format for the backend and a single file for the changesets and with sane parameters conventions miming SVN. The internal algorithms of arch seems the most advanced possible. It's just the interface and the fs backend that's so bad and doesn't compress in the backups either. SVN bsddb doesn't compress either by default, but at least the new fsfs compresses pretty well, not as good as CVS, but not as badly as bsddb and arch either. I may be completely wrong, so take the above just as a humble suggestion. darcs scares me a bit because it's in haskell, I don't believe very much in functional languages for compute intensive stuff, ram utilization skyrockets sometime (I wouldn't like to need >1G of ram to manage the tree). Other languages like python or perl are much slower than C/C++ too but at least ram utilization can be normally dominated to sane levels with them and they can be greatly optimized easily with C/C++ extensions of the performance critical parts. ----- When sending patches by email (optionall) send each one as a separate mail, with a sequence number [%03d/%03d] at the start of the Subject. See mail from linus 2005-04-07. http://linux.yyz.us/patch-format.html http://www.zip.com.au/~akpm/linux/patches/stuff/tpp.txt ---- dwmw2 (2005-04-07) reorder patches by cherry-picking them from a main development tree before sending them on. ---- ignore BitKeeper re-add should give the same id as before commit refs/heads/master mark :283 committer Martin Pool 1114494677 +1000 data 27 - update doc upload scripts from :282 M 644 inline doc/Makefile data 242 %.html: %.txt rest2html $^ > $@.tmp && mv $@.tmp $@ all: $(addsuffix .html,$(basename $(wildcard *.txt))) upload: all rsync -av \ ./ \ escudero.ubuntu.com:/srv/www.bazaar-ng.org/www/doc/ \ --delete \ --include \*.html --exclude \* commit refs/heads/master mark :284 committer Martin Pool 1114497831 +1000 data 17 - more TODO items from :283 M 644 inline TODO data 2483 (See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues, or ) Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be "bzr export -r REV". Medium things ------------- * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * mv command? * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Expansion of $Id$ tags within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. Large things ------------ * Web interface * GUI (maybe in Python GTK+?) * C library interface commit refs/heads/master mark :285 committer Martin Pool 1114511072 +1000 data 14 - update todos from :284 M 644 inline TODO data 2503 (See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues, or ) Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be "bzr export -r REV". * "cat -rREV FILE" Medium things ------------- * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * mv command? * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Expansion of $Id$ tags within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. Large things ------------ * Web interface * GUI (maybe in Python GTK+?) * C library interface M 644 inline bzrlib/branch.py data 34776 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. TODO: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. TODO: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. TODO: Keep the on-disk branch locked while the object exists. TODO: mkdir() method. """ def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) self.append_revision(rev_id) if verbose: note("commited r%d" % self.revno()) def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history', 'r').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] else: return None def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original', verbose=False): """Write out human-readable log of commits to this branch utc -- If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l if verbose == True and precursor != None: print 'changed files:' tree = self.revision_tree(p) prevtree = self.revision_tree(precursor) for file_state, fid, old_name, new_name, kind in \ diff_trees(prevtree, tree, ): if file_state == 'A' or file_state == 'M': show_status(file_state, kind, new_name) elif file_state == 'D': show_status(file_state, kind, old_name) elif file_state == 'R': show_status(file_state, kind, old_name + ' => ' + new_name) revno += 1 precursor = p def rename_one(self, from_rel, to_rel): tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(self, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo TODO: Get state for single files. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = self.basis_tree() new = self.working_tree() for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("weird file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" try: shutil.rmtree(self.base) except OSError: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/commands.py data 32534 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool http://bazaar-ng.org/ **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** * Metadata format is not stable yet -- you may need to discard history in the future. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr rename FROM TO Rename one file. bzr move FROM... DESTDIR Move one or more files to a different directory. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. bzr check Verify history is stored safely. (for more type 'bzr help commands') """ import sys, os, time, types, shutil, tempfile, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. cmd_aliases = { '?': 'help', 'ci': 'commit', 'checkin': 'commit', 'di': 'diff', 'st': 'status', 'stat': 'status', } def get_cmd_handler(cmd): cmd = str(cmd) cmd = cmd_aliases.get(cmd, cmd) try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: raise BzrError("unknown command %r" % cmd) return cmd, cmd_handler def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. if file_list: file_list = [b.relpath(f) for f in file_list] # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and (new_name not in file_list): continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) sys.stdout.writelines(ud) if nonl: print "\\ No newline at end of file" sys.stdout.write('\n') if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_dump_text_inventory(): import bzrlib.textinv inv = Branch('.').basis_tree().inventory bzrlib.textinv.write_text_inventory(inv, sys.stdout) def cmd_load_text_inventory(): import bzrlib.textinv inv = bzrlib.textinv.read_text_inventory(sys.stdin) print 'loaded %d entries' % len(inv) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original', verbose=False): """Show log of this branch. TODO: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone, verbose=verbose) def cmd_ls(revision=None, verbose=False): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_whoami(): """Show bzr user id. usage: bzr whoami TODO: Command to show only the email-address part as parsed out. """ print bzrlib.osutils.username() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ elif topic == 'commands': help_commands() else: # otherwise, maybe the name of a command? topic, cmdfn = get_cmd_handler(topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def help_commands(): """List all commands""" accu = [] for k in globals().keys(): if k.startswith('cmd_'): accu.append(k[4:].replace('_','-')) accu.sort() print "bzr commands: " for x in accu: print " " + x print "note: some of these commands are internal-use or obsolete" # TODO: Some kind of marker for internal-use commands? # TODO: Show aliases? def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone', 'verbose'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_handler = get_cmd_handler(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(canonical_cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(canonical_cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: return cmd_handler(**cmdargs) or 0 def _report_exception(e, summary): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception(e) tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def cmd_assert_fail(): assert False, "always fails" def main(argv): bzrlib.trace.create_tracefile(argv) try: try: ret = run_bzr(argv) return ret except BzrError, e: _report_exception(e, 'error: ' + e.args[0]) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(e, msg) except Exception, e: _report_exception(e, 'exception: %s' % str(e).rstrip('\n')) return 1 finally: bzrlib.trace.close_trace() ## TODO: Trap AssertionError # TODO: Maybe nicer handling of IOError especially for broken pipe. if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :286 committer Martin Pool 1114511873 +1000 data 31 - New bzr whoami --email option from :285 M 644 inline NEWS data 4172 bzr-0.0.5 NOT RELEASED YET ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/__init__.py data 1588 # (C) 2005 Canonical Development Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr library""" from inventory import Inventory, InventoryEntry from branch import Branch, ScratchBranch from osutils import format_date from tree import Tree from diff import diff_trees from trace import mutter, warning import add BZRDIR = ".bzr" DEFAULT_IGNORE = ['.bzr.log', '*~', '#*#', '*$', '.#*', '*.tmp', '*.bak', '*.BAK', '*.orig', '*.o', '*.obj', '*.a', '*.py[oc]', '*.so', '*.exe', '*.elc', '{arch}', 'CVS', '.svn', '_darcs', 'SCCS', 'RCS', 'BitKeeper', 'TAGS', '.make.state', '.sconsign', '.tmp*'] IGNORE_FILENAME = ".bzrignore" import locale user_encoding = locale.getpreferredencoding() __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __version__ = '0.0.5pre' M 644 inline bzrlib/commands.py data 32714 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool http://bazaar-ng.org/ **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** * Metadata format is not stable yet -- you may need to discard history in the future. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr rename FROM TO Rename one file. bzr move FROM... DESTDIR Move one or more files to a different directory. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. bzr check Verify history is stored safely. (for more type 'bzr help commands') """ import sys, os, time, types, shutil, tempfile, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. cmd_aliases = { '?': 'help', 'ci': 'commit', 'checkin': 'commit', 'di': 'diff', 'st': 'status', 'stat': 'status', } def get_cmd_handler(cmd): cmd = str(cmd) cmd = cmd_aliases.get(cmd, cmd) try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: raise BzrError("unknown command %r" % cmd) return cmd, cmd_handler def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. if file_list: file_list = [b.relpath(f) for f in file_list] # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and (new_name not in file_list): continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) sys.stdout.writelines(ud) if nonl: print "\\ No newline at end of file" sys.stdout.write('\n') if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_dump_text_inventory(): import bzrlib.textinv inv = Branch('.').basis_tree().inventory bzrlib.textinv.write_text_inventory(inv, sys.stdout) def cmd_load_text_inventory(): import bzrlib.textinv inv = bzrlib.textinv.read_text_inventory(sys.stdin) print 'loaded %d entries' % len(inv) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original', verbose=False): """Show log of this branch. TODO: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone, verbose=verbose) def cmd_ls(revision=None, verbose=False): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_whoami(email=False): """Show bzr user id. usage: bzr whoami options: --email Show only the email address. """ if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ elif topic == 'commands': help_commands() else: # otherwise, maybe the name of a command? topic, cmdfn = get_cmd_handler(topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def help_commands(): """List all commands""" accu = [] for k in globals().keys(): if k.startswith('cmd_'): accu.append(k[4:].replace('_','-')) accu.sort() print "bzr commands: " for x in accu: print " " + x print "note: some of these commands are internal-use or obsolete" # TODO: Some kind of marker for internal-use commands? # TODO: Show aliases? def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone', 'verbose'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], 'whoami': ['email'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_handler = get_cmd_handler(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(canonical_cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(canonical_cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: return cmd_handler(**cmdargs) or 0 def _report_exception(e, summary): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception(e) tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def cmd_assert_fail(): assert False, "always fails" def main(argv): bzrlib.trace.create_tracefile(argv) try: try: ret = run_bzr(argv) return ret except BzrError, e: _report_exception(e, 'error: ' + e.args[0]) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(e, msg) except Exception, e: _report_exception(e, 'exception: %s' % str(e).rstrip('\n')) return 1 finally: bzrlib.trace.close_trace() ## TODO: Trap AssertionError # TODO: Maybe nicer handling of IOError especially for broken pipe. if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline test.sh data 2666 #! /bin/sh -pe # Simple shell-based tests for bzr. # This is meant to exercise the external behaviour, command line # parsing and similar things and compliment the inwardly-turned # testing done by doctest. # This must already exist and be in the right place if ! [ -d bzr-test.tmp ] then echo "please create directory bzr-test.tmp" exit 1 fi echo "testing `which bzr`" bzr --version | head -n 1 echo rm -rf bzr-test.tmp mkdir bzr-test.tmp # save it for real errors exec 3>&2 exec > bzr-test.log exec 2>&1 set -x quitter() { echo "tests failed, look in bzr-test.log" >&3; exit 2; } trap quitter ERR cd bzr-test.tmp rm -rf .bzr mkdir branch1 cd branch1 # some information commands bzr help bzr version [ $(bzr help commands | wc -l) -gt 20 ] # user identification is set bzr whoami bzr whoami --email # invalid commands are detected ! bzr pants # TODO: test unicode user names bzr help # some experiments with renames bzr init echo "hello world" > test.txt bzr unknowns # should be the only unknown file [ "`bzr unknowns`" = test.txt ] bzr status --all > status.tmp ! diff -u - status.tmp < new-in-2.txt bzr add new-in-2.txt bzr commit -m "add file to branch 2 only" [ `bzr revno` = 3 ] cd ../branch1 [ `bzr revno` = 2 ] bzr check echo "tests completed ok" >&3 commit refs/heads/master mark :287 committer Martin Pool 1114558425 +1000 data 15 - todo: plugins from :286 M 644 inline TODO data 2953 -*- indented-text -*- (See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues, or ) Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be "bzr export -r REV". * "cat -rREV FILE" * Plugins that provide commands. By just installing a file into some directory (e.g. /usr/share/bzr/plugins) it should be possible to create new top-level commands ("bzr frob"). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. Medium things ------------- * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * mv command? * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Expansion of $Id$ tags within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. Large things ------------ * Web interface * GUI (maybe in Python GTK+?) * C library interface commit refs/heads/master mark :288 committer Martin Pool 1114558734 +1000 data 4 TODO from :287 M 644 inline TODO data 3039 -*- indented-text -*- (See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues, or ) Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be "bzr export -r REV". * "cat -rREV FILE" * Plugins that provide commands. By just installing a file into some directory (e.g. /usr/share/bzr/plugins) it should be possible to create new top-level commands ("bzr frob"). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) Medium things ------------- * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * mv command? * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Expansion of $Id$ tags within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. Large things ------------ * Web interface * GUI (maybe in Python GTK+?) * C library interface commit refs/heads/master mark :289 committer Martin Pool 1114558812 +1000 data 4 todo from :288 M 644 inline TODO data 3098 -*- indented-text -*- (See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues, or ) Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be "bzr export -r REV". * "cat -rREV FILE" * Plugins that provide commands. By just installing a file into some directory (e.g. /usr/share/bzr/plugins) it should be possible to create new top-level commands ("bzr frob"). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. Medium things ------------- * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * mv command? * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Expansion of $Id$ tags within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. Large things ------------ * Web interface * GUI (maybe in Python GTK+?) * C library interface commit refs/heads/master mark :290 committer Martin Pool 1114559081 +1000 data 4 todo from :289 M 644 inline TODO data 3332 -*- indented-text -*- (See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues, or ) Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be "bzr export -r REV". * "cat -rREV FILE" * Plugins that provide commands. By just installing a file into some directory (e.g. /usr/share/bzr/plugins) it should be possible to create new top-level commands ("bzr frob"). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * "bzr info" could show space used by working tree, versioned files, unknown and ignored files. * "bzr info" should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) Medium things ------------- * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * mv command? * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Expansion of $Id$ tags within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. Large things ------------ * Web interface * GUI (maybe in Python GTK+?) * C library interface M 644 inline bzrlib/info.py data 3194 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import time import bzrlib from osutils import format_date def show_info(b): print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl != None: return pl else: return 's' count_version_dirs = 0 count_status = {'A': 0, 'D': 0, 'M': 0, 'R': 0, '?': 0, 'I': 0, '.': 0} for st_tup in bzrlib.diff_trees(b.basis_tree(), b.working_tree()): fs = st_tup[0] count_status[fs] += 1 if fs not in ['I', '?'] and st_tup[4] == 'directory': count_version_dirs += 1 print print 'in the working tree:' for name, fs in (('unchanged', '.'), ('modified', 'M'), ('added', 'A'), ('removed', 'D'), ('renamed', 'R'), ('unknown', '?'), ('ignored', 'I'), ): print ' %8d %s' % (count_status[fs], name) print ' %8d versioned subdirector%s' % (count_version_dirs, plural(count_version_dirs, 'y', 'ies')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %8d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %8d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %8d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) print print 'text store:' c, t = b.text_store.total_size() print ' %8d file texts' % c print ' %8d kB' % (t/1024) print print 'revision store:' c, t = b.revision_store.total_size() print ' %8d revisions' % c print ' %8d kB' % (t/1024) print print 'inventory store:' c, t = b.inventory_store.total_size() print ' %8d inventories' % c print ' %8d kB' % (t/1024) commit refs/heads/master mark :291 committer Martin Pool 1114657365 +1000 data 4 todo from :290 M 644 inline TODO data 3469 -*- indented-text -*- (See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues, or ) Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be "bzr export -r REV". * "cat -rREV FILE" * Plugins that provide commands. By just installing a file into some directory (e.g. /usr/share/bzr/plugins) it should be possible to create new top-level commands ("bzr frob"). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * "bzr info" could show space used by working tree, versioned files, unknown and ignored files. * "bzr info" should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * Tidier error for EPIPE: should be just "bzr: broken pipe" with no other details because debugging information is rarely interesting. Medium things ------------- * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * mv command? * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Expansion of $Id$ tags within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. Large things ------------ * Web interface * GUI (maybe in Python GTK+?) * C library interface commit refs/heads/master mark :292 committer Martin Pool 1114659099 +1000 data 48 - start adding a pure-python blackbox test suite from :291 M 644 inline testbzr data 2064 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os try: import shutil from subprocess import call, Popen except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) def runcmd(cmd): """run one command and check the output. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" if isinstance(cmd, basestring): cmd = cmd.split() retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != 0: raise Exception("test failed: %r returned %r" % (cmd, retcode)) def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') TESTDIR = "bzr-test.tmp" # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open('bzr-test.log', 'wt') os.mkdir(TESTDIR) os.chdir(TESTDIR) progress("testing introductory commands") runcmd("bzr version") runcmd("bzr help") runcmd("bzr --help") progress("all tests passed!") commit refs/heads/master mark :293 committer Martin Pool 1114672026 +1000 data 39 - todos - log commands run from testbzr from :292 M 644 inline TODO data 3871 -*- indented-text -*- See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be "bzr export -r REV". * "cat -rREV FILE" * Plugins that provide commands. By just installing a file into some directory (e.g. /usr/share/bzr/plugins) it should be possible to create new top-level commands ("bzr frob"). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * "bzr info" could show space used by working tree, versioned files, unknown and ignored files. * "bzr info" should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * Tidier error for EPIPE: should be just "bzr: broken pipe" with no other details because debugging information is rarely interesting. * On Windows, command-line arguments should be glob-expanded__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html Medium things ------------- * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * mv command? * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Expansion of $Id$ tags within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. Large things ------------ * Web interface * GUI (maybe in Python GTK+?) * C library interface M 644 inline testbzr data 2098 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os try: import shutil from subprocess import call, Popen except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) def runcmd(cmd): """run one command and check the output. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" if isinstance(cmd, basestring): cmd = cmd.split() logfile.write('$ %r\n' % cmd) retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != 0: raise Exception("test failed: %r returned %r" % (cmd, retcode)) def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') TESTDIR = "bzr-test.tmp" # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open('bzr-test.log', 'wt') os.mkdir(TESTDIR) os.chdir(TESTDIR) progress("testing introductory commands") runcmd("bzr version") runcmd("bzr help") runcmd("bzr --help") progress("all tests passed!") commit refs/heads/master mark :294 committer Martin Pool 1114673095 +1000 data 4 todo from :293 M 644 inline TODO data 4397 -*- indented-text -*- See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be "bzr export -r REV". * "cat -rREV FILE" * Plugins that provide commands. By just installing a file into some directory (e.g. /usr/share/bzr/plugins) it should be possible to create new top-level commands ("bzr frob"). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * "bzr info" could show space used by working tree, versioned files, unknown and ignored files. * "bzr info" should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * Tidier error for EPIPE: should be just "bzr: broken pipe" with no other details because debugging information is rarely interesting. * On Windows, command-line arguments should be glob-expanded__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html Medium things ------------- * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * mv command? * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. Large things ------------ * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface commit refs/heads/master mark :295 committer Martin Pool 1114676201 +1000 data 4 todo from :294 M 644 inline TODO data 4870 -*- indented-text -*- See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be "bzr export -r REV". * "cat -rREV FILE" * Plugins that provide commands. By just installing a file into some directory (e.g. /usr/share/bzr/plugins) it should be possible to create new top-level commands ("bzr frob"). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * "bzr info" could show space used by working tree, versioned files, unknown and ignored files. * "bzr info" should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * Tidier error for EPIPE: should be just "bzr: broken pipe" with no other details because debugging information is rarely interesting. * On Windows, command-line arguments should be glob-expanded__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * 'bzr ignore' command that just adds a line to the .bzrignore file and makes it versioned. * 'bzr help commands' should give a one-line summary of each command. Medium things ------------- * Display command grammar in help messages rather than hardcoding it. * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * mv command? * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. Large things ------------ * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface commit refs/heads/master mark :296 committer Martin Pool 1114678242 +1000 data 43 - better reports from testbzr when it fails from :295 M 644 inline NEWS data 4255 bzr-0.0.5 NOT RELEASED YET ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. TESTING: * Converted black-box test suites from Bourne shell into Python. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline testbzr data 2495 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback try: import shutil from subprocess import call, Popen except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def runcmd(cmd): """run one command and check the output. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" if isinstance(cmd, basestring): cmd = cmd.split() logfile.write('$ %r\n' % cmd) retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != 0: raise CommandFailed("test failed: %r returned %r" % (cmd, retcode)) def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') TESTDIR = "bzr-test.tmp" # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open('bzr-test.log', 'wt') try: os.mkdir(TESTDIR) os.chdir(TESTDIR) progress("testing introductory commands") runcmd("bzr version") runcmd("bzr help") runcmd("bzr --helpx") progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see bzr-test.log for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :297 committer Martin Pool 1114678992 +1000 data 90 - fix intentional testcase failure - log line numbers from test case - test whoami command from :296 M 644 inline NEWS data 4305 bzr-0.0.5 NOT RELEASED YET ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. TESTING: * Converted black-box test suites from Bourne shell into Python. Various structural improvements to the tests. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline testbzr data 2848 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback try: import shutil from subprocess import call, Popen except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def runcmd(cmd): """run one command and check the output. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" if isinstance(cmd, basestring): cmd = cmd.split() log_linenumber() logfile.write('$ %r\n' % cmd) retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != 0: raise CommandFailed("test failed: %r returned %r" % (cmd, retcode)) def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) TESTDIR = "testbzr.tmp" # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open('testbzr.log', 'wt', buffering=1) try: os.mkdir(TESTDIR) os.chdir(TESTDIR) progress("testing introductory commands") runcmd("bzr version") runcmd("bzr help") runcmd("bzr --help") progress("testing user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see bzr-test.log for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :298 committer Martin Pool 1114679139 +1000 data 34 - test some commands known to fail from :297 M 644 inline testbzr data 3087 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback try: import shutil from subprocess import call, Popen except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def runcmd(cmd, retcode=0): """run one command and check the output. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" if isinstance(cmd, basestring): cmd = cmd.split() log_linenumber() logfile.write('$ %r\n' % cmd) actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) TESTDIR = "testbzr.tmp" # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open('testbzr.log', 'wt', buffering=1) try: os.mkdir(TESTDIR) os.chdir(TESTDIR) progress("testing introductory commands") runcmd("bzr version") runcmd("bzr help") runcmd("bzr --help") progress("testing user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") progress("testing invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see bzr-test.log for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :299 committer Martin Pool 1114679258 +1000 data 61 testbzr: - log directory changes - better logging of commands from :298 M 644 inline testbzr data 3214 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback try: import shutil from subprocess import call, Popen except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def runcmd(cmd, retcode=0): """run one command and check the output. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" if isinstance(cmd, basestring): logfile.write('$ %s\n' % cmd) cmd = cmd.split() else: logfile.write('$ %r\n' % cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) TESTDIR = "testbzr.tmp" # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open('testbzr.log', 'wt', buffering=1) try: os.mkdir(TESTDIR) cd(TESTDIR) progress("testing introductory commands") runcmd("bzr version") runcmd("bzr help") runcmd("bzr --help") progress("testing user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") progress("testing invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see bzr-test.log for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :300 committer Martin Pool 1114679731 +1000 data 12 - more tests from :299 M 644 inline testbzr data 3418 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback try: import shutil from subprocess import call, Popen except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def runcmd(cmd, retcode=0): """Run one command and check the return code. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" if isinstance(cmd, basestring): logfile.write('$ %s\n' % cmd) cmd = cmd.split() else: logfile.write('$ %r\n' % cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) TESTDIR = "testbzr.tmp" # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open('testbzr.log', 'wt', buffering=1) try: runcmd(['mkdir', TESTDIR]) cd(TESTDIR) progress("introductory commands") runcmd("bzr version") runcmd("bzr help") runcmd("bzr --help") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') f = file('test.txt', 'wt') f.write('hello world!\n') f.close() cd('..') progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see bzr-test.log for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :301 committer Martin Pool 1114680388 +1000 data 49 - provide for catching output from shell commands from :300 M 644 inline testbzr data 3747 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def runcmd(cmd, retcode=0, catchstdout=False): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" if isinstance(cmd, basestring): logfile.write('$ %s\n' % cmd) cmd = cmd.split() else: logfile.write('$ %r\n' % cmd) log_linenumber() if catchstdout: stdout = PIPE else: stdout = logfile child = Popen(cmd, stdout=stdout, stderr=logfile) outd, errd = child.communicate() actual_retcode = child.wait() if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd, errd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) TESTDIR = "testbzr.tmp" # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open('testbzr.log', 'wt', buffering=1) try: runcmd(['mkdir', TESTDIR]) cd(TESTDIR) progress("introductory commands") runcmd("bzr version") runcmd("bzr help") runcmd("bzr --help") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert runcmd("bzr whoami --email", catchstdout=True)[0].count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') f = file('test.txt', 'wt') f.write('hello world!\n') f.close() cd('..') progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see bzr-test.log for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :302 committer Martin Pool 1114680889 +1000 data 30 testbzr: new backtick() helper from :301 M 644 inline testbzr data 4092 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): logfile.write('$ %s\n' % cmd) cmd = cmd.split() else: logfile.write('$ %r\n' % cmd) return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) TESTDIR = "testbzr.tmp" # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open('testbzr.log', 'wt', buffering=1) try: runcmd(['mkdir', TESTDIR]) cd(TESTDIR) progress("introductory commands") runcmd("bzr version") runcmd("bzr help") runcmd("bzr --help") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') f = file('test.txt', 'wt') f.write('hello world!\n') f.close() unknowns = backtick("bzr unknowns").rstrip('\r\n') assert unknowns == 'test.txt' cd('..') progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see testbzr.log for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :303 committer Martin Pool 1114681340 +1000 data 29 - more tests for unknown file from :302 M 644 inline testbzr data 4313 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): logfile.write('$ %s\n' % cmd) cmd = cmd.split() else: logfile.write('$ %r\n' % cmd) return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) TESTDIR = "testbzr.tmp" # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open('testbzr.log', 'wt', buffering=1) try: runcmd(['mkdir', TESTDIR]) cd(TESTDIR) progress("introductory commands") runcmd("bzr version") runcmd("bzr help") runcmd("bzr --help") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns").rstrip('\r\n') assert out == 'test.txt' out = backtick("bzr status").replace('\r', '') assert out == '''? test.txt\n''' out = backtick("bzr status --all").replace('\r', '') assert out == '''? test.txt\n''' cd('..') progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see testbzr.log for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :304 committer Martin Pool 1114681520 +1000 data 27 testbzr: test adding a file from :303 M 644 inline testbzr data 4496 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): logfile.write('$ %s\n' % cmd) cmd = cmd.split() else: logfile.write('$ %r\n' % cmd) return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) TESTDIR = "testbzr.tmp" # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open('testbzr.log', 'wt', buffering=1) try: runcmd(['mkdir', TESTDIR]) cd(TESTDIR) progress("introductory commands") runcmd("bzr version") runcmd("bzr help") runcmd("bzr --help") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns").rstrip('\r\n') assert out == 'test.txt' out = backtick("bzr status").replace('\r', '') assert out == '''? test.txt\n''' out = backtick("bzr status --all").replace('\r', '') assert out == "? test.txt\n" progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all").replace('\r', '') == "A test.txt\n" cd('..') progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see testbzr.log for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :305 committer Martin Pool 1114681708 +1000 data 21 testbzr: test renames from :304 M 644 inline testbzr data 4678 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): logfile.write('$ %s\n' % cmd) cmd = cmd.split() else: logfile.write('$ %r\n' % cmd) return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) TESTDIR = "testbzr.tmp" # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open('testbzr.log', 'wt', buffering=1) try: runcmd(['mkdir', TESTDIR]) cd(TESTDIR) progress("introductory commands") runcmd("bzr version") runcmd("bzr help") runcmd("bzr --help") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns").rstrip('\r\n') assert out == 'test.txt' out = backtick("bzr status").replace('\r', '') assert out == '''? test.txt\n''' out = backtick("bzr status --all").replace('\r', '') assert out == "? test.txt\n" progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all").replace('\r', '') == "A test.txt\n" progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") cd('..') progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see testbzr.log for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :306 committer Martin Pool 1114681715 +1000 data 21 testbzr: test renames from :305 M 644 inline testbzr data 4759 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): logfile.write('$ %s\n' % cmd) cmd = cmd.split() else: logfile.write('$ %r\n' % cmd) return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) TESTDIR = "testbzr.tmp" # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open('testbzr.log', 'wt', buffering=1) try: runcmd(['mkdir', TESTDIR]) cd(TESTDIR) progress("introductory commands") runcmd("bzr version") runcmd("bzr help") runcmd("bzr --help") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns").rstrip('\r\n') assert out == 'test.txt' out = backtick("bzr status").replace('\r', '') assert out == '''? test.txt\n''' out = backtick("bzr status --all").replace('\r', '') assert out == "? test.txt\n" progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all").replace('\r', '') == "A test.txt\n" progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") cd('..') progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see testbzr.log for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :307 committer Martin Pool 1114682115 +1000 data 31 testbzr: clean up crlf handling from :306 M 644 inline testbzr data 4768 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): logfile.write('$ %s\n' % cmd) cmd = cmd.split() else: logfile.write('$ %r\n' % cmd) return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) TESTDIR = "testbzr.tmp" # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open('testbzr.log', 'wt', buffering=1) try: runcmd(['mkdir', TESTDIR]) cd(TESTDIR) progress("introductory commands") runcmd("bzr version") runcmd("bzr help") runcmd("bzr --help") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == '''? test.txt\n''' out = backtick("bzr status --all") assert out == "? test.txt\n" progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == "A test.txt\n" progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0' cd('..') progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see testbzr.log for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :308 committer Martin Pool 1114682140 +1000 data 14 fix test suite from :307 M 644 inline testbzr data 4770 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): logfile.write('$ %s\n' % cmd) cmd = cmd.split() else: logfile.write('$ %r\n' % cmd) return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) TESTDIR = "testbzr.tmp" # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open('testbzr.log', 'wt', buffering=1) try: runcmd(['mkdir', TESTDIR]) cd(TESTDIR) progress("introductory commands") runcmd("bzr version") runcmd("bzr help") runcmd("bzr --help") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == '''? test.txt\n''' out = backtick("bzr status --all") assert out == "? test.txt\n" progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == "A test.txt\n" progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' cd('..') progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see testbzr.log for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :309 committer Martin Pool 1114682272 +1000 data 3 doc from :308 M 644 inline bzrlib/branch.py data 34881 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. TODO: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. TODO: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. TODO: Keep the on-disk branch locked while the object exists. TODO: mkdir() method. """ def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) self.append_revision(rev_id) if verbose: note("commited r%d" % self.revno()) def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [chomp(l) for l in self.controlfile('revision-history', 'r').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] else: return None def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original', verbose=False): """Write out human-readable log of commits to this branch utc -- If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l if verbose == True and precursor != None: print 'changed files:' tree = self.revision_tree(p) prevtree = self.revision_tree(precursor) for file_state, fid, old_name, new_name, kind in \ diff_trees(prevtree, tree, ): if file_state == 'A' or file_state == 'M': show_status(file_state, kind, new_name) elif file_state == 'D': show_status(file_state, kind, old_name) elif file_state == 'R': show_status(file_state, kind, old_name + ' => ' + new_name) revno += 1 precursor = p def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(self, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo TODO: Get state for single files. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = self.basis_tree() new = self.working_tree() for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("weird file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" try: shutil.rmtree(self.base) except OSError: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :310 committer Martin Pool 1114682504 +1000 data 28 - new 'bzr ignored' command! from :309 M 644 inline .bzrignore data 110 ./doc/*.html *.py[oc] *~ .arch-ids .bzr.profile .arch-inventory {arch} CHANGELOG bzr-test.log ,,* testbzr.log M 644 inline NEWS data 4346 bzr-0.0.5 NOT RELEASED YET ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New 'bzr ignore PATTERN' command. TESTING: * Converted black-box test suites from Bourne shell into Python. Various structural improvements to the tests. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline TODO data 4919 -*- indented-text -*- See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be "bzr export -r REV". * "cat -rREV FILE" * Plugins that provide commands. By just installing a file into some directory (e.g. /usr/share/bzr/plugins) it should be possible to create new top-level commands ("bzr frob"). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * "bzr info" could show space used by working tree, versioned files, unknown and ignored files. * "bzr info" should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * Tidier error for EPIPE: should be just "bzr: broken pipe" with no other details because debugging information is rarely interesting. * On Windows, command-line arguments should be glob-expanded__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * 'bzr ignore' command that just adds a line to the .bzrignore file and makes it versioned. * 'bzr help commands' should give a one-line summary of each command. * Are any sanity checks useful in 'bzr ignore'? Medium things ------------- * Display command grammar in help messages rather than hardcoding it. * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * mv command? * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. Large things ------------ * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface M 644 inline bzrlib/commands.py data 33173 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool http://bazaar-ng.org/ **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** * Metadata format is not stable yet -- you may need to discard history in the future. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr rename FROM TO Rename one file. bzr move FROM... DESTDIR Move one or more files to a different directory. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. bzr check Verify history is stored safely. (for more type 'bzr help commands') """ import sys, os, time, types, shutil, tempfile, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. cmd_aliases = { '?': 'help', 'ci': 'commit', 'checkin': 'commit', 'di': 'diff', 'st': 'status', 'stat': 'status', } def get_cmd_handler(cmd): cmd = str(cmd) cmd = cmd_aliases.get(cmd, cmd) try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: raise BzrError("unknown command %r" % cmd) return cmd, cmd_handler def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. if file_list: file_list = [b.relpath(f) for f in file_list] # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and (new_name not in file_list): continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) sys.stdout.writelines(ud) if nonl: print "\\ No newline at end of file" sys.stdout.write('\n') if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_dump_text_inventory(): import bzrlib.textinv inv = Branch('.').basis_tree().inventory bzrlib.textinv.write_text_inventory(inv, sys.stdout) def cmd_load_text_inventory(): import bzrlib.textinv inv = bzrlib.textinv.read_text_inventory(sys.stdin) print 'loaded %d entries' % len(inv) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original', verbose=False): """Show log of this branch. TODO: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone, verbose=verbose) def cmd_ls(revision=None, verbose=False): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignore(name_pattern): """Ignore a command or pattern""" b = Branch('.') f = open(b.abspath('.bzrignore'), 'at') f.write(name_pattern + '\n') f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_whoami(email=False): """Show bzr user id. usage: bzr whoami options: --email Show only the email address. """ if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ elif topic == 'commands': help_commands() else: # otherwise, maybe the name of a command? topic, cmdfn = get_cmd_handler(topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def help_commands(): """List all commands""" accu = [] for k in globals().keys(): if k.startswith('cmd_'): accu.append(k[4:].replace('_','-')) accu.sort() print "bzr commands: " for x in accu: print " " + x print "note: some of these commands are internal-use or obsolete" # TODO: Some kind of marker for internal-use commands? # TODO: Show aliases? def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone', 'verbose'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], 'whoami': ['email'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'ignore': ['name_pattern'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_handler = get_cmd_handler(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(canonical_cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(canonical_cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: return cmd_handler(**cmdargs) or 0 def _report_exception(e, summary): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception(e) tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def cmd_assert_fail(): assert False, "always fails" def main(argv): bzrlib.trace.create_tracefile(argv) try: try: ret = run_bzr(argv) return ret except BzrError, e: _report_exception(e, 'error: ' + e.args[0]) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(e, msg) except Exception, e: _report_exception(e, 'exception: %s' % str(e).rstrip('\n')) return 1 finally: bzrlib.trace.close_trace() ## TODO: Trap AssertionError # TODO: Maybe nicer handling of IOError especially for broken pipe. if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :311 committer Martin Pool 1114684304 +1000 data 4 todo from :310 M 644 inline bzrlib/commands.py data 33251 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool http://bazaar-ng.org/ **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** * Metadata format is not stable yet -- you may need to discard history in the future. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr rename FROM TO Rename one file. bzr move FROM... DESTDIR Move one or more files to a different directory. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. bzr check Verify history is stored safely. (for more type 'bzr help commands') """ import sys, os, time, types, shutil, tempfile, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. cmd_aliases = { '?': 'help', 'ci': 'commit', 'checkin': 'commit', 'di': 'diff', 'st': 'status', 'stat': 'status', } def get_cmd_handler(cmd): cmd = str(cmd) cmd = cmd_aliases.get(cmd, cmd) try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: raise BzrError("unknown command %r" % cmd) return cmd, cmd_handler def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. if file_list: file_list = [b.relpath(f) for f in file_list] # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and (new_name not in file_list): continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) sys.stdout.writelines(ud) if nonl: print "\\ No newline at end of file" sys.stdout.write('\n') if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_dump_text_inventory(): import bzrlib.textinv inv = Branch('.').basis_tree().inventory bzrlib.textinv.write_text_inventory(inv, sys.stdout) def cmd_load_text_inventory(): import bzrlib.textinv inv = bzrlib.textinv.read_text_inventory(sys.stdin) print 'loaded %d entries' % len(inv) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original', verbose=False): """Show log of this branch. TODO: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone, verbose=verbose) def cmd_ls(revision=None, verbose=False): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignore(name_pattern): """Ignore a command or pattern""" b = Branch('.') # XXX: This will fail if it's a hardlink; should use an AtomicFile class. f = open(b.abspath('.bzrignore'), 'at') f.write(name_pattern + '\n') f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_whoami(email=False): """Show bzr user id. usage: bzr whoami options: --email Show only the email address. """ if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ elif topic == 'commands': help_commands() else: # otherwise, maybe the name of a command? topic, cmdfn = get_cmd_handler(topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def help_commands(): """List all commands""" accu = [] for k in globals().keys(): if k.startswith('cmd_'): accu.append(k[4:].replace('_','-')) accu.sort() print "bzr commands: " for x in accu: print " " + x print "note: some of these commands are internal-use or obsolete" # TODO: Some kind of marker for internal-use commands? # TODO: Show aliases? def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone', 'verbose'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], 'whoami': ['email'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'ignore': ['name_pattern'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_handler = get_cmd_handler(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(canonical_cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(canonical_cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: return cmd_handler(**cmdargs) or 0 def _report_exception(e, summary): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception(e) tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def cmd_assert_fail(): assert False, "always fails" def main(argv): bzrlib.trace.create_tracefile(argv) try: try: ret = run_bzr(argv) return ret except BzrError, e: _report_exception(e, 'error: ' + e.args[0]) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(e, msg) except Exception, e: _report_exception(e, 'exception: %s' % str(e).rstrip('\n')) return 1 finally: bzrlib.trace.close_trace() ## TODO: Trap AssertionError # TODO: Maybe nicer handling of IOError especially for broken pipe. if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :312 committer Martin Pool 1114733430 +1000 data 4 todo from :311 M 644 inline TODO data 5228 -*- indented-text -*- See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be "bzr export -r REV". * "cat -rREV FILE" * Plugins that provide commands. By just installing a file into some directory (e.g. /usr/share/bzr/plugins) it should be possible to create new top-level commands ("bzr frob"). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * "bzr info" could show space used by working tree, versioned files, unknown and ignored files. * "bzr info" should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * Tidier error for EPIPE: should be just "bzr: broken pipe" with no other details because debugging information is rarely interesting. * On Windows, command-line arguments should be glob-expanded__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * 'bzr ignore' command that just adds a line to the .bzrignore file and makes it versioned. * 'bzr help commands' should give a one-line summary of each command. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. Medium things ------------- * Display command grammar in help messages rather than hardcoding it. * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * mv command? * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * Consider using Python logging library as well as/instead of bzrlib.trace. * Change to using gettext message localization. Large things ------------ * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface commit refs/heads/master mark :313 committer Martin Pool 1114735414 +1000 data 29 - update install instructions from :312 M 644 inline README data 1211 *********************************** Release notes for Bazaar-NG (pre-0) *********************************** mbp@sourcefrog.net, March 2005, Canberra Caveats ------- * There is little locking or transaction control here; if you interrupt it the tree may be arbitrarily broken. This will be fixed. * Don't use this for critical data; at the very least keep separate regular snapshots of your tree. Dependencies ------------ This is mostly developed on Linux (Ubuntu); it should work on Unix, Windows, or OS X with relatively little trouble. The only dependency is Python, at least 2.3 and preferably 2.4. On Windows, Python2.4 is required. You may optionally install cElementTree to speed up some operations. Installation ------------ The best way to install bzr is to symlink the ``bzr`` command onto a directory on your path. For example:: ln -s ~/work/bzr/bzr ~/bin/bzr If you use a symlink for this, Python will be able to automatically find the bzr libraries. Otherwise you must ensure they are listed on your $PYTHONPATH. If you use the setup.py script then bzr will be installed into the specified path. In this case you must install ElementTree or cElementTree separately. commit refs/heads/master mark :314 committer Martin Pool 1114735799 +1000 data 29 - Update ElementTree to 1.2.6 from :313 M 644 inline NEWS data 4431 bzr-0.0.5 NOT RELEASED YET ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New 'bzr ignore PATTERN' command. TESTING: * Converted black-box test suites from Bourne shell into Python. Various structural improvements to the tests. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline elementtree/ElementTree.py data 40886 # # ElementTree # $Id: ElementTree.py 2326 2005-03-17 07:45:21Z fredrik $ # # light-weight XML support for Python 1.5.2 and later. # # history: # 2001-10-20 fl created (from various sources) # 2001-11-01 fl return root from parse method # 2002-02-16 fl sort attributes in lexical order # 2002-04-06 fl TreeBuilder refactoring, added PythonDoc markup # 2002-05-01 fl finished TreeBuilder refactoring # 2002-07-14 fl added basic namespace support to ElementTree.write # 2002-07-25 fl added QName attribute support # 2002-10-20 fl fixed encoding in write # 2002-11-24 fl changed default encoding to ascii; fixed attribute encoding # 2002-11-27 fl accept file objects or file names for parse/write # 2002-12-04 fl moved XMLTreeBuilder back to this module # 2003-01-11 fl fixed entity encoding glitch for us-ascii # 2003-02-13 fl added XML literal factory # 2003-02-21 fl added ProcessingInstruction/PI factory # 2003-05-11 fl added tostring/fromstring helpers # 2003-05-26 fl added ElementPath support # 2003-07-05 fl added makeelement factory method # 2003-07-28 fl added more well-known namespace prefixes # 2003-08-15 fl fixed typo in ElementTree.findtext (Thomas Dartsch) # 2003-09-04 fl fall back on emulator if ElementPath is not installed # 2003-10-31 fl markup updates # 2003-11-15 fl fixed nested namespace bug # 2004-03-28 fl added XMLID helper # 2004-06-02 fl added default support to findtext # 2004-06-08 fl fixed encoding of non-ascii element/attribute names # 2004-08-23 fl take advantage of post-2.1 expat features # 2005-02-01 fl added iterparse implementation # 2005-03-02 fl fixed iterparse support for pre-2.2 versions # # Copyright (c) 1999-2005 by Fredrik Lundh. All rights reserved. # # fredrik@pythonware.com # http://www.pythonware.com # # -------------------------------------------------------------------- # The ElementTree toolkit is # # Copyright (c) 1999-2005 by Fredrik Lundh # # By obtaining, using, and/or copying this software and/or its # associated documentation, you agree that you have read, understood, # and will comply with the following terms and conditions: # # Permission to use, copy, modify, and distribute this software and # its associated documentation for any purpose and without fee is # hereby granted, provided that the above copyright notice appears in # all copies, and that both that copyright notice and this permission # notice appear in supporting documentation, and that the name of # Secret Labs AB or the author not be used in advertising or publicity # pertaining to distribution of the software without specific, written # prior permission. # # SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD # TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- # ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR # BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS # ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE # OF THIS SOFTWARE. # -------------------------------------------------------------------- __all__ = [ # public symbols "Comment", "dump", "Element", "ElementTree", "fromstring", "iselement", "iterparse", "parse", "PI", "ProcessingInstruction", "QName", "SubElement", "tostring", "TreeBuilder", "VERSION", "XML", "XMLTreeBuilder", ] ## # The Element type is a flexible container object, designed to # store hierarchical data structures in memory. The type can be # described as a cross between a list and a dictionary. #

# Each element has a number of properties associated with it: #

# # To create an element instance, use the {@link #Element} or {@link # #SubElement} factory functions. #

# The {@link #ElementTree} class can be used to wrap an element # structure, and convert it from and to XML. ## import string, sys, re class _SimpleElementPath: # emulate pre-1.2 find/findtext/findall behaviour def find(self, element, tag): for elem in element: if elem.tag == tag: return elem return None def findtext(self, element, tag, default=None): for elem in element: if elem.tag == tag: return elem.text or "" return default def findall(self, element, tag): if tag[:3] == ".//": return element.getiterator(tag[3:]) result = [] for elem in element: if elem.tag == tag: result.append(elem) return result try: import ElementPath except ImportError: # FIXME: issue warning in this case? ElementPath = _SimpleElementPath() # TODO: add support for custom namespace resolvers/default namespaces # TODO: add improved support for incremental parsing VERSION = "1.2.6" ## # Internal element class. This class defines the Element interface, # and provides a reference implementation of this interface. #

# You should not create instances of this class directly. Use the # appropriate factory functions instead, such as {@link #Element} # and {@link #SubElement}. # # @see Element # @see SubElement # @see Comment # @see ProcessingInstruction class _ElementInterface: # text...tail ## # (Attribute) Element tag. tag = None ## # (Attribute) Element attribute dictionary. Where possible, use # {@link #_ElementInterface.get}, # {@link #_ElementInterface.set}, # {@link #_ElementInterface.keys}, and # {@link #_ElementInterface.items} to access # element attributes. attrib = None ## # (Attribute) Text before first subelement. This is either a # string or the value None, if there was no text. text = None ## # (Attribute) Text after this element's end tag, but before the # next sibling element's start tag. This is either a string or # the value None, if there was no text. tail = None # text after end tag, if any def __init__(self, tag, attrib): self.tag = tag self.attrib = attrib self._children = [] def __repr__(self): return "" % (self.tag, id(self)) ## # Creates a new element object of the same type as this element. # # @param tag Element tag. # @param attrib Element attributes, given as a dictionary. # @return A new element instance. def makeelement(self, tag, attrib): return Element(tag, attrib) ## # Returns the number of subelements. # # @return The number of subelements. def __len__(self): return len(self._children) ## # Returns the given subelement. # # @param index What subelement to return. # @return The given subelement. # @exception IndexError If the given element does not exist. def __getitem__(self, index): return self._children[index] ## # Replaces the given subelement. # # @param index What subelement to replace. # @param element The new element value. # @exception IndexError If the given element does not exist. # @exception AssertionError If element is not a valid object. def __setitem__(self, index, element): assert iselement(element) self._children[index] = element ## # Deletes the given subelement. # # @param index What subelement to delete. # @exception IndexError If the given element does not exist. def __delitem__(self, index): del self._children[index] ## # Returns a list containing subelements in the given range. # # @param start The first subelement to return. # @param stop The first subelement that shouldn't be returned. # @return A sequence object containing subelements. def __getslice__(self, start, stop): return self._children[start:stop] ## # Replaces a number of subelements with elements from a sequence. # # @param start The first subelement to replace. # @param stop The first subelement that shouldn't be replaced. # @param elements A sequence object with zero or more elements. # @exception AssertionError If a sequence member is not a valid object. def __setslice__(self, start, stop, elements): for element in elements: assert iselement(element) self._children[start:stop] = list(elements) ## # Deletes a number of subelements. # # @param start The first subelement to delete. # @param stop The first subelement to leave in there. def __delslice__(self, start, stop): del self._children[start:stop] ## # Adds a subelement to the end of this element. # # @param element The element to add. # @exception AssertionError If a sequence member is not a valid object. def append(self, element): assert iselement(element) self._children.append(element) ## # Inserts a subelement at the given position in this element. # # @param index Where to insert the new subelement. # @exception AssertionError If the element is not a valid object. def insert(self, index, element): assert iselement(element) self._children.insert(index, element) ## # Removes a matching subelement. Unlike the find methods, # this method compares elements based on identity, not on tag # value or contents. # # @param element What element to remove. # @exception ValueError If a matching element could not be found. # @exception AssertionError If the element is not a valid object. def remove(self, element): assert iselement(element) self._children.remove(element) ## # Returns all subelements. The elements are returned in document # order. # # @return A list of subelements. # @defreturn list of Element instances def getchildren(self): return self._children ## # Finds the first matching subelement, by tag name or path. # # @param path What element to look for. # @return The first matching element, or None if no element was found. # @defreturn Element or None def find(self, path): return ElementPath.find(self, path) ## # Finds text for the first matching subelement, by tag name or path. # # @param path What element to look for. # @param default What to return if the element was not found. # @return The text content of the first matching element, or the # default value no element was found. Note that if the element # has is found, but has no text content, this method returns an # empty string. # @defreturn string def findtext(self, path, default=None): return ElementPath.findtext(self, path, default) ## # Finds all matching subelements, by tag name or path. # # @param path What element to look for. # @return A list or iterator containing all matching elements, # in document order. # @defreturn list of Element instances def findall(self, path): return ElementPath.findall(self, path) ## # Resets an element. This function removes all subelements, clears # all attributes, and sets the text and tail attributes to None. def clear(self): self.attrib.clear() self._children = [] self.text = self.tail = None ## # Gets an element attribute. # # @param key What attribute to look for. # @param default What to return if the attribute was not found. # @return The attribute value, or the default value, if the # attribute was not found. # @defreturn string or None def get(self, key, default=None): return self.attrib.get(key, default) ## # Sets an element attribute. # # @param key What attribute to set. # @param value The attribute value. def set(self, key, value): self.attrib[key] = value ## # Gets a list of attribute names. The names are returned in an # arbitrary order (just like for an ordinary Python dictionary). # # @return A list of element attribute names. # @defreturn list of strings def keys(self): return self.attrib.keys() ## # Gets element attributes, as a sequence. The attributes are # returned in an arbitrary order. # # @return A list of (name, value) tuples for all attributes. # @defreturn list of (string, string) tuples def items(self): return self.attrib.items() ## # Creates a tree iterator. The iterator loops over this element # and all subelements, in document order, and returns all elements # with a matching tag. #

# If the tree structure is modified during iteration, the result # is undefined. # # @param tag What tags to look for (default is to return all elements). # @return A list or iterator containing all the matching elements. # @defreturn list or iterator def getiterator(self, tag=None): nodes = [] if tag == "*": tag = None if tag is None or self.tag == tag: nodes.append(self) for node in self._children: nodes.extend(node.getiterator(tag)) return nodes # compatibility _Element = _ElementInterface ## # Element factory. This function returns an object implementing the # standard Element interface. The exact class or type of that object # is implementation dependent, but it will always be compatible with # the {@link #_ElementInterface} class in this module. #

# The element name, attribute names, and attribute values can be # either 8-bit ASCII strings or Unicode strings. # # @param tag The element name. # @param attrib An optional dictionary, containing element attributes. # @param **extra Additional attributes, given as keyword arguments. # @return An element instance. # @defreturn Element def Element(tag, attrib={}, **extra): attrib = attrib.copy() attrib.update(extra) return _ElementInterface(tag, attrib) ## # Subelement factory. This function creates an element instance, and # appends it to an existing element. #

# The element name, attribute names, and attribute values can be # either 8-bit ASCII strings or Unicode strings. # # @param parent The parent element. # @param tag The subelement name. # @param attrib An optional dictionary, containing element attributes. # @param **extra Additional attributes, given as keyword arguments. # @return An element instance. # @defreturn Element def SubElement(parent, tag, attrib={}, **extra): attrib = attrib.copy() attrib.update(extra) element = parent.makeelement(tag, attrib) parent.append(element) return element ## # Comment element factory. This factory function creates a special # element that will be serialized as an XML comment. #

# The comment string can be either an 8-bit ASCII string or a Unicode # string. # # @param text A string containing the comment string. # @return An element instance, representing a comment. # @defreturn Element def Comment(text=None): element = Element(Comment) element.text = text return element ## # PI element factory. This factory function creates a special element # that will be serialized as an XML processing instruction. # # @param target A string containing the PI target. # @param text A string containing the PI contents, if any. # @return An element instance, representing a PI. # @defreturn Element def ProcessingInstruction(target, text=None): element = Element(ProcessingInstruction) element.text = target if text: element.text = element.text + " " + text return element PI = ProcessingInstruction ## # QName wrapper. This can be used to wrap a QName attribute value, in # order to get proper namespace handling on output. # # @param text A string containing the QName value, in the form {uri}local, # or, if the tag argument is given, the URI part of a QName. # @param tag Optional tag. If given, the first argument is interpreted as # an URI, and this argument is interpreted as a local name. # @return An opaque object, representing the QName. class QName: def __init__(self, text_or_uri, tag=None): if tag: text_or_uri = "{%s}%s" % (text_or_uri, tag) self.text = text_or_uri def __str__(self): return self.text def __hash__(self): return hash(self.text) def __cmp__(self, other): if isinstance(other, QName): return cmp(self.text, other.text) return cmp(self.text, other) ## # ElementTree wrapper class. This class represents an entire element # hierarchy, and adds some extra support for serialization to and from # standard XML. # # @param element Optional root element. # @keyparam file Optional file handle or name. If given, the # tree is initialized with the contents of this XML file. class ElementTree: def __init__(self, element=None, file=None): assert element is None or iselement(element) self._root = element # first node if file: self.parse(file) ## # Gets the root element for this tree. # # @return An element instance. # @defreturn Element def getroot(self): return self._root ## # Replaces the root element for this tree. This discards the # current contents of the tree, and replaces it with the given # element. Use with care. # # @param element An element instance. def _setroot(self, element): assert iselement(element) self._root = element ## # Loads an external XML document into this element tree. # # @param source A file name or file object. # @param parser An optional parser instance. If not given, the # standard {@link XMLTreeBuilder} parser is used. # @return The document root element. # @defreturn Element def parse(self, source, parser=None): if not hasattr(source, "read"): source = open(source, "rb") if not parser: parser = XMLTreeBuilder() while 1: data = source.read(32768) if not data: break parser.feed(data) self._root = parser.close() return self._root ## # Creates a tree iterator for the root element. The iterator loops # over all elements in this tree, in document order. # # @param tag What tags to look for (default is to return all elements) # @return An iterator. # @defreturn iterator def getiterator(self, tag=None): assert self._root is not None return self._root.getiterator(tag) ## # Finds the first toplevel element with given tag. # Same as getroot().find(path). # # @param path What element to look for. # @return The first matching element, or None if no element was found. # @defreturn Element or None def find(self, path): assert self._root is not None if path[:1] == "/": path = "." + path return self._root.find(path) ## # Finds the element text for the first toplevel element with given # tag. Same as getroot().findtext(path). # # @param path What toplevel element to look for. # @param default What to return if the element was not found. # @return The text content of the first matching element, or the # default value no element was found. Note that if the element # has is found, but has no text content, this method returns an # empty string. # @defreturn string def findtext(self, path, default=None): assert self._root is not None if path[:1] == "/": path = "." + path return self._root.findtext(path, default) ## # Finds all toplevel elements with the given tag. # Same as getroot().findall(path). # # @param path What element to look for. # @return A list or iterator containing all matching elements, # in document order. # @defreturn list of Element instances def findall(self, path): assert self._root is not None if path[:1] == "/": path = "." + path return self._root.findall(path) ## # Writes the element tree to a file, as XML. # # @param file A file name, or a file object opened for writing. # @param encoding Optional output encoding (default is US-ASCII). def write(self, file, encoding="us-ascii"): assert self._root is not None if not hasattr(file, "write"): file = open(file, "wb") if not encoding: encoding = "us-ascii" elif encoding != "utf-8" and encoding != "us-ascii": file.write("\n" % encoding) self._write(file, self._root, encoding, {}) def _write(self, file, node, encoding, namespaces): # write XML to file tag = node.tag if tag is Comment: file.write("" % _escape_cdata(node.text, encoding)) elif tag is ProcessingInstruction: file.write("" % _escape_cdata(node.text, encoding)) else: items = node.items() xmlns_items = [] # new namespaces in this scope try: if isinstance(tag, QName) or tag[:1] == "{": tag, xmlns = fixtag(tag, namespaces) if xmlns: xmlns_items.append(xmlns) except TypeError: _raise_serialization_error(tag) file.write("<" + _encode(tag, encoding)) if items or xmlns_items: items.sort() # lexical order for k, v in items: try: if isinstance(k, QName) or k[:1] == "{": k, xmlns = fixtag(k, namespaces) if xmlns: xmlns_items.append(xmlns) except TypeError: _raise_serialization_error(k) try: if isinstance(v, QName): v, xmlns = fixtag(v, namespaces) if xmlns: xmlns_items.append(xmlns) except TypeError: _raise_serialization_error(v) file.write(" %s=\"%s\"" % (_encode(k, encoding), _escape_attrib(v, encoding))) for k, v in xmlns_items: file.write(" %s=\"%s\"" % (_encode(k, encoding), _escape_attrib(v, encoding))) if node.text or len(node): file.write(">") if node.text: file.write(_escape_cdata(node.text, encoding)) for n in node: self._write(file, n, encoding, namespaces) file.write("") else: file.write(" />") for k, v in xmlns_items: del namespaces[v] if node.tail: file.write(_escape_cdata(node.tail, encoding)) # -------------------------------------------------------------------- # helpers ## # Checks if an object appears to be a valid element object. # # @param An element instance. # @return A true value if this is an element object. # @defreturn flag def iselement(element): # FIXME: not sure about this; might be a better idea to look # for tag/attrib/text attributes return isinstance(element, _ElementInterface) or hasattr(element, "tag") ## # Writes an element tree or element structure to sys.stdout. This # function should be used for debugging only. #

# The exact output format is implementation dependent. In this # version, it's written as an ordinary XML file. # # @param elem An element tree or an individual element. def dump(elem): # debugging if not isinstance(elem, ElementTree): elem = ElementTree(elem) elem.write(sys.stdout) tail = elem.getroot().tail if not tail or tail[-1] != "\n": sys.stdout.write("\n") def _encode(s, encoding): try: return s.encode(encoding) except AttributeError: return s # 1.5.2: assume the string uses the right encoding if sys.version[:3] == "1.5": _escape = re.compile(r"[&<>\"\x80-\xff]+") # 1.5.2 else: _escape = re.compile(eval(r'u"[&<>\"\u0080-\uffff]+"')) _escape_map = { "&": "&", "<": "<", ">": ">", '"': """, } _namespace_map = { # "well-known" namespace prefixes "http://www.w3.org/XML/1998/namespace": "xml", "http://www.w3.org/1999/xhtml": "html", "http://www.w3.org/1999/02/22-rdf-syntax-ns#": "rdf", "http://schemas.xmlsoap.org/wsdl/": "wsdl", } def _raise_serialization_error(text): raise TypeError( "cannot serialize %r (type %s)" % (text, type(text).__name__) ) def _encode_entity(text, pattern=_escape): # map reserved and non-ascii characters to numerical entities def escape_entities(m, map=_escape_map): out = [] append = out.append for char in m.group(): text = map.get(char) if text is None: text = "&#%d;" % ord(char) append(text) return string.join(out, "") try: return _encode(pattern.sub(escape_entities, text), "ascii") except TypeError: _raise_serialization_error(text) # # the following functions assume an ascii-compatible encoding # (or "utf-16") def _escape_cdata(text, encoding=None, replace=string.replace): # escape character data try: if encoding: try: text = _encode(text, encoding) except UnicodeError: return _encode_entity(text) text = replace(text, "&", "&") text = replace(text, "<", "<") text = replace(text, ">", ">") return text except (TypeError, AttributeError): _raise_serialization_error(text) def _escape_attrib(text, encoding=None, replace=string.replace): # escape attribute value try: if encoding: try: text = _encode(text, encoding) except UnicodeError: return _encode_entity(text) text = replace(text, "&", "&") text = replace(text, "'", "'") # FIXME: overkill text = replace(text, "\"", """) text = replace(text, "<", "<") text = replace(text, ">", ">") return text except (TypeError, AttributeError): _raise_serialization_error(text) def fixtag(tag, namespaces): # given a decorated tag (of the form {uri}tag), return prefixed # tag and namespace declaration, if any if isinstance(tag, QName): tag = tag.text namespace_uri, tag = string.split(tag[1:], "}", 1) prefix = namespaces.get(namespace_uri) if prefix is None: prefix = _namespace_map.get(namespace_uri) if prefix is None: prefix = "ns%d" % len(namespaces) namespaces[namespace_uri] = prefix if prefix == "xml": xmlns = None else: xmlns = ("xmlns:%s" % prefix, namespace_uri) else: xmlns = None return "%s:%s" % (prefix, tag), xmlns ## # Parses an XML document into an element tree. # # @param source A filename or file object containing XML data. # @param parser An optional parser instance. If not given, the # standard {@link XMLTreeBuilder} parser is used. # @return An ElementTree instance def parse(source, parser=None): tree = ElementTree() tree.parse(source, parser) return tree ## # Parses an XML document into an element tree incrementally, and reports # what's going on to the user. # # @param source A filename or file object containing XML data. # @param events A list of events to report back. If omitted, only "end" # events are reported. # @return A (event, elem) iterator. class iterparse: def __init__(self, source, events=None): if not hasattr(source, "read"): source = open(source, "rb") self._file = source self._events = [] self._index = 0 self.root = self._root = None self._parser = XMLTreeBuilder() # wire up the parser for event reporting parser = self._parser._parser append = self._events.append if events is None: events = ["end"] for event in events: if event == "start": try: parser.ordered_attributes = 1 parser.specified_attributes = 1 def handler(tag, attrib_in, event=event, append=append, start=self._parser._start_list): append((event, start(tag, attrib_in))) parser.StartElementHandler = handler except AttributeError: def handler(tag, attrib_in, event=event, append=append, start=self._parser._start): append((event, start(tag, attrib_in))) parser.StartElementHandler = handler elif event == "end": def handler(tag, event=event, append=append, end=self._parser._end): append((event, end(tag))) parser.EndElementHandler = handler elif event == "start-ns": def handler(prefix, uri, event=event, append=append): try: uri = _encode(uri, "ascii") except UnicodeError: pass append((event, (prefix or "", uri))) parser.StartNamespaceDeclHandler = handler elif event == "end-ns": def handler(prefix, event=event, append=append): append((event, None)) parser.EndNamespaceDeclHandler = handler def next(self): while 1: try: item = self._events[self._index] except IndexError: if self._parser is None: self.root = self._root try: raise StopIteration except NameError: raise IndexError # load event buffer del self._events[:] self._index = 0 data = self._file.read(16384) if data: self._parser.feed(data) else: self._root = self._parser.close() self._parser = None else: self._index = self._index + 1 return item try: iter def __iter__(self): return self except NameError: def __getitem__(self, index): return self.next() ## # Parses an XML document from a string constant. This function can # be used to embed "XML literals" in Python code. # # @param source A string containing XML data. # @return An Element instance. # @defreturn Element def XML(text): parser = XMLTreeBuilder() parser.feed(text) return parser.close() ## # Parses an XML document from a string constant, and also returns # a dictionary which maps from element id:s to elements. # # @param source A string containing XML data. # @return A tuple containing an Element instance and a dictionary. # @defreturn (Element, dictionary) def XMLID(text): parser = XMLTreeBuilder() parser.feed(text) tree = parser.close() ids = {} for elem in tree.getiterator(): id = elem.get("id") if id: ids[id] = elem return tree, ids ## # Parses an XML document from a string constant. Same as {@link #XML}. # # @def fromstring(text) # @param source A string containing XML data. # @return An Element instance. # @defreturn Element fromstring = XML ## # Generates a string representation of an XML element, including all # subelements. # # @param element An Element instance. # @return An encoded string containing the XML data. # @defreturn string def tostring(element, encoding=None): class dummy: pass data = [] file = dummy() file.write = data.append ElementTree(element).write(file, encoding) return string.join(data, "") ## # Generic element structure builder. This builder converts a sequence # of {@link #TreeBuilder.start}, {@link #TreeBuilder.data}, and {@link # #TreeBuilder.end} method calls to a well-formed element structure. #

# You can use this class to build an element structure using a custom XML # parser, or a parser for some other XML-like format. # # @param element_factory Optional element factory. This factory # is called to create new Element instances, as necessary. class TreeBuilder: def __init__(self, element_factory=None): self._data = [] # data collector self._elem = [] # element stack self._last = None # last element self._tail = None # true if we're after an end tag if element_factory is None: element_factory = _ElementInterface self._factory = element_factory ## # Flushes the parser buffers, and returns the toplevel documen # element. # # @return An Element instance. # @defreturn Element def close(self): assert len(self._elem) == 0, "missing end tags" assert self._last != None, "missing toplevel element" return self._last def _flush(self): if self._data: if self._last is not None: text = string.join(self._data, "") if self._tail: assert self._last.tail is None, "internal error (tail)" self._last.tail = text else: assert self._last.text is None, "internal error (text)" self._last.text = text self._data = [] ## # Adds text to the current element. # # @param data A string. This should be either an 8-bit string # containing ASCII text, or a Unicode string. def data(self, data): self._data.append(data) ## # Opens a new element. # # @param tag The element name. # @param attrib A dictionary containing element attributes. # @return The opened element. # @defreturn Element def start(self, tag, attrs): self._flush() self._last = elem = self._factory(tag, attrs) if self._elem: self._elem[-1].append(elem) self._elem.append(elem) self._tail = 0 return elem ## # Closes the current element. # # @param tag The element name. # @return The closed element. # @defreturn Element def end(self, tag): self._flush() self._last = self._elem.pop() assert self._last.tag == tag,\ "end tag mismatch (expected %s, got %s)" % ( self._last.tag, tag) self._tail = 1 return self._last ## # Element structure builder for XML source data, based on the # expat parser. # # @keyparam target Target object. If omitted, the builder uses an # instance of the standard {@link #TreeBuilder} class. # @keyparam html Predefine HTML entities. This flag is not supported # by the current implementation. # @see #ElementTree # @see #TreeBuilder class XMLTreeBuilder: def __init__(self, html=0, target=None): try: from xml.parsers import expat except ImportError: raise ImportError( "No module named expat; use SimpleXMLTreeBuilder instead" ) self._parser = parser = expat.ParserCreate(None, "}") if target is None: target = TreeBuilder() self._target = target self._names = {} # name memo cache # callbacks parser.DefaultHandlerExpand = self._default parser.StartElementHandler = self._start parser.EndElementHandler = self._end parser.CharacterDataHandler = self._data # let expat do the buffering, if supported try: self._parser.buffer_text = 1 except AttributeError: pass # use new-style attribute handling, if supported try: self._parser.ordered_attributes = 1 self._parser.specified_attributes = 1 parser.StartElementHandler = self._start_list except AttributeError: pass encoding = None if not parser.returns_unicode: encoding = "utf-8" # target.xml(encoding, None) self._doctype = None self.entity = {} def _fixtext(self, text): # convert text string to ascii, if possible try: return _encode(text, "ascii") except UnicodeError: return text def _fixname(self, key): # expand qname, and convert name string to ascii, if possible try: name = self._names[key] except KeyError: name = key if "}" in name: name = "{" + name self._names[key] = name = self._fixtext(name) return name def _start(self, tag, attrib_in): fixname = self._fixname tag = fixname(tag) attrib = {} for key, value in attrib_in.items(): attrib[fixname(key)] = self._fixtext(value) return self._target.start(tag, attrib) def _start_list(self, tag, attrib_in): fixname = self._fixname tag = fixname(tag) attrib = {} if attrib_in: for i in range(0, len(attrib_in), 2): attrib[fixname(attrib_in[i])] = self._fixtext(attrib_in[i+1]) return self._target.start(tag, attrib) def _data(self, text): return self._target.data(self._fixtext(text)) def _end(self, tag): return self._target.end(self._fixname(tag)) def _default(self, text): prefix = text[:1] if prefix == "&": # deal with undefined entities try: self._target.data(self.entity[text[1:-1]]) except KeyError: from xml.parsers import expat raise expat.error( "undefined entity %s: line %d, column %d" % (text, self._parser.ErrorLineNumber, self._parser.ErrorColumnNumber) ) elif prefix == "<" and text[:9] == "": self._doctype = None return text = string.strip(text) if not text: return self._doctype.append(text) n = len(self._doctype) if n > 2: type = self._doctype[1] if type == "PUBLIC" and n == 4: name, type, pubid, system = self._doctype elif type == "SYSTEM" and n == 3: name, type, system = self._doctype pubid = None else: return if pubid: pubid = pubid[1:-1] self.doctype(name, pubid, system[1:-1]) self._doctype = None ## # Handles a doctype declaration. # # @param name Doctype name. # @param pubid Public identifier. # @param system System identifier. def doctype(self, name, pubid, system): pass ## # Feeds data to the parser. # # @param data Encoded data. def feed(self, data): self._parser.Parse(data, 0) ## # Finishes feeding data to the parser. # # @return An element structure. # @defreturn Element def close(self): self._parser.Parse("", 1) # end of data tree = self._target.close() del self._target, self._parser # get rid of circular references return tree commit refs/heads/master mark :315 committer Martin Pool 1114745560 +1000 data 4 todo from :314 M 644 inline TODO data 5736 -*- indented-text -*- See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be "bzr export -r REV". * "cat -rREV FILE" * Plugins that provide commands. By just installing a file into some directory (e.g. /usr/share/bzr/plugins) it should be possible to create new top-level commands ("bzr frob"). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * "bzr info" could show space used by working tree, versioned files, unknown and ignored files. * "bzr info" should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * Tidier error for EPIPE: should be just "bzr: broken pipe" with no other details because debugging information is rarely interesting. * On Windows, command-line arguments should be glob-expanded__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * 'bzr ignore' command that just adds a line to the .bzrignore file and makes it versioned. * 'bzr help commands' should give a one-line summary of each command. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. Medium things ------------- * Display command grammar in help messages rather than hardcoding it. * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * mv command? * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * Consider using Python logging library as well as/instead of bzrlib.trace. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers. * Commands to dump out all command help into a manpage or HTML file or whatever. Large things ------------ * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface commit refs/heads/master mark :316 committer Martin Pool 1115007873 +1000 data 19 doc: notes on merge from :315 M 644 inline doc/merge.txt data 6669 ======= Merging ======= There should be one merge command which does the right thing, and which is called 'merge'. The merge command pulls a changeset or a range of changesets into your tree. It knows what changes have already been integrated and avoids pulling them again. There should be some intelligence about working out what changes have already been merged. The tool intelligently chooses (or perhaps synthesizes) an ancestor and two trees to merge. These are then intelligently merged. Merge should refuse to run (unless forced) if there are any uncommitted changes in your tree beforehand. This has two purposes: if you mess up the merge you won't lose anything important; secondly this makes it more likely that the merge will be relatively pure. It is a good idea to commit as soon as a merge is complete and satisfactorily resolved, so as to protect the work you did in the merge and to keep it separate from later development. (Mark suggests an option to automatically commit when the merge is complete.) Recording merges ---------------- bzr records what branches have been merged so far. This is useful as historical information and also for later choosing a merge ancestor. For each revision we record all the other revisions which have come into this tree, either by being completely merged or as cherry-picks. (This design is similar to the PatchLogPruning__ draft from baz.) __ http://wiki.gnuarch.org/PatchLogPruning This list of merged revisions is generally append-only, but can be reduced if changes are taken back out. Changes can be anti-cherry-picked, which causes any successors to change from being fully-merged to being cherry-picked. The list of merged patches is stored delta-compressed. ``tla update`` -------------- ``tla update`` performs a useful but slightly subtle change: it pulls in only changes that have been made on the other branch since you last merged. That is to say, it sets the merge basis as the most recent merged-from point on the other branch. This means that any changes which were taken from your branch into the other and then reversed or modified will not be reversed. Those changes will always be considered as new in your branch and will have precedence. The basic idea of a merge that only brings in remote work and doesn't revert your own changes is good. It could be handled by a three way merge with a specified version but perhaps there is a better way. Merging tree shape ------------------ Merge is conducted at two levels: merging the tree shape, and merging the file contents. Merging the tree shape means accounting for renames, adds, deletes, etc. This is almost the same as merging the two inventories, but we need to do a smart merge on them to enforce structural invariants. Interrupting a merge -------------------- Some tools insist that you complete the entire merge while the ``merge`` command is running; you cannot exit the program or restart the computer because state is held in memory. We should avoid that. At least when the tool is waiting for user input it should have written everything to disk sufficient to pick up and continue the merge from that point. This suggests that there should be a command to continue a merge; perhaps ``bzr resolve`` should look for any unresolved changes and start resolving them. ``bzr merge`` can (by default) automatically start this process. One hard aspect is transformation of the tree state such as renames, directory creation, etc. This might include files swapping place, etc. We would like to do atomically but cannot. Aborting a merge ---------------- If a merge has been begun but not committed then ``bzr revert`` should put everything back as it was in the previous revision. This includes resetting the tree state and texts, and also clearing the list of pending-merged revisions. Offline merge ------------- It should be possible to download all the data necessary to do a merge from a remote branch, then disconnect and complete the merge. It should be possible to interrupt and continue the merge during this process. This implies that all the data is pulled down and stored somewhere locally before the actual merge begins. It could be pulled either into the revision history on non-trunk revisions, or into temporary files. It seems useful to move all revisions and texts from the other branch into the storage of this branch, in concordance with the general idea of every branch moving towards complete knowledge. This allows the most options for an offline merge, and also for later looking back to see what was merged in and what decisions were made during the merge. Merge metadata -------------- What does cherry-picking mean here? It means we merged the changes from a revision relative to its predecessor? But what if we actually want to merge the delta relative to something else? Can that be represented? Rejected merges --------------- ddaa says perhaps we should have three states: with respect to a branch any foreign revision can be *merged*, *not-merged*, or *rejected*. The difference between *not-merged* and *rejected* is that not-merged patches will try to merge in when you next sync from their branch, whereas rejected changes will not. 'rejected' seems technically equivalent to it merged with the text changes not present. But perhaps there should be something more? Excluded changes ---------------- Bitkeeper has the very interesting feature of *excluded* changesets, where something previously committed to or merged into this branch is treated as if it never occurred. Because of their representation of changes as a weave they can do this without dependency problems on later merges. Although the later changes will not mechanically conflict, there is of course no guarantee that excluding the patch will generate anything semantically valid. Cool though this is, it seems that few people understand it well enough to use it confidently. It complicates the merge algorithms and seems to have been a source of some bugs. It may be better just to apply a reversed patch. Implementation -------------- Each revision has a pointer to two sets, represented as lists of revision-ids: * ``merged-revisions`` * ``picked-revisions`` When a branch is merged in, its revision history plus merged-revisions are added to the merged-revisions file. When changes are cherry-picked in they are added to the picked-revisions. These lists are stored in the text store and their sha1 and id is stored in the revision. While a merge is underway, these are stored in ``.bzr/pending-merged-revisions`` and ``.bzr/pending-picked-revisions``. commit refs/heads/master mark :317 committer Martin Pool 1115008376 +1000 data 38 - better error message for broken pipe from :316 M 644 inline NEWS data 4542 bzr-0.0.5 NOT RELEASED YET ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New 'bzr ignore PATTERN' command. * Nicer error message for broken pipe and similar conditions that don't indicate an internal error. TESTING: * Converted black-box test suites from Bourne shell into Python. Various structural improvements to the tests. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 33480 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool http://bazaar-ng.org/ **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** * Metadata format is not stable yet -- you may need to discard history in the future. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr rename FROM TO Rename one file. bzr move FROM... DESTDIR Move one or more files to a different directory. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. bzr check Verify history is stored safely. (for more type 'bzr help commands') """ import sys, os, time, types, shutil, tempfile, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. cmd_aliases = { '?': 'help', 'ci': 'commit', 'checkin': 'commit', 'di': 'diff', 'st': 'status', 'stat': 'status', } def get_cmd_handler(cmd): cmd = str(cmd) cmd = cmd_aliases.get(cmd, cmd) try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: raise BzrError("unknown command %r" % cmd) return cmd, cmd_handler def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. if file_list: file_list = [b.relpath(f) for f in file_list] # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and (new_name not in file_list): continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) sys.stdout.writelines(ud) if nonl: print "\\ No newline at end of file" sys.stdout.write('\n') if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_dump_text_inventory(): import bzrlib.textinv inv = Branch('.').basis_tree().inventory bzrlib.textinv.write_text_inventory(inv, sys.stdout) def cmd_load_text_inventory(): import bzrlib.textinv inv = bzrlib.textinv.read_text_inventory(sys.stdin) print 'loaded %d entries' % len(inv) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original', verbose=False): """Show log of this branch. TODO: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone, verbose=verbose) def cmd_ls(revision=None, verbose=False): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignore(name_pattern): """Ignore a command or pattern""" b = Branch('.') # XXX: This will fail if it's a hardlink; should use an AtomicFile class. f = open(b.abspath('.bzrignore'), 'at') f.write(name_pattern + '\n') f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_whoami(email=False): """Show bzr user id. usage: bzr whoami options: --email Show only the email address. """ if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ elif topic == 'commands': help_commands() else: # otherwise, maybe the name of a command? topic, cmdfn = get_cmd_handler(topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def help_commands(): """List all commands""" accu = [] for k in globals().keys(): if k.startswith('cmd_'): accu.append(k[4:].replace('_','-')) accu.sort() print "bzr commands: " for x in accu: print " " + x print "note: some of these commands are internal-use or obsolete" # TODO: Some kind of marker for internal-use commands? # TODO: Show aliases? def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone', 'verbose'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], 'whoami': ['email'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'ignore': ['name_pattern'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_handler = get_cmd_handler(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(canonical_cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(canonical_cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: return cmd_handler(**cmdargs) or 0 def _report_exception(e, summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception(e) if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def cmd_assert_fail(): assert False, "always fails" def main(argv): import errno bzrlib.trace.create_tracefile(argv) try: try: ret = run_bzr(argv) # do this here to catch EPIPE sys.stdout.flush() return ret except BzrError, e: _report_exception(e, 'error: ' + e.args[0]) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(e, msg) except Exception, e: quiet = False if isinstance(e, IOError) and e.errno == errno.EPIPE: quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(e, msg, quiet) return 1 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :318 committer Martin Pool 1115008502 +1000 data 33 - better error message for Ctrl-c from :317 M 644 inline NEWS data 4553 bzr-0.0.5 NOT RELEASED YET ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New 'bzr ignore PATTERN' command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. TESTING: * Converted black-box test suites from Bourne shell into Python. Various structural improvements to the tests. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 33619 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool http://bazaar-ng.org/ **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** * Metadata format is not stable yet -- you may need to discard history in the future. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr rename FROM TO Rename one file. bzr move FROM... DESTDIR Move one or more files to a different directory. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export REVNO DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. bzr check Verify history is stored safely. (for more type 'bzr help commands') """ import sys, os, time, types, shutil, tempfile, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob from inspect import getdoc import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' ## TODO: Perhaps a different version of inventory commands that ## returns iterators... ## TODO: Perhaps an AtomicFile class that writes to a temporary file and then renames. ## TODO: Some kind of locking on branches. Perhaps there should be a ## parameter to the branch object saying whether we want a read or ## write lock; release it from destructor. Perhaps don't even need a ## read lock to look at immutable objects? ## TODO: Perhaps make UUIDs predictable in test mode to make it easier ## to compare output? ## TODO: Some kind of global code to generate the right Branch object ## to work on. Almost, but not quite all, commands need one, and it ## can be taken either from their parameters or their working ## directory. cmd_aliases = { '?': 'help', 'ci': 'commit', 'checkin': 'commit', 'di': 'diff', 'st': 'status', 'stat': 'status', } def get_cmd_handler(cmd): cmd = str(cmd) cmd = cmd_aliases.get(cmd, cmd) try: cmd_handler = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: raise BzrError("unknown command %r" % cmd) return cmd, cmd_handler def cmd_status(all=False): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) ###################################################################### # examining history def cmd_get_revision(revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) def cmd_get_file_text(text_id): """Get contents of a file by hash.""" sf = Branch('.').text_store[text_id] pumpfile(sf, sys.stdout) ###################################################################### # commands def cmd_revno(): """Show number of revisions on this branch""" print Branch('.').revno() def cmd_add(file_list, verbose=False): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ bzrlib.add.smart_add(file_list, verbose) def cmd_relpath(filename): """Show path of file relative to root""" print Branch(filename).relpath(filename) def cmd_inventory(revision=None): """Show inventory of the current working copy.""" ## TODO: Also optionally show a previous inventory ## TODO: Format options b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) # TODO: Maybe a 'mv' command that has the combined move/rename # special behaviour of Unix? def cmd_move(source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) def cmd_rename(from_name, to_name): """Change the name of an entry. usage: bzr rename FROM_NAME TO_NAME examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) def cmd_renames(dir='.'): """Show list of renamed files. usage: bzr renames [BRANCH] TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) def cmd_info(): """info: Show statistical information for this branch usage: bzr info""" import info info.show_info(Branch('.')) def cmd_remove(file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) def cmd_file_id(filename): """Print file_id of a particular file or directory. usage: bzr file-id FILE The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i def cmd_file_id_path(filename): """Print path of file_ids to a file or directory. usage: bzr file-id-path FILE This prints one line for each directory down to the target, starting at the branch root.""" b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip def cmd_revision_history(): for patchid in Branch('.').revision_history(): print patchid def cmd_directories(): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name def cmd_missing(): for name, ie in Branch('.').working_tree().missing(): print name def cmd_init(): # TODO: Check we're not already in a working directory? At the # moment you'll get an ugly error. # TODO: What if we're in a subdirectory of a branch? Would like # to allow that, but then the parent may need to understand that # the children have disappeared, or should they be versioned in # both? # TODO: Take an argument/option for branch name. Branch('.', init=True) def cmd_diff(revision=None, file_list=None): """bzr diff: Show differences in working tree. usage: bzr diff [-r REV] [FILE...] --revision REV Show changes since REV, rather than predecessor. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ ## TODO: Shouldn't be in the cmd function. b = Branch('.') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. if file_list: file_list = [b.relpath(f) for f in file_list] # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in bzrlib.diff_trees(old_tree, new_tree): if file_list and (new_name not in file_list): continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) sys.stdout.writelines(ud) if nonl: print "\\ No newline at end of file" sys.stdout.write('\n') if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) def cmd_deleted(show_ids=False): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path def cmd_parse_inventory(): import cElementTree cElementTree.ElementTree().parse(file('.bzr/inventory')) def cmd_load_inventory(): """Load inventory for timing purposes""" Branch('.').basis_tree().inventory def cmd_dump_inventory(): Branch('.').read_working_inventory().write_xml(sys.stdout) def cmd_dump_new_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_inventory(inv, sys.stdout) def cmd_load_new_inventory(): import bzrlib.newinventory bzrlib.newinventory.read_new_inventory(sys.stdin) def cmd_dump_slacker_inventory(): import bzrlib.newinventory inv = Branch('.').basis_tree().inventory bzrlib.newinventory.write_slacker_inventory(inv, sys.stdout) def cmd_dump_text_inventory(): import bzrlib.textinv inv = Branch('.').basis_tree().inventory bzrlib.textinv.write_text_inventory(inv, sys.stdout) def cmd_load_text_inventory(): import bzrlib.textinv inv = bzrlib.textinv.read_text_inventory(sys.stdin) print 'loaded %d entries' % len(inv) def cmd_root(filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) def cmd_log(timezone='original', verbose=False): """Show log of this branch. TODO: Options for utc; to show ids; to limit range; etc. """ Branch('.').write_log(show_timezone=timezone, verbose=verbose) def cmd_ls(revision=None, verbose=False): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp def cmd_unknowns(): """List unknown files""" for f in Branch('.').unknowns(): print quotefn(f) def cmd_ignore(name_pattern): """Ignore a command or pattern""" b = Branch('.') # XXX: This will fail if it's a hardlink; should use an AtomicFile class. f = open(b.abspath('.bzrignore'), 'at') f.write(name_pattern + '\n') f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) def cmd_ignored(): """List ignored files and the patterns that matched them. """ tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) def cmd_lookup_revision(revno): try: revno = int(revno) except ValueError: bailout("usage: lookup-revision REVNO", ["REVNO is a non-negative revision number for this branch"]) print Branch('.').lookup_revision(revno) or NONE_STRING def cmd_export(revno, dest): """Export past revision to destination directory.""" b = Branch('.') rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) def cmd_cat(revision, filename): """Print file to stdout.""" b = Branch('.') b.print_file(b.relpath(filename), int(revision)) ###################################################################### # internal/test commands def cmd_uuid(): """Print a newly-generated UUID.""" print bzrlib.osutils.uuid() def cmd_local_time_offset(): print bzrlib.osutils.local_time_offset() def cmd_commit(message=None, verbose=False): """Commit changes to a new revision. --message MESSAGE Description of changes in this revision; free form text. It is recommended that the first line be a single-sentence summary. --verbose Show status of changed files, TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ if not message: bailout("please specify a commit message") Branch('.').commit(message, verbose=verbose) def cmd_check(dir='.'): """check: Consistency check of branch history. usage: bzr check [-v] [BRANCH] options: --verbose, -v Show progress of checking. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) def cmd_is(pred, *rest): """Test whether PREDICATE is true.""" try: cmd_handler = globals()['assert_' + pred.replace('-', '_')] except KeyError: bailout("unknown predicate: %s" % quotefn(pred)) try: cmd_handler(*rest) except BzrCheckError: # by default we don't print the message so that this can # be used from shell scripts without producing noise sys.exit(1) def cmd_whoami(email=False): """Show bzr user id. usage: bzr whoami options: --email Show only the email address. """ if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() def cmd_gen_revision_id(): print bzrlib.branch._gen_revision_id(time.time()) def cmd_selftest(): """Run internal test suite""" ## -v, if present, is seen by doctest; the argument is just here ## so our parser doesn't complain ## TODO: --verbose option failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print # deprecated cmd_doctest = cmd_selftest ###################################################################### # help def cmd_help(topic=None): if topic == None: print __doc__ elif topic == 'commands': help_commands() else: # otherwise, maybe the name of a command? topic, cmdfn = get_cmd_handler(topic) doc = getdoc(cmdfn) if doc == None: bailout("sorry, no detailed help yet for %r" % topic) print doc def help_commands(): """List all commands""" accu = [] for k in globals().keys(): if k.startswith('cmd_'): accu.append(k[4:].replace('_','-')) accu.sort() print "bzr commands: " for x in accu: print " " + x print "note: some of these commands are internal-use or obsolete" # TODO: Some kind of marker for internal-use commands? # TODO: Show aliases? def cmd_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print \ """bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and you may use, modify and redistribute it under the terms of the GNU General Public License version 2 or later.""" def cmd_rocks(): """Statement of optimism.""" print "it sure does!" ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } # List of options that apply to particular commands; commands not # listed take none. cmd_options = { 'add': ['verbose'], 'cat': ['revision'], 'commit': ['message', 'verbose'], 'deleted': ['show-ids'], 'diff': ['revision'], 'inventory': ['revision'], 'log': ['timezone', 'verbose'], 'ls': ['revision', 'verbose'], 'remove': ['verbose'], 'status': ['all'], 'whoami': ['email'], } cmd_args = { 'add': ['file+'], 'cat': ['filename'], 'commit': [], 'diff': ['file*'], 'export': ['revno', 'dest'], 'file-id': ['filename'], 'file-id-path': ['filename'], 'get-file-text': ['text_id'], 'get-inventory': ['inventory_id'], 'get-revision': ['revision_id'], 'get-revision-inventory': ['revision_id'], 'help': ['topic?'], 'ignore': ['name_pattern'], 'init': [], 'log': [], 'lookup-revision': ['revno'], 'move': ['source$', 'dest'], 'relpath': ['filename'], 'remove': ['file+'], 'rename': ['from_name', 'to_name'], 'renames': ['dir?'], 'root': ['filename?'], 'status': [], } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_args(cmd, args): """Check non-option arguments match required pattern. >>> _match_args('status', ['asdasdsadasd']) Traceback (most recent call last): ... BzrError: ("extra arguments to command status: ['asdasdsadasd']", []) >>> _match_args('add', ['asdasdsadasd']) {'file_list': ['asdasdsadasd']} >>> _match_args('add', 'abc def gj'.split()) {'file_list': ['abc', 'def', 'gj']} """ # match argument pattern argform = cmd_args.get(cmd, []) argdict = {} # TODO: Need a way to express 'cp SRC... DEST', where it matches # all but one. # step through args and argform, allowing appropriate 0-many matches for ap in argform: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: bailout("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: bailout("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: bailout("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: bailout("extra arguments to command %s: %r" % (cmd, args)) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: # TODO: pass down other arguments in case they asked for # help on a command name? if args: cmd_help(args[0]) else: cmd_help() return 0 elif 'version' in opts: cmd_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_handler = get_cmd_handler(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_options.get(canonical_cmd, []) for oname in opts: if oname not in allowed: bailout("option %r is not allowed for command %r" % (oname, cmd)) # TODO: give an error if there are any mandatory options which are # not specified? Or maybe there shouldn't be any "mandatory # options" (it is an oxymoron) # mix arguments and options into one dictionary cmdargs = _match_args(canonical_cmd, args) for k, v in opts.items(): cmdargs[k.replace('-', '_')] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_handler, **cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: return cmd_handler(**cmdargs) or 0 def _report_exception(e, summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception(e) if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def cmd_assert_fail(): assert False, "always fails" def main(argv): import errno bzrlib.trace.create_tracefile(argv) try: try: ret = run_bzr(argv) # do this here to catch EPIPE sys.stdout.flush() return ret except BzrError, e: _report_exception(e, 'error: ' + e.args[0]) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(e, msg) return 2 except KeyboardInterrupt, e: _report_exception(e, 'interrupted', quiet=True) return 2 except Exception, e: quiet = False if isinstance(e, IOError) and e.errno == errno.EPIPE: quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(e, msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :319 committer Martin Pool 1115008633 +1000 data 33 - remove trivial chomp() function from :318 M 644 inline bzrlib/branch.py data 34882 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. TODO: Perhaps use different stores for different classes of object, so that we can keep track of how much space each one uses, or garbage-collect them. TODO: Add a RemoteBranch subclass. For the basic case of read-only HTTP access this should be very easy by, just redirecting controlfile access into HTTP requests. We would need a RemoteStore working similarly. TODO: Keep the on-disk branch locked while the object exists. TODO: mkdir() method. """ def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) self.append_revision(rev_id) if verbose: note("commited r%d" % self.revno()) def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] else: return None def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original', verbose=False): """Write out human-readable log of commits to this branch utc -- If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l if verbose == True and precursor != None: print 'changed files:' tree = self.revision_tree(p) prevtree = self.revision_tree(precursor) for file_state, fid, old_name, new_name, kind in \ diff_trees(prevtree, tree, ): if file_state == 'A' or file_state == 'M': show_status(file_state, kind, new_name) elif file_state == 'D': show_status(file_state, kind, old_name) elif file_state == 'R': show_status(file_state, kind, old_name + ' => ' + new_name) revno += 1 precursor = p def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(self, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo TODO: Get state for single files. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = self.basis_tree() new = self.working_tree() for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("weird file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" try: shutil.rmtree(self.base) except OSError: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/osutils.py data 8602 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, types, re, time, errno from stat import S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE from errors import bailout, BzrError from trace import mutter import bzrlib def make_readonly(filename): """Make a filename read-only.""" # TODO: probably needs to be fixed for windows mod = os.stat(filename).st_mode mod = mod & 0777555 os.chmod(filename, mod) def make_writable(filename): mod = os.stat(filename).st_mode mod = mod | 0200 os.chmod(filename, mod) _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])') def quotefn(f): """Return shell-quoted filename""" ## We could be a bit more terse by using double-quotes etc f = _QUOTE_RE.sub(r'\\\1', f) if f[0] == '~': f[0:1] = r'\~' return f def file_kind(f): mode = os.lstat(f)[ST_MODE] if S_ISREG(mode): return 'file' elif S_ISDIR(mode): return 'directory' elif S_ISLNK(mode): return 'symlink' else: raise BzrError("can't handle file kind with mode %o of %r" % (mode, f)) def isdir(f): """True if f is an accessible directory.""" try: return S_ISDIR(os.lstat(f)[ST_MODE]) except OSError: return False def isfile(f): """True if f is a regular file.""" try: return S_ISREG(os.lstat(f)[ST_MODE]) except OSError: return False def pumpfile(fromfile, tofile): """Copy contents of one file to another.""" tofile.write(fromfile.read()) def uuid(): """Return a new UUID""" try: return file('/proc/sys/kernel/random/uuid').readline().rstrip('\n') except IOError: return chomp(os.popen('uuidgen').readline()) def sha_file(f): import sha ## TODO: Maybe read in chunks to handle big files if hasattr(f, 'tell'): assert f.tell() == 0 s = sha.new() s.update(f.read()) return s.hexdigest() def sha_string(f): import sha s = sha.new() s.update(f) return s.hexdigest() def fingerprint_file(f): import sha s = sha.new() b = f.read() s.update(b) size = len(b) return {'size': size, 'sha1': s.hexdigest()} def config_dir(): """Return per-user configuration directory. By default this is ~/.bzr.conf/ TODO: Global option --config-dir to override this. """ return os.path.expanduser("~/.bzr.conf") def _auto_user_id(): """Calculate automatic user identification. Returns (realname, email). Only used when none is set in the environment or the id file. This previously used the FQDN as the default domain, but that can be very slow on machines where DNS is broken. So now we simply use the hostname. """ import socket # XXX: Any good way to get real user name on win32? try: import pwd uid = os.getuid() w = pwd.getpwuid(uid) gecos = w.pw_gecos.decode(bzrlib.user_encoding) username = w.pw_name.decode(bzrlib.user_encoding) comma = gecos.find(',') if comma == -1: realname = gecos else: realname = gecos[:comma] if not realname: realname = username except ImportError: import getpass realname = username = getpass.getuser().decode(bzrlib.user_encoding) return realname, (username + '@' + socket.gethostname()) def _get_user_id(): """Return the full user id from a file or environment variable. TODO: Allow taking this from a file in the branch directory too for per-branch ids.""" v = os.environ.get('BZREMAIL') if v: return v.decode(bzrlib.user_encoding) try: return (open(os.path.join(config_dir(), "email")) .read() .decode(bzrlib.user_encoding) .rstrip("\r\n")) except IOError, e: if e.errno != errno.ENOENT: raise e v = os.environ.get('EMAIL') if v: return v.decode(bzrlib.user_encoding) else: return None def username(): """Return email-style username. Something similar to 'Martin Pool ' TODO: Check it's reasonably well-formed. """ v = _get_user_id() if v: return v name, email = _auto_user_id() if name: return '%s <%s>' % (name, email) else: return email _EMAIL_RE = re.compile(r'[\w+.-]+@[\w+.-]+') def user_email(): """Return just the email component of a username.""" e = _get_user_id() if e: m = _EMAIL_RE.search(e) if not m: bailout("%r doesn't seem to contain a reasonable email address" % e) return m.group(0) return _auto_user_id()[1] def compare_files(a, b): """Returns true if equal in contents""" # TODO: don't read the whole thing in one go. BUFSIZE = 4096 while True: ai = a.read(BUFSIZE) bi = b.read(BUFSIZE) if ai != bi: return False if ai == '': return True def local_time_offset(t=None): """Return offset of local zone from GMT, either at present or at time t.""" # python2.3 localtime() can't take None if t == None: t = time.time() if time.localtime(t).tm_isdst and time.daylight: return -time.altzone else: return -time.timezone def format_date(t, offset=0, timezone='original'): ## TODO: Perhaps a global option to use either universal or local time? ## Or perhaps just let people set $TZ? assert isinstance(t, float) if timezone == 'utc': tt = time.gmtime(t) offset = 0 elif timezone == 'original': if offset == None: offset = 0 tt = time.gmtime(t + offset) elif timezone == 'local': tt = time.localtime(t) offset = local_time_offset(t) else: bailout("unsupported timezone format %r", ['options are "utc", "original", "local"']) return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt) + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)) def compact_date(when): return time.strftime('%Y%m%d%H%M%S', time.gmtime(when)) def filesize(f): """Return size of given open file.""" return os.fstat(f.fileno())[ST_SIZE] if hasattr(os, 'urandom'): # python 2.4 and later rand_bytes = os.urandom else: # FIXME: No good on non-Linux _rand_file = file('/dev/urandom', 'rb') rand_bytes = _rand_file.read ## TODO: We could later have path objects that remember their list ## decomposition (might be too tricksy though.) def splitpath(p): """Turn string into list of parts. >>> splitpath('a') ['a'] >>> splitpath('a/b') ['a', 'b'] >>> splitpath('a/./b') ['a', 'b'] >>> splitpath('a/.b') ['a', '.b'] >>> splitpath('a/../b') Traceback (most recent call last): ... BzrError: ("sorry, '..' not allowed in path", []) """ assert isinstance(p, types.StringTypes) # split on either delimiter because people might use either on # Windows ps = re.split(r'[\\/]', p) rps = [] for f in ps: if f == '..': bailout("sorry, %r not allowed in path" % f) elif (f == '.') or (f == ''): pass else: rps.append(f) return rps def joinpath(p): assert isinstance(p, list) for f in p: if (f == '..') or (f == None) or (f == ''): bailout("sorry, %r not allowed in path" % f) return os.path.join(*p) def appendpath(p1, p2): if p1 == '': return p2 else: return os.path.join(p1, p2) def extern_command(cmd, ignore_errors = False): mutter('external command: %s' % `cmd`) if os.system(cmd): if not ignore_errors: bailout('command failed') commit refs/heads/master mark :320 committer Martin Pool 1115008863 +1000 data 34 - Compute SHA-1 of files in chunks from :319 M 644 inline bzrlib/osutils.py data 8597 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, types, re, time, errno from stat import S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE from errors import bailout, BzrError from trace import mutter import bzrlib def make_readonly(filename): """Make a filename read-only.""" # TODO: probably needs to be fixed for windows mod = os.stat(filename).st_mode mod = mod & 0777555 os.chmod(filename, mod) def make_writable(filename): mod = os.stat(filename).st_mode mod = mod | 0200 os.chmod(filename, mod) _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])') def quotefn(f): """Return shell-quoted filename""" ## We could be a bit more terse by using double-quotes etc f = _QUOTE_RE.sub(r'\\\1', f) if f[0] == '~': f[0:1] = r'\~' return f def file_kind(f): mode = os.lstat(f)[ST_MODE] if S_ISREG(mode): return 'file' elif S_ISDIR(mode): return 'directory' elif S_ISLNK(mode): return 'symlink' else: raise BzrError("can't handle file kind with mode %o of %r" % (mode, f)) def isdir(f): """True if f is an accessible directory.""" try: return S_ISDIR(os.lstat(f)[ST_MODE]) except OSError: return False def isfile(f): """True if f is a regular file.""" try: return S_ISREG(os.lstat(f)[ST_MODE]) except OSError: return False def pumpfile(fromfile, tofile): """Copy contents of one file to another.""" tofile.write(fromfile.read()) def uuid(): """Return a new UUID""" try: return file('/proc/sys/kernel/random/uuid').readline().rstrip('\n') except IOError: return chomp(os.popen('uuidgen').readline()) def sha_file(f): import sha if hasattr(f, 'tell'): assert f.tell() == 0 s = sha.new() BUFSIZE = 128<<10 while True: b = f.read(BUFSIZE) if not b: break s.update(b) return s.hexdigest() def sha_string(f): import sha s = sha.new() s.update(f) return s.hexdigest() def fingerprint_file(f): import sha s = sha.new() b = f.read() s.update(b) size = len(b) return {'size': size, 'sha1': s.hexdigest()} def config_dir(): """Return per-user configuration directory. By default this is ~/.bzr.conf/ TODO: Global option --config-dir to override this. """ return os.path.expanduser("~/.bzr.conf") def _auto_user_id(): """Calculate automatic user identification. Returns (realname, email). Only used when none is set in the environment or the id file. This previously used the FQDN as the default domain, but that can be very slow on machines where DNS is broken. So now we simply use the hostname. """ import socket # XXX: Any good way to get real user name on win32? try: import pwd uid = os.getuid() w = pwd.getpwuid(uid) gecos = w.pw_gecos.decode(bzrlib.user_encoding) username = w.pw_name.decode(bzrlib.user_encoding) comma = gecos.find(',') if comma == -1: realname = gecos else: realname = gecos[:comma] if not realname: realname = username except ImportError: import getpass realname = username = getpass.getuser().decode(bzrlib.user_encoding) return realname, (username + '@' + socket.gethostname()) def _get_user_id(): """Return the full user id from a file or environment variable. TODO: Allow taking this from a file in the branch directory too for per-branch ids.""" v = os.environ.get('BZREMAIL') if v: return v.decode(bzrlib.user_encoding) try: return (open(os.path.join(config_dir(), "email")) .read() .decode(bzrlib.user_encoding) .rstrip("\r\n")) except IOError, e: if e.errno != errno.ENOENT: raise e v = os.environ.get('EMAIL') if v: return v.decode(bzrlib.user_encoding) else: return None def username(): """Return email-style username. Something similar to 'Martin Pool ' TODO: Check it's reasonably well-formed. """ v = _get_user_id() if v: return v name, email = _auto_user_id() if name: return '%s <%s>' % (name, email) else: return email _EMAIL_RE = re.compile(r'[\w+.-]+@[\w+.-]+') def user_email(): """Return just the email component of a username.""" e = _get_user_id() if e: m = _EMAIL_RE.search(e) if not m: bailout("%r doesn't seem to contain a reasonable email address" % e) return m.group(0) return _auto_user_id()[1] def compare_files(a, b): """Returns true if equal in contents""" BUFSIZE = 4096 while True: ai = a.read(BUFSIZE) bi = b.read(BUFSIZE) if ai != bi: return False if ai == '': return True def local_time_offset(t=None): """Return offset of local zone from GMT, either at present or at time t.""" # python2.3 localtime() can't take None if t == None: t = time.time() if time.localtime(t).tm_isdst and time.daylight: return -time.altzone else: return -time.timezone def format_date(t, offset=0, timezone='original'): ## TODO: Perhaps a global option to use either universal or local time? ## Or perhaps just let people set $TZ? assert isinstance(t, float) if timezone == 'utc': tt = time.gmtime(t) offset = 0 elif timezone == 'original': if offset == None: offset = 0 tt = time.gmtime(t + offset) elif timezone == 'local': tt = time.localtime(t) offset = local_time_offset(t) else: bailout("unsupported timezone format %r", ['options are "utc", "original", "local"']) return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt) + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)) def compact_date(when): return time.strftime('%Y%m%d%H%M%S', time.gmtime(when)) def filesize(f): """Return size of given open file.""" return os.fstat(f.fileno())[ST_SIZE] if hasattr(os, 'urandom'): # python 2.4 and later rand_bytes = os.urandom else: # FIXME: No good on non-Linux _rand_file = file('/dev/urandom', 'rb') rand_bytes = _rand_file.read ## TODO: We could later have path objects that remember their list ## decomposition (might be too tricksy though.) def splitpath(p): """Turn string into list of parts. >>> splitpath('a') ['a'] >>> splitpath('a/b') ['a', 'b'] >>> splitpath('a/./b') ['a', 'b'] >>> splitpath('a/.b') ['a', '.b'] >>> splitpath('a/../b') Traceback (most recent call last): ... BzrError: ("sorry, '..' not allowed in path", []) """ assert isinstance(p, types.StringTypes) # split on either delimiter because people might use either on # Windows ps = re.split(r'[\\/]', p) rps = [] for f in ps: if f == '..': bailout("sorry, %r not allowed in path" % f) elif (f == '.') or (f == ''): pass else: rps.append(f) return rps def joinpath(p): assert isinstance(p, list) for f in p: if (f == '..') or (f == None) or (f == ''): bailout("sorry, %r not allowed in path" % f) return os.path.join(*p) def appendpath(p1, p2): if p1 == '': return p2 else: return os.path.join(p1, p2) def extern_command(cmd, ignore_errors = False): mutter('external command: %s' % `cmd`) if os.system(cmd): if not ignore_errors: bailout('command failed') commit refs/heads/master mark :321 committer Martin Pool 1115018435 +1000 data 39 doc: revfile storage and related things from :320 M 644 inline doc/revfile.txt data 3263 ******** Revfiles ******** The unit for compressed storage in bzr is a *revfile*, whose design was suggested by Matt Mackall. Requirements ============ Compressed storage is a tradeoff between several goals: * Reasonably compact storage of long histories. * Robustness and simplicity. * Fast extraction of versions and addition of new versions (preferably without rewriting the whole file, or reading the whole history.) * Fast and precise annotations. * Storage of files of at least a few hundred MB. Design ====== revfiles store the history of a single logical file, which is identified in bzr by its file-id. In this sense they are similar to an RCS or CVS ``,v`` file or an SCCS sfile. Each state of the file is called a *text*. Renaming, adding and deleting this file is handled at a higher level by the inventory system, and is outside the scope of the revfile. The revfile name is typically based on the file id which is itself typically based on the name the file had when it was first added. But this is purely cosmetic. For example a file now called ``frob.c`` may have the id ``frobber.c-12873`` because it was originally called ``frobber.c``. Its texts are kept in the revfile ``.bzr/revfiles/frobber.c-12873.revs``. When the file is deleted from the inventory the revfile does not change. It's just not used in reproducing trees from that point onwards. The revfile does not record the date when the text was added, a commit message, properties, or any other metadata. That is handled in the higher-level revision history. Inventories and other metadata files that vary from one version to the next can themselves be stored in revfiles. revfiles store files as simple byte streams, with no consideration of translating character sets, line endings, or keywords. Those are also handled at a higher level. However, the revfile may make use of knowledge that a file is line-based in generating a diff. (The Python builtin difflib is too slow when generating a purely byte-by-byte delta so we always make a line-by-line diff; when this is fixed it may be feasible to use line-by-line diffs for all files.) Files whose text does not change from one revision to the next are stored as just a single text in the revfile. This can happen even if the file was renamed or other properties were changed in the inventory. Skip-deltas ----------- Because the basis of a delta does not need to be the text's logical predecessor, we can adjust the deltas Annotations ----------- Storing Open issues =========== * revfiles use unsigned 32-bit integers both in diffs and the index. This should be more than enough for any reasonable source file but perhaps not enough for large binaries that are frequently committed. Perhaps for those files there should be an option to continue to use the text-store. There is unlikely to be any benefit in holding deltas between them, and deltas will anyhow be hard to calculate. * The append-only design does not allow for destroying committed data, as when confidential information is accidentally added. That could be fixed by creating the fixed repository as a separate branch, into which only the preserved revisions are exported. M 644 inline TODO data 6361 .. -*- mode: rst; compile-command: "rest2html TODO >doc/todo.html" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be "bzr export -r REV". * "cat -rREV FILE" * Plugins that provide commands. By just installing a file into some directory (e.g. /usr/share/bzr/plugins) it should be possible to create new top-level commands ("bzr frob"). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * "bzr info" could show space used by working tree, versioned files, unknown and ignored files. * "bzr info" should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * Tidier error for EPIPE: should be just "bzr: broken pipe" with no other details because debugging information is rarely interesting. * On Windows, command-line arguments should be glob-expanded__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * 'bzr ignore' command that just adds a line to the .bzrignore file and makes it versioned. * 'bzr help commands' should give a one-line summary of each command. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. Medium things ------------- * Display command grammar in help messages rather than hardcoding it. * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * mv command? * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. M 644 inline doc/index.txt data 5650 Bazaar-NG ********* .. These documents are formatted as ReStructuredText. You can .. .. convert them to HTML, PDF, etc using the ``python-docutils`` .. .. package. .. *Bazaar-NG* (``bzr``) is a project of `Canonical Ltd`__ to develop an open source distributed version control system that is powerful, friendly, and scalable. The project is at an early stage of development. __ http://canonical.com/ **Note:** These documents are in a very preliminary state, and so may be internally or externally inconsistent or redundant. Comments are still very welcome. Please send them to . For more information, see the homepage at http://bazaar-ng.org/ User documentation ------------------ * `Project overview/introduction `__ * `Command reference `__ -- intended to be user documentation, and gives the best overview at the moment of what the system will feel like to use. Fairly complete. * `Quick reference `__ -- single page description of how to use, intended to check it's adequately simple. Incomplete. * `FAQ `__ -- mostly user-oriented FAQ. Requirements and general design ------------------------------- * `Various purposes of a VCS `__ -- taking snapshots and helping with merges is not the whole story. * `Requirements `__ * `Costs `__ of various factors: time, disk, network, etc. * `Deadly sins `__ that gcc maintainers suggest we avoid. * `Overview of the whole design `__ and miscellaneous small design points. * `File formats `__ * `Random observations `__ that don't fit anywhere else yet. Design of particular features ----------------------------- * `Automatic generation of ChangeLogs `__ * `Cherry picking `__ -- merge just selected non-contiguous changes from a branch. * `Common changeset format `__ for interchange format between VCS. * `Compression `__ of file text for more efficient storage. * `Config specs `__ assemble a tree from several places. * `Conflicts `_ that can occur during merge-like operations. * `Ignored files `__ * `Recovering from interrupted operations `__ * `Inventory command `__ * `Branch joins `__ represent that all the changes from one branch are integrated into another. * `Kill a version `__ to fix a broken commit or wrong message, or to remove confidential information from the history. * `Hash collisions `__ and weaknesses, and the security implications thereof. * `Layers `__ within the design * `Library interface `__ for Python. * `Merge `__ * `Mirroring `__ * `Optional edit command `__: sometimes people want to make the working copy read-only, or not present at all. * `Partial commits `__ * `Patch pools `__ to efficiently store related branches. * `Revfiles `__ store the text history of files. * `Revision syntax `__ -- ``hello.c@12``, etc. * `Roll-up commits `__ -- a single revision incorporates the changes from several others. * `Scalability `__ * `Security `__ * `Shared branches `__ maintained by more than one person * `Supportability `__ -- how to handle any bugs or problems in the field. * `Place tags on revisions for easy reference `__ * `Detecting unchanged files `__ * `Merging previously-unrelated branches `__ * `Usability principles `__ (very small at the moment) * ``__ * ``__ * ``__ Modelling/controlling flow of patches. * ``__ -- Discussion of using YAML_ as a storage or transmission format. .. _YAML: http://www.yaml.org/ Comparisons to other systems ---------------------------- * `Taxonomy `__: basic questions a VCS must answer. * `Bitkeeper `__, the proprietary system used by some kernel developers. * `Aegis `__, a tool focussed on enforcing process and workflow. * `Codeville `__ has an intruiging but scarcely-documented merge algorithm. * `CVSNT `__, with more Windows support and some merge enhancements. * `OpenCM `__, another hash-based tool with a good whitepaper. * `PRCS `__, a non-distributed inventory-based tool. * `GNU Arch `__, with many pros and cons. * `Darcs `__, a merge-focussed tool with good usability. * `Quilt `__ -- Andrew Morton's patch scripts, popular with kernel maintainers. * `Monotone `__, Graydon Hoare's hash-based distributed system. * `SVK `__ -- distributed operation stacked on Subversion. * `Sun Teamware `__ Project management and organization ----------------------------------- * `Notes on how to get a VCS adopted `__ * `Thanks `__ to various people * `Extra commands `__ for internal/developer/debugger use. * `Choice of Python as a development language `__ commit refs/heads/master mark :322 committer Martin Pool 1115018647 +1000 data 18 - update todo list from :321 M 644 inline TODO data 6247 .. -*- mode: indented-text; compile-command: "rest2html TODO >doc/todo.html" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be ``bzr export -r REV``. * "cat -rREV FILE" * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * 'bzr ignore' command that just adds a line to the .bzrignore file and makes it versioned. * 'bzr help commands' should give a one-line summary of each command. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. Medium things ------------- * Display command grammar in help messages rather than hardcoding it. * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * mv command? * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. commit refs/heads/master mark :323 committer Martin Pool 1115018712 +1000 data 18 - update todo list from :322 M 644 inline TODO data 6227 .. -*- mode: indented-text; compile-command: "rest2html TODO >doc/todo.html" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be ``bzr export -r REV``. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * 'bzr ignore' command that just adds a line to the .bzrignore file and makes it versioned. * 'bzr help commands' should give a one-line summary of each command. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. Medium things ------------- * Display command grammar in help messages rather than hardcoding it. * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * mv command? * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. commit refs/heads/master mark :324 committer Martin Pool 1115018968 +1000 data 27 - update doc build commands from :323 M 644 inline doc/Makefile data 332 all: $(addsuffix .html,$(basename $(wildcard *.txt))) todo.html clean: -rm *.html %.html: %.txt rest2html $^ > $@.tmp && mv $@.tmp $@ todo.html: ../TODO rest2html $^ > $@.tmp && mv $@.tmp $@ upload: all rsync -av \ ./ \ escudero.ubuntu.com:/srv/www.bazaar-ng.org/www/doc/ \ --delete \ --include \*.html --exclude \* commit refs/heads/master mark :325 committer Martin Pool 1115084458 +1000 data 27 - more revfile design notes from :324 M 644 inline TODO data 6238 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be ``bzr export -r REV``. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * 'bzr ignore' command that just adds a line to the .bzrignore file and makes it versioned. * 'bzr help commands' should give a one-line summary of each command. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html Medium things ------------- * Display command grammar in help messages rather than hardcoding it. * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * mv command? * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. M 644 inline doc/revfile.txt data 7634 ******** Revfiles ******** The unit for compressed storage in bzr is a *revfile*, whose design was suggested by Matt Mackall. Requirements ============ Compressed storage is a tradeoff between several goals: * Reasonably compact storage of long histories. * Robustness and simplicity. * Fast extraction of versions and addition of new versions (preferably without rewriting the whole file, or reading the whole history.) * Fast and precise annotations. * Storage of files of at least a few hundred MB. Design ====== revfiles store the history of a single logical file, which is identified in bzr by its file-id. In this sense they are similar to an RCS or CVS ``,v`` file or an SCCS sfile. Each state of the file is called a *text*. Renaming, adding and deleting this file is handled at a higher level by the inventory system, and is outside the scope of the revfile. The revfile name is typically based on the file id which is itself typically based on the name the file had when it was first added. But this is purely cosmetic. For example a file now called ``frob.c`` may have the id ``frobber.c-12873`` because it was originally called ``frobber.c``. Its texts are kept in the revfile ``.bzr/revfiles/frobber.c-12873.revs``. When the file is deleted from the inventory the revfile does not change. It's just not used in reproducing trees from that point onwards. The revfile does not record the date when the text was added, a commit message, properties, or any other metadata. That is handled in the higher-level revision history. Inventories and other metadata files that vary from one version to the next can themselves be stored in revfiles. revfiles store files as simple byte streams, with no consideration of translating character sets, line endings, or keywords. Those are also handled at a higher level. However, the revfile may make use of knowledge that a file is line-based in generating a diff. (The Python builtin difflib is too slow when generating a purely byte-by-byte delta so we always make a line-by-line diff; when this is fixed it may be feasible to use line-by-line diffs for all files.) Files whose text does not change from one revision to the next are stored as just a single text in the revfile. This can happen even if the file was renamed or other properties were changed in the inventory. The revfile is held on disk as two files: an *index* and a *data* file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. In previous versions, the index file identified texts by their SHA-1 digest. This was unsatisfying for two reasons. Firstly it assumes that SHA-1 will not collide, which is not an assumption we wish to make in long-lived files. Secondly for annotations we need to be able to map from file versions back to a revision. Texts are identified by the name of the revfile and a UUID corresponding to the first revision in which they were first introduced. This means that given a text we can identify which revision it belongs to, and annotations can use the index within the revfile to identify where a region was first introduced. We cannot identify texts by the integer revision number, because that would limit us to only referring to a file in a particular branch. I'd like to just use the revision-id, but those are variable-length strings, and I'd like the revfile index to be fixed-length and relatively short. UUIDs can be encoded in binary as only 16 bytes. Perhaps we should just use UUIDs for revisions and be done? This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. This performs pretty well except when trying to calculate deltas of really large files. For that the main thing would be to plug in something faster than difflib, which is after all pure Python. Another approach is to just store the gzipped full text of big files, though perhaps that's too perverse? Skip-deltas ----------- Because the basis of a delta does not need to be the text's logical predecessor, we can adjust the deltas to avoid ever needing to apply too many deltas to reproduce a particular file. Annotations ----------- Annotations indicate which revision of a file first inserted a line (or region of bytes). Given a string, we can write annotations on it like so: a sequence of *(index, length)* pairs, giving the *index* of the revision which introduced the next run of *length* bytes. The sum of the lengths must equal the length of the string. For text files the regions will typically fall on line breaks. This can be transformed in memory to other structures, such as a list of *(index, content)* pairs. When a line was inserted from a merge revision then the annotation for that line should still be the source in the merged branch, rather than just being the revision in which the merge took place. They can cheaply be calculated when inserting a new text, but are expensive to calculate after the fact because that requires searching back through all previous text and all texts which were merged in. It therefore seems sensible to calculate them once and store them. To do this we need two operators which update an existing annotated file: A. Given an annotated file and a working text, update the annotation to mark regions inserted in the working file as new in this revision. B. Given two annotated files, merge them to produce an annotated result. When there are conflicts, both texts should be included and annotated. These may be repeated: after a merge there may be another merge, or there may be manual fixups or conflict resolutions. So what we require is given a diff or a diff3 between two files, map the regions of bytes changed into corresponding updates to the origin annotations. Open issues =========== * revfiles use unsigned 32-bit integers both in diffs and the index. This should be more than enough for any reasonable source file but perhaps not enough for large binaries that are frequently committed. Perhaps for those files there should be an option to continue to use the text-store. There is unlikely to be any benefit in holding deltas between them, and deltas will anyhow be hard to calculate. * The append-only design does not allow for destroying committed data, as when confidential information is accidentally added. That could be fixed by creating the fixed repository as a separate branch, into which only the preserved revisions are exported. * Should annotations also indicate where text was deleted? * This design calls for only one annotation per line, which seems standard. However, this is lacking in at least two cases: - Lines which originate in the same way in more than one revision, through being independently introduced. In this case we would apparently have to make an arbitrary choice; I suppose branches could prefer to assume lines originated in their own history. - It might be useful to directly indicate which mergers included which lines. We do have that information in the revision history though, so there seems no need to store it for every line. commit refs/heads/master mark :326 committer Martin Pool 1115084533 +1000 data 4 todo from :325 M 644 inline TODO data 6452 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be ``bzr export -r REV``. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * 'bzr ignore' command that just adds a line to the .bzrignore file and makes it versioned. * 'bzr help commands' should give a one-line summary of each command. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html Medium things ------------- * Display command grammar in help messages rather than hardcoding it. * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * mv command? * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. commit refs/heads/master mark :327 committer Martin Pool 1115084588 +1000 data 4 todo from :326 M 644 inline TODO data 6545 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be ``bzr export -r REV``. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * 'bzr ignore' command that just adds a line to the .bzrignore file and makes it versioned. * 'bzr help commands' should give a one-line summary of each command. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Read and write locks on branch while it's open. * Separate read and write version checks? Medium things ------------- * Display command grammar in help messages rather than hardcoding it. * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * mv command? * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. commit refs/heads/master mark :328 committer Martin Pool 1115087985 +1000 data 42 - more documentation of revfile+annotation from :327 M 644 inline bzrlib/revfile.py data 15763 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. The iter method here will generally read through the whole index file in one go. With readahead in the kernel and python/libc (typically 128kB) this means that there should be no seeks and often only one read() call to get everything into memory. """ # TODO: Something like pread() would make this slightly simpler and # perhaps more efficient. # TODO: Could also try to mmap things... Might be faster for the # index in particular? # TODO: Some kind of faster lookup of SHAs? The bad thing is that probably means # rewriting existing records, which is not so nice. # TODO: Something to check that regions identified in the index file # completely butt up and do not overlap. Strictly it's not a problem # if there are gaps and that can happen if we're interrupted while # writing to the datafile. Overlapping would be very bad though. import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify, unhexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_RECORD = 0xFFFFFFFFL # fields in the index record I_SHA = 0 I_BASE = 1 I_FLAGS = 2 I_OFFSET = 3 I_LEN = 4 FL_GZIP = 1 # maximum number of patches in a row before recording a whole text. CHAIN_LIMIT = 50 class RevfileError(Exception): pass class LimitHitException(Exception): pass class Revfile: def __init__(self, basename, mode): # TODO: Lock file while open # TODO: advise of random access self.basename = basename if mode not in ['r', 'w']: raise RevfileError("invalid open mode %r" % mode) self.mode = mode idxname = basename + '.irev' dataname = basename + '.drev' idx_exists = os.path.exists(idxname) data_exists = os.path.exists(dataname) if idx_exists != data_exists: raise RevfileError("half-assed revfile") if not idx_exists: if mode == 'r': raise RevfileError("Revfile %r does not exist" % basename) self.idxfile = open(idxname, 'w+b') self.datafile = open(dataname, 'w+b') print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: if mode == 'r': diskmode = 'rb' else: diskmode = 'r+b' self.idxfile = open(idxname, diskmode) self.datafile = open(dataname, diskmode) h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def _check_index(self, idx): if idx < 0 or idx > len(self): raise RevfileError("invalid index %r" % idx) def _check_write(self): if self.mode != 'w': raise RevfileError("%r is open readonly" % self.basename) def find_sha(self, s): assert isinstance(s, str) assert len(s) == 20 for idx, idxrec in enumerate(self): if idxrec[I_SHA] == s: return idx else: return _NO_RECORD def _add_compressed(self, text_sha, data, base, compress): # well, maybe compress flags = 0 if compress: data_len = len(data) if data_len > 50: # don't do compression if it's too small; it's unlikely to win # enough to be worthwhile compr_data = zlib.compress(data) compr_len = len(compr_data) if compr_len < data_len: data = compr_data flags = FL_GZIP ##print '- compressed %d -> %d, %.1f%%' \ ## % (data_len, compr_len, float(compr_len)/float(data_len) * 100.0) return self._add_raw(text_sha, data, base, flags) def _add_raw(self, text_sha, data, base, flags): """Add pre-processed data, can be either full text or delta. This does the compression if that makes sense.""" idx = len(self) self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * (idx + 1) data_offset = self.datafile.tell() assert isinstance(data, str) # not unicode or anything weird self.datafile.write(data) self.datafile.flush() assert isinstance(text_sha, str) entry = text_sha entry += struct.pack(">IIII12x", base, flags, data_offset, len(data)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def _add_full_text(self, text, text_sha, compress): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" return self._add_compressed(text_sha, text, _NO_RECORD, compress) def _add_delta(self, text, text_sha, base, compress): """Add a text stored relative to a previous text.""" self._check_index(base) try: base_text = self.get(base, recursion_limit=CHAIN_LIMIT) except LimitHitException: return self._add_full_text(text, text_sha, compress) data = mdiff.bdiff(base_text, text) # If the delta is larger than the text, we might as well just # store the text. (OK, the delta might be more compressible, # but the overhead of applying it probably still makes it # bad, and I don't want to compress both of them to find out.) if len(data) >= len(text): return self._add_full_text(text, text_sha, compress) else: return self._add_compressed(text_sha, data, base, compress) def add(self, text, base=_NO_RECORD, compress=True): """Add a new text to the revfile. If the text is already present them its existing id is returned and the file is not changed. If compress is true then gzip compression will be used if it reduces the size. If a base index is specified, that text *may* be used for delta compression of the new text. Delta compression will only be used if it would be a size win and if the existing base is not at too long of a delta chain already. """ self._check_write() text_sha = sha.new(text).digest() idx = self.find_sha(text_sha) if idx != _NO_RECORD: # TODO: Optional paranoid mode where we read out that record and make sure # it's the same, in case someone ever breaks SHA-1. return idx # already present if base == _NO_RECORD: return self._add_full_text(text, text_sha, compress) else: return self._add_delta(text, text_sha, base, compress) def get(self, idx, recursion_limit=None): """Retrieve text of a previous revision. If recursion_limit is an integer then walk back at most that many revisions and then raise LimitHitException, indicating that we ought to record a new file text instead of another delta. Don't use this when trying to get out an existing revision.""" idxrec = self[idx] base = idxrec[I_BASE] if base == _NO_RECORD: text = self._get_full_text(idx, idxrec) else: text = self._get_patched(idx, idxrec, recursion_limit) if sha.new(text).digest() != idxrec[I_SHA]: raise RevfileError("corrupt SHA-1 digest on record %d" % idx) return text def _get_raw(self, idx, idxrec): flags = idxrec[I_FLAGS] if flags & ~FL_GZIP: raise RevfileError("unsupported index flags %#x on index %d" % (flags, idx)) l = idxrec[I_LEN] if l == 0: return '' self.datafile.seek(idxrec[I_OFFSET]) data = self.datafile.read(l) if len(data) != l: raise RevfileError("short read %d of %d " "getting text for record %d in %r" % (len(data), l, idx, self.basename)) if flags & FL_GZIP: data = zlib.decompress(data) return data def _get_full_text(self, idx, idxrec): assert idxrec[I_BASE] == _NO_RECORD text = self._get_raw(idx, idxrec) return text def _get_patched(self, idx, idxrec, recursion_limit): base = idxrec[I_BASE] assert base >= 0 assert base < idx # no loops! if recursion_limit == None: sub_limit = None else: sub_limit = recursion_limit - 1 if sub_limit < 0: raise LimitHitException() base_text = self.get(base, sub_limit) patch = self._get_raw(idx, idxrec) text = mdiff.bpatch(base_text, patch) return text def __len__(self): """Return number of revisions.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) if l < _RECORDSIZE: raise RevfileError("no header present in index of %r" % (self.basename)) return int(l / _RECORDSIZE) - 1 def __getitem__(self, idx): """Index by sequence id returns the index field""" ## TODO: Can avoid seek if we just moved there... self._seek_index(idx) idxrec = self._read_next_index() if idxrec == None: raise IndexError() else: return idxrec def _seek_index(self, idx): if idx < 0: raise RevfileError("invalid index %r" % idx) self.idxfile.seek((idx + 1) * _RECORDSIZE) def __iter__(self): """Read back all index records. Do not seek the index file while this is underway!""" sys.stderr.write(" ** iter called ** \n") self._seek_index(0) while True: idxrec = self._read_next_index() if not idxrec: break yield idxrec def _read_next_index(self): rec = self.idxfile.read(_RECORDSIZE) if not rec: return None elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_RECORD: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def total_text_size(self): """Return the sum of sizes of all file texts. This is how much space they would occupy if they were stored without delta and gzip compression. As a side effect this completely validates the Revfile, checking that all texts can be reproduced with the correct SHA-1.""" t = 0L for idx in range(len(self)): t += len(self.get(idx)) return t def main(argv): try: cmd = argv[1] except IndexError: sys.stderr.write("usage: revfile dump\n" " revfile add\n" " revfile add-delta BASE\n" " revfile get IDX\n" " revfile find-sha HEX\n" " revfile total-text-size\n" " revfile last\n") return 1 def rw(): return Revfile('testrev', 'w') def ro(): return Revfile('testrev', 'r') if cmd == 'add': print rw().add(sys.stdin.read()) elif cmd == 'add-delta': print rw().add(sys.stdin.read(), int(argv[2])) elif cmd == 'dump': ro().dump() elif cmd == 'get': try: idx = int(argv[2]) except IndexError: sys.stderr.write("usage: revfile get IDX\n") return 1 if idx < 0 or idx >= len(r): sys.stderr.write("invalid index %r\n" % idx) return 1 sys.stdout.write(ro().get(idx)) elif cmd == 'find-sha': try: s = unhexlify(argv[2]) except IndexError: sys.stderr.write("usage: revfile find-sha HEX\n") return 1 idx = ro().find_sha(s) if idx == _NO_RECORD: sys.stderr.write("no such record\n") return 1 else: print idx elif cmd == 'total-text-size': print ro().total_text_size() elif cmd == 'last': print len(ro())-1 else: sys.stderr.write("unknown command %r\n" % cmd) return 1 if __name__ == '__main__': import sys sys.exit(main(sys.argv) or 0) M 644 inline doc/revfile.txt data 9931 ******** Revfiles ******** The unit for compressed storage in bzr is a *revfile*, whose design was suggested by Matt Mackall. Requirements ============ Compressed storage is a tradeoff between several goals: * Reasonably compact storage of long histories. * Robustness and simplicity. * Fast extraction of versions and addition of new versions (preferably without rewriting the whole file, or reading the whole history.) * Fast and precise annotations. * Storage of files of at least a few hundred MB. Design ====== revfiles store the history of a single logical file, which is identified in bzr by its file-id. In this sense they are similar to an RCS or CVS ``,v`` file or an SCCS sfile. Each state of the file is called a *text*. Renaming, adding and deleting this file is handled at a higher level by the inventory system, and is outside the scope of the revfile. The revfile name is typically based on the file id which is itself typically based on the name the file had when it was first added. But this is purely cosmetic. For example a file now called ``frob.c`` may have the id ``frobber.c-12873`` because it was originally called ``frobber.c``. Its texts are kept in the revfile ``.bzr/revfiles/frobber.c-12873.revs``. When the file is deleted from the inventory the revfile does not change. It's just not used in reproducing trees from that point onwards. The revfile does not record the date when the text was added, a commit message, properties, or any other metadata. That is handled in the higher-level revision history. Inventories and other metadata files that vary from one version to the next can themselves be stored in revfiles. revfiles store files as simple byte streams, with no consideration of translating character sets, line endings, or keywords. Those are also handled at a higher level. However, the revfile may make use of knowledge that a file is line-based in generating a diff. (The Python builtin difflib is too slow when generating a purely byte-by-byte delta so we always make a line-by-line diff; when this is fixed it may be feasible to use line-by-line diffs for all files.) Files whose text does not change from one revision to the next are stored as just a single text in the revfile. This can happen even if the file was renamed or other properties were changed in the inventory. The revfile is held on disk as two files: an *index* and a *data* file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. In previous versions, the index file identified texts by their SHA-1 digest. This was unsatisfying for two reasons. Firstly it assumes that SHA-1 will not collide, which is not an assumption we wish to make in long-lived files. Secondly for annotations we need to be able to map from file versions back to a revision. Texts are identified by the name of the revfile and a UUID corresponding to the first revision in which they were first introduced. This means that given a text we can identify which revision it belongs to, and annotations can use the index within the revfile to identify where a region was first introduced. We cannot identify texts by the integer revision number, because that would limit us to only referring to a file in a particular branch. I'd like to just use the revision-id, but those are variable-length strings, and I'd like the revfile index to be fixed-length and relatively short. UUIDs can be encoded in binary as only 16 bytes. Perhaps we should just use UUIDs for revisions and be done? This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. This performs pretty well except when trying to calculate deltas of really large files. For that the main thing would be to plug in something faster than difflib, which is after all pure Python. Another approach is to just store the gzipped full text of big files, though perhaps that's too perverse? Skip-deltas ----------- Because the basis of a delta does not need to be the text's logical predecessor, we can adjust the deltas to avoid ever needing to apply too many deltas to reproduce a particular file. Annotations ----------- Annotations indicate which revision of a file first inserted a line (or region of bytes). Given a string, we can write annotations on it like so: a sequence of *(index, length)* pairs, giving the *index* of the revision which introduced the next run of *length* bytes. The sum of the lengths must equal the length of the string. For text files the regions will typically fall on line breaks. This can be transformed in memory to other structures, such as a list of *(index, content)* pairs. When a line was inserted from a merge revision then the annotation for that line should still be the source in the merged branch, rather than just being the revision in which the merge took place. They can cheaply be calculated when inserting a new text, but are expensive to calculate after the fact because that requires searching back through all previous text and all texts which were merged in. It therefore seems sensible to calculate them once and store them. To do this we need two operators which update an existing annotated file: A. Given an annotated file and a working text, update the annotation to mark regions inserted in the working file as new in this revision. B. Given two annotated files, merge them to produce an annotated result. When there are conflicts, both texts should be included and annotated. These may be repeated: after a merge there may be another merge, or there may be manual fixups or conflict resolutions. So what we require is given a diff or a diff3 between two files, map the regions of bytes changed into corresponding updates to the origin annotations. Annotations can also be delta-compressed; we only need to add new annotation data when there is a text insertion. (It is possible in a merge to have a change of annotation when there is no text change, though this seems unlikely. This can still be represented as a "pointless" delta, plus an update to the annotations.) Tools ----- The revfile module can be invoked as a program to give low-level access for data recovery, debugging, etc. Format ====== Index file ---------- The index file is a series of fixed-length records:: byte[16] UUID of revision byte[20] SHA-1 of expanded text (as binary, not hex) uint32 flags: 1=zlib compressed uint32 sequence number this is based on, or -1 for full text uint32 offset in text file of start uint32 length of compressed delta in text file uint32[3] reserved Total 64 bytes. The header is also 64 bytes, for tidyness and easy calculation. For this format the header must be ``bzr revfile v2\n`` padded with ``\xff`` to 64 bytes. The first record after the header is index 0. A record's base index must be less than its own index. The SHA-1 is redundant with the inventory but stored just as a check on the compression methods and so that the file can be validated without reference to any other information. Each byte in the text file should be included by at most one delta. Deltas ------ Deltas to the text are stored as a series of variable-length records:: uint32 idx uint32 m uint32 n uint32 l byte[l] new This describes a change originally introduced in the revision described by *idx* in the index. This indicates that the region [m:n] of the input file should be replaced by the text *new*. If m==n this is a pure insertion of l bytes. If l==0 this is a pure deletion of (n-m) bytes. Open issues =========== * revfiles use unsigned 32-bit integers both in diffs and the index. This should be more than enough for any reasonable source file but perhaps not enough for large binaries that are frequently committed. Perhaps for those files there should be an option to continue to use the text-store. There is unlikely to be any benefit in holding deltas between them, and deltas will anyhow be hard to calculate. * The append-only design does not allow for destroying committed data, as when confidential information is accidentally added. That could be fixed by creating the fixed repository as a separate branch, into which only the preserved revisions are exported. * Should annotations also indicate where text was deleted? * This design calls for only one annotation per line, which seems standard. However, this is lacking in at least two cases: - Lines which originate in the same way in more than one revision, through being independently introduced. In this case we would apparently have to make an arbitrary choice; I suppose branches could prefer to assume lines originated in their own history. - It might be useful to directly indicate which mergers included which lines. We do have that information in the revision history though, so there seems no need to store it for every line. * Should we also store full-texts as a transitional step? * Storing the annotations with the text is reasonably simple and compact, but means that we always need to process the annotation structure even when we only want the text. In particular it means that full-texts cannot just simply be copied out but rather composed from chunks. That seems inefficient since it is probably common to only want the text. commit refs/heads/master mark :329 committer Martin Pool 1115106496 +1000 data 164 - refactor command functions into command classes - much more help - better system for generating help from command descriptions - split diff code into bzrlib.diff from :328 M 644 inline NEWS data 4707 bzr-0.0.5 NOT RELEASED YET ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New 'bzr ignore PATTERN' command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. TESTING: * Converted black-box test suites from Bourne shell into Python. Various structural improvements to the tests. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on mango-sorbet by Scott James Remnant. * Better help messages for many commands. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline TODO data 7130 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be ``bzr export -r REV``. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * ``bzr help commands`` should give a one-line summary of each command. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Read and write locks on branch while it's open. * Separate read and write version checks? * ``bzr status FILE...`` * Check all commands have decent help. * Autogenerate argument/option help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. Medium things ------------- * Display command grammar in help messages rather than hardcoding it. * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` M 644 inline bzrlib/commands.py data 29150 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool http://bazaar-ng.org/ **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** * Metadata format is not stable yet -- you may need to discard history in the future. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr rename FROM TO Rename one file. bzr move FROM... DESTDIR Move one or more files to a different directory. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export [-r REVNO] DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. bzr check Verify history is stored safely. (for more type 'bzr help commands') """ import sys, os, time, types, shutil, tempfile, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' CMD_ALIASES = { '?': 'help', 'ci': 'commit', 'checkin': 'commit', 'di': 'diff', 'st': 'status', 'stat': 'status', } def get_cmd_class(cmd): cmd = str(cmd) cmd = CMD_ALIASES.get(cmd, cmd) try: cmd_class = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: raise BzrError("unknown command %r" % cmd) return cmd, cmd_class class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return True if the command was successful, False if not. """ return True class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_options = ['all'] def run(self, all=False): #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) def Relpath(Command): """Show path of a file relative to root""" takes_args = ('filename') def run(self): print Branch(self.args['filename']).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) class cmd_log(Command): """Show log of this branch. TODO: Options to show ids; to limit range; etc. """ takes_options = ['timezone', 'verbose'] def run(self, timezone='original', verbose=False): Branch('.').write_log(show_timezone=timezone, verbose=verbose) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern""" takes_args = ['name_pattern'] def run(self, name_pattern): b = Branch('.') # XXX: This will fail if it's a hardlink; should use an AtomicFile class. f = open(b.abspath('.bzrignore'), 'at') f.write(name_pattern + '\n') f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them.""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True def run(self, revno): try: revno = int(revno) except ValueError: raise BzrError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) or NONE_STRING class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revno=None): b = Branch('.') if revno == None: rh = b.revision_history[-1] else: rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'verbose'] def run(self, message=None, verbose=False): if not message: raise BzrCommandError("please specify a commit message") Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] def run(self, topic=None): help(topic) def help(topic=None): if topic == None: print __doc__ elif topic == 'commands': help_commands() else: help_on_command(topic) def help_on_command(cmdname): cmdname = str(cmdname) from inspect import getdoc topic, cmdclass = get_cmd_class(cmdname) doc = getdoc(cmdclass) if doc == None: raise NotImplementedError("sorry, no detailed help yet for %r" % cmdname) if '\n' in doc: short, rest = doc.split('\n', 1) else: short = doc rest = '' print 'usage: bzr ' + topic, for aname in cmdclass.takes_args: aname = aname.upper() if aname[-1] in ['$', '+']: aname = aname[:-1] + '...' elif aname[-1] == '?': aname = '[' + aname[:-1] + ']' elif aname[-1] == '*': aname = '[' + aname[:-1] + '...]' print aname, print print short if rest: print rest if cmdclass.takes_options: print print 'options:' for on in cmdclass.takes_options: print ' --%s' % on def help_commands(): """List all commands""" import inspect accu = [] for k, v in globals().items(): if k.startswith('cmd_'): accu.append((k[4:].replace('_','-'), v)) accu.sort() for cmdname, cmdclass in accu: if cmdclass.hidden: continue print cmdname help = inspect.getdoc(cmdclass) if help: print " " + help.split('\n', 1)[0] ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: if args: help(args[0]) else: help() return 0 elif 'version' in opts: cmd_version([], []) return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option %r is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs) or 0 def _report_exception(e, summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception(e) if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.trace.create_tracefile(argv) try: try: ret = run_bzr(argv) # do this here to catch EPIPE sys.stdout.flush() return ret except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception(e, 'error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(e, msg) return 2 except KeyboardInterrupt, e: _report_exception(e, 'interrupted', quiet=True) return 2 except Exception, e: quiet = False if isinstance(e, IOError) and e.errno == errno.EPIPE: quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(e, msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/diff.py data 9291 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set from trace import mutter def diff_trees(old_tree, new_tree): """Compute diff between two trees. They may be in different branches and may be working or historical trees. Yields a sequence of (state, id, old_name, new_name, kind). Each filename and each id is listed only once. """ ## TODO: Compare files before diffing; only mention those that have changed ## TODO: Set nice names in the headers, maybe include diffstat ## TODO: Perhaps make this a generator rather than using ## a callback object? ## TODO: Allow specifying a list of files to compare, rather than ## doing the whole tree? (Not urgent.) ## TODO: Allow diffing any two inventories, not just the ## current one against one. We mgiht need to specify two ## stores to look for the files if diffing two branches. That ## might imply this shouldn't be primarily a Branch method. ## XXX: This doesn't report on unknown files; that can be done ## from a separate method. old_it = old_tree.list_files() new_it = new_tree.list_files() def next(it): try: return it.next() except StopIteration: return None old_item = next(old_it) new_item = next(new_it) # We step through the two sorted iterators in parallel, trying to # keep them lined up. while (old_item != None) or (new_item != None): # OK, we still have some remaining on both, but they may be # out of step. if old_item != None: old_name, old_class, old_kind, old_id = old_item else: old_name = None if new_item != None: new_name, new_class, new_kind, new_id = new_item else: new_name = None mutter(" diff pairwise %r" % (old_item,)) mutter(" %r" % (new_item,)) if old_item: # can't handle the old tree being a WorkingTree assert old_class == 'V' if new_item and (new_class != 'V'): yield new_class, None, None, new_name, new_kind new_item = next(new_it) elif (not new_item) or (old_item and (old_name < new_name)): mutter(" extra entry in old-tree sequence") if new_tree.has_id(old_id): # will be mentioned as renamed under new name pass else: yield 'D', old_id, old_name, None, old_kind old_item = next(old_it) elif (not old_item) or (new_item and (new_name < old_name)): mutter(" extra entry in new-tree sequence") if old_tree.has_id(new_id): yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind else: yield 'A', new_id, None, new_name, new_kind new_item = next(new_it) elif old_id != new_id: assert old_name == new_name # both trees have a file of this name, but it is not the # same file. in other words, the old filename has been # overwritten by either a newly-added or a renamed file. # (should we return something about the overwritten file?) if old_tree.has_id(new_id): # renaming, overlying a deleted file yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind else: yield 'A', new_id, None, new_name, new_kind new_item = next(new_it) old_item = next(old_it) else: assert old_id == new_id assert old_id != None assert old_name == new_name assert old_kind == new_kind if old_kind == 'directory': yield '.', new_id, old_name, new_name, new_kind elif old_tree.get_file_size(old_id) != new_tree.get_file_size(old_id): mutter(" file size has changed, must be different") yield 'M', new_id, old_name, new_name, new_kind elif old_tree.get_file_sha1(old_id) == new_tree.get_file_sha1(old_id): mutter(" SHA1 indicates they're identical") ## assert compare_files(old_tree.get_file(i), new_tree.get_file(i)) yield '.', new_id, old_name, new_name, new_kind else: mutter(" quick compare shows different") yield 'M', new_id, old_name, new_name, new_kind new_item = next(new_it) old_item = next(old_it) def show_diff(b, revision, file_list): import difflib, sys if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. if file_list: file_list = [b.relpath(f) for f in file_list] # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in diff_trees(old_tree, new_tree): if file_list and (new_name not in file_list): continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) sys.stdout.writelines(ud) if nonl: print "\\ No newline at end of file" sys.stdout.write('\n') if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: bailout("can't represent state %s {%s}" % (file_state, fid)) M 644 inline bzrlib/errors.py data 1252 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " ###################################################################### # exceptions class BzrError(StandardError): pass class BzrCheckError(BzrError): pass class BzrCommandError(BzrError): # Error from malformed user command pass def bailout(msg, explanation=[]): ex = BzrError(msg, explanation) import trace trace._tracefile.write('* raising %s\n' % ex) raise ex M 644 inline bzrlib/textui.py data 1094 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def show_status(state, kind, name): if kind == 'directory': # use this even on windows? kind_ch = '/' else: assert kind == 'file', ("can't handle file of type %r" % kind) kind_ch = '' assert len(state) == 1 print state + ' ' + name + kind_ch M 644 inline doc/revfile.txt data 9932 ******** Revfiles ******** The unit for compressed storage in bzr is a *revfile*, whose design was suggested by Matt Mackall. Requirements ============ Compressed storage is a tradeoff between several goals: * Reasonably compact storage of long histories. * Robustness and simplicity. * Fast extraction of versions and addition of new versions (preferably without rewriting the whole file, or reading the whole history.) * Fast and precise annotations. * Storage of files of at least a few hundred MB. Design ====== revfiles store the history of a single logical file, which is identified in bzr by its file-id. In this sense they are similar to an RCS or CVS ``,v`` file or an SCCS sfile. Each state of the file is called a *text*. Renaming, adding and deleting this file is handled at a higher level by the inventory system, and is outside the scope of the revfile. The revfile name is typically based on the file id which is itself typically based on the name the file had when it was first added. But this is purely cosmetic. For example a file now called ``frob.c`` may have the id ``frobber.c-12873`` because it was originally called ``frobber.c``. Its texts are kept in the revfile ``.bzr/revfiles/frobber.c-12873.revs``. When the file is deleted from the inventory the revfile does not change. It's just not used in reproducing trees from that point onwards. The revfile does not record the date when the text was added, a commit message, properties, or any other metadata. That is handled in the higher-level revision history. Inventories and other metadata files that vary from one version to the next can themselves be stored in revfiles. revfiles store files as simple byte streams, with no consideration of translating character sets, line endings, or keywords. Those are also handled at a higher level. However, the revfile may make use of knowledge that a file is line-based in generating a diff. (The Python builtin difflib is too slow when generating a purely byte-by-byte delta so we always make a line-by-line diff; when this is fixed it may be feasible to use line-by-line diffs for all files.) Files whose text does not change from one revision to the next are stored as just a single text in the revfile. This can happen even if the file was renamed or other properties were changed in the inventory. The revfile is held on disk as two files: an *index* and a *data* file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. In previous versions, the index file identified texts by their SHA-1 digest. This was unsatisfying for two reasons. Firstly it assumes that SHA-1 will not collide, which is not an assumption we wish to make in long-lived files. Secondly for annotations we need to be able to map from file versions back to a revision. Texts are identified by the name of the revfile and a UUID corresponding to the first revision in which they were first introduced. This means that given a text we can identify which revision it belongs to, and annotations can use the index within the revfile to identify where a region was first introduced. We cannot identify texts by the integer revision number, because that would limit us to only referring to a file in a particular branch. I'd like to just use the revision-id, but those are variable-length strings, and I'd like the revfile index to be fixed-length and relatively short. UUIDs can be encoded in binary as only 16 bytes. Perhaps we should just use UUIDs for revisions and be done? This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. This performs pretty well except when trying to calculate deltas of really large files. For that the main thing would be to plug in something faster than difflib, which is after all pure Python. Another approach is to just store the gzipped full text of big files, though perhaps that's too perverse? Skip-deltas ----------- Because the basis of a delta does not need to be the text's logical predecessor, we can adjust the deltas to avoid ever needing to apply too many deltas to reproduce a particular file. Annotations ----------- Annotations indicate which revision of a file first inserted a line (or region of bytes). Given a string, we can write annotations on it like so: a sequence of *(index, length)* pairs, giving the *index* of the revision which introduced the next run of *length* bytes. The sum of the lengths must equal the length of the string. For text files the regions will typically fall on line breaks. This can be transformed in memory to other structures, such as a list of *(index, content)* pairs. When a line was inserted from a merge revision then the annotation for that line should still be the source in the merged branch, rather than just being the revision in which the merge took place. They can cheaply be calculated when inserting a new text, but are expensive to calculate after the fact because that requires searching back through all previous text and all texts which were merged in. It therefore seems sensible to calculate them once and store them. To do this we need two operators which update an existing annotated file: A. Given an annotated file and a working text, update the annotation to mark regions inserted in the working file as new in this revision. B. Given two annotated files, merge them to produce an annotated result. When there are conflicts, both texts should be included and annotated. These may be repeated: after a merge there may be another merge, or there may be manual fixups or conflict resolutions. So what we require is given a diff or a diff3 between two files, map the regions of bytes changed into corresponding updates to the origin annotations. Annotations can also be delta-compressed; we only need to add new annotation data when there is a text insertion. (It is possible in a merge to have a change of annotation when there is no text change, though this seems unlikely. This can still be represented as a "pointless" delta, plus an update to the annotations.) Tools ----- The revfile module can be invoked as a program to give low-level access for data recovery, debugging, etc. Format ====== Index file ---------- The index file is a series of fixed-length records:: byte[16] UUID of revision byte[20] SHA-1 of expanded text (as binary, not hex) uint32 flags: 1=zlib compressed uint32 sequence number this is based on, or -1 for full text uint32 offset in text file of start uint32 length of compressed delta in text file uint32[3] reserved Total 64 bytes. The header is also 64 bytes, for tidyness and easy calculation. For this format the header must be ``bzr revfile v2\n`` padded with ``\xff`` to 64 bytes. The first record after the header is index 0. A record's base index must be less than its own index. The SHA-1 is redundant with the inventory but stored just as a check on the compression methods and so that the file can be validated without reference to any other information. Each byte in the text file should be included by at most one delta. Deltas ------ Deltas to the text are stored as a series of variable-length records:: uint32 idx uint32 m uint32 n uint32 l byte[l] new This describes a change originally introduced in the revision described by *idx* in the index. This indicates that the region [m:n] of the input file should be replaced by the text *new*. If m==n this is a pure insertion of l bytes. If l==0 this is a pure deletion of (n-m) bytes. Open issues =========== * revfiles use unsigned 32-bit integers both in diffs and the index. This should be more than enough for any reasonable source file but perhaps not enough for large binaries that are frequently committed. Perhaps for those files there should be an option to continue to use the text-store. There is unlikely to be any benefit in holding deltas between them, and deltas will anyhow be hard to calculate. * The append-only design does not allow for destroying committed data, as when confidential information is accidentally added. That could be fixed by creating the fixed repository as a separate branch, into which only the preserved revisions are exported. * Should annotations also indicate where text was deleted? * This design calls for only one annotation per line, which seems standard. However, this is lacking in at least two cases: - Lines which originate in the same way in more than one revision, through being independently introduced. In this case we would apparently have to make an arbitrary choice; I suppose branches could prefer to assume lines originated in their own history. - It might be useful to directly indicate which mergers included which lines. We do have that information in the revision history though, so there seems no need to store it for every line. * Should we also store full-texts as a transitional step? * Storing the annotations with the text is reasonably simple and compact, but means that we always need to process the annotation structure even when we only want the text. In particular it means that full-texts cannot just simply be copied out but rather composed from chunks. That seems inefficient since it is probably common to only want the text. commit refs/heads/master mark :330 committer Martin Pool 1115106515 +1000 data 3 doc from :329 M 644 inline doc/switch-in-branch.txt data 194 Trees which are annoying to set up. Want to switch to a different branch without needing to set everything up again. Uses for switch: keeps around unknown/ignored files and any local changes. commit refs/heads/master mark :331 committer Martin Pool 1115106534 +1000 data 48 - sketchy experiments in bash and zsh completion from :330 M 644 inline contrib/bash/bzr data 532 # -*- shell-script -*- # experimental bzr bash completion # author: Martin Pool _bzr_commands() { bzr help commands | grep '^ ' } _bzr() { cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]} if [ $COMP_CWORD -eq 1 ]; then COMPREPLY=( $( compgen -W "$(_bzr_commands)" $cur ) ) elif [ $COMP_CWORD -eq 2 ]; then case "$prev" in help) COMPREPLY=( $( compgen -W "$(_bzr_commands) commands" $cur ) ) ;; esac fi } complete -F _bzr bzr M 644 inline contrib/zsh/_bzr data 322 #compdef bzr # Rudimentary zsh completion support for bzr. # -S means there are no options after a -- and that argument is ignored # To use this you must arrange for it to be in a directory that is on # your $fpath, and also for compinit to be run. _arguments -S "1::bzr command:($(bzr help commands | grep '^ '))" commit refs/heads/master mark :332 committer Martin Pool 1115106974 +1000 data 38 - nicer formatting of help for options from :331 M 644 inline bzrlib/commands.py data 29376 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool http://bazaar-ng.org/ **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** * Metadata format is not stable yet -- you may need to discard history in the future. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr rename FROM TO Rename one file. bzr move FROM... DESTDIR Move one or more files to a different directory. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export [-r REVNO] DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. bzr check Verify history is stored safely. (for more type 'bzr help commands') """ import sys, os, time, types, shutil, tempfile, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' CMD_ALIASES = { '?': 'help', 'ci': 'commit', 'checkin': 'commit', 'di': 'diff', 'st': 'status', 'stat': 'status', } def get_cmd_class(cmd): cmd = str(cmd) cmd = CMD_ALIASES.get(cmd, cmd) try: cmd_class = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: raise BzrError("unknown command %r" % cmd) return cmd, cmd_class class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return True if the command was successful, False if not. """ return True class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_options = ['all'] def run(self, all=False): #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) def Relpath(Command): """Show path of a file relative to root""" takes_args = ('filename') def run(self): print Branch(self.args['filename']).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) class cmd_log(Command): """Show log of this branch. TODO: Options to show ids; to limit range; etc. """ takes_options = ['timezone', 'verbose'] def run(self, timezone='original', verbose=False): Branch('.').write_log(show_timezone=timezone, verbose=verbose) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern""" takes_args = ['name_pattern'] def run(self, name_pattern): b = Branch('.') # XXX: This will fail if it's a hardlink; should use an AtomicFile class. f = open(b.abspath('.bzrignore'), 'at') f.write(name_pattern + '\n') f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them.""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True def run(self, revno): try: revno = int(revno) except ValueError: raise BzrError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) or NONE_STRING class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revno=None): b = Branch('.') if revno == None: rh = b.revision_history[-1] else: rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'verbose'] def run(self, message=None, verbose=False): if not message: raise BzrCommandError("please specify a commit message") Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] def run(self, topic=None): help(topic) def help(topic=None): if topic == None: print __doc__ elif topic == 'commands': help_commands() else: help_on_command(topic) def help_on_command(cmdname): cmdname = str(cmdname) from inspect import getdoc topic, cmdclass = get_cmd_class(cmdname) doc = getdoc(cmdclass) if doc == None: raise NotImplementedError("sorry, no detailed help yet for %r" % cmdname) if '\n' in doc: short, rest = doc.split('\n', 1) else: short = doc rest = '' print 'usage: bzr ' + topic, for aname in cmdclass.takes_args: aname = aname.upper() if aname[-1] in ['$', '+']: aname = aname[:-1] + '...' elif aname[-1] == '?': aname = '[' + aname[:-1] + ']' elif aname[-1] == '*': aname = '[' + aname[:-1] + '...]' print aname, print print short if rest: print rest help_on_option(cmdclass.takes_options) def help_on_option(options): if not options: return print print 'options:' for on in options: l = ' --' + on for shortname, longname in SHORT_OPTIONS.items(): if longname == on: l += ', -' + shortname break print l def help_commands(): """List all commands""" import inspect accu = [] for k, v in globals().items(): if k.startswith('cmd_'): accu.append((k[4:].replace('_','-'), v)) accu.sort() for cmdname, cmdclass in accu: if cmdclass.hidden: continue print cmdname help = inspect.getdoc(cmdclass) if help: print " " + help.split('\n', 1)[0] ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: if args: help(args[0]) else: help() return 0 elif 'version' in opts: cmd_version([], []) return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option %r is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs) or 0 def _report_exception(e, summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception(e) if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.trace.create_tracefile(argv) try: try: ret = run_bzr(argv) # do this here to catch EPIPE sys.stdout.flush() return ret except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception(e, 'error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(e, msg) return 2 except KeyboardInterrupt, e: _report_exception(e, 'interrupted', quiet=True) return 2 except Exception, e: quiet = False if isinstance(e, IOError) and e.errno == errno.EPIPE: quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(e, msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :333 committer Martin Pool 1115107026 +1000 data 36 - allow trace file to grow up to 4MB from :332 M 644 inline bzrlib/trace.py data 4275 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os, time, socket, stat, codecs import bzrlib ###################################################################### # messages and logging ## TODO: If --verbose is given then write to both stderr and ## _tracefile; perhaps replace _tracefile with a tee thing. global _tracefile, _starttime _tracefile = None # used to have % (os.environ['USER'], time.time(), os.getpid()), 'w') _starttime = None # If false, notes also go to stdout; should replace this with --silent # at some point. silent = False # TODO: Somehow tie this to the --verbose option? verbose = False # fix this if we ever fork within python _mypid = os.getpid() _logprefix = '[%d] ' % _mypid def _write_trace(msg): _tracefile.write(_logprefix + msg + '\n') def warning(msg): sys.stderr.write('bzr: warning: ' + msg + '\n') _write_trace('warning: ' + msg) mutter = _write_trace def note(msg): b = '* ' + str(msg) + '\n' if not silent: sys.stderr.write(b) _write_trace('note: ' + msg) def log_error(msg): sys.stderr.write(msg + '\n') _write_trace(msg) # TODO: Something to log exceptions in here. def _rollover_trace_maybe(trace_fname): try: size = os.stat(trace_fname)[stat.ST_SIZE] if size <= 4 << 20: return old_fname = trace_fname + '.old' try: # must remove before rename on windows os.remove(old_fname) except OSError: pass try: # might fail if in use on windows os.rename(trace_fname, old_fname) except OSError: pass except OSError: return def create_tracefile(argv): # TODO: Also show contents of /etc/lsb-release, if it can be parsed. # Perhaps that should eventually go into the platform library? # TODO: If the file doesn't exist, add a note describing it. # Messages are always written to here, so that we have some # information if something goes wrong. In a future version this # file will be removed on successful completion. global _starttime, _tracefile _starttime = os.times()[4] trace_fname = os.path.join(os.path.expanduser('~/.bzr.log')) _rollover_trace_maybe(trace_fname) # buffering=1 means line buffered _tracefile = codecs.open(trace_fname, 'at', 'utf8', buffering=1) t = _tracefile if os.fstat(t.fileno())[stat.ST_SIZE] == 0: t.write("\nthis is a debug log for diagnosing/reporting problems in bzr\n") t.write("you can delete or truncate this file, or include sections in\n") t.write("bug reports to bazaar-ng@lists.canonical.com\n\n") # TODO: If we failed to create the file, perhaps give a warning # but don't abort; send things to /dev/null instead? _write_trace('bzr %s invoked on python %s (%s)' % (bzrlib.__version__, '.'.join(map(str, sys.version_info)), sys.platform)) _write_trace(' arguments: %r' % argv) _write_trace(' working dir: ' + os.getcwdu()) def close_trace(): times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum, %.3f elapsed" % (times[:4] + ((times[4] - _starttime),))) def log_exception(e): import traceback, cStringIO s = cStringIO.StringIO() traceback.print_exc(None, s) for l in s.getvalue().split('\n'): _write_trace(l) commit refs/heads/master mark :334 committer Martin Pool 1115107227 +1000 data 3 doc from :333 M 644 inline TODO data 7280 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be ``bzr export -r REV``. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * ``bzr help commands`` should give a one-line summary of each command. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Read and write locks on branch while it's open. * Separate read and write version checks? * ``bzr status FILE...`` * Check all commands have decent help. * Autogenerate argument/option help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. Medium things ------------- * Display command grammar in help messages rather than hardcoding it. * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` M 644 inline bzrlib/trace.py data 3825 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import sys, os, time, socket, stat, codecs import bzrlib ###################################################################### # messages and logging global _tracefile, _starttime _tracefile = None # used to have % (os.environ['USER'], time.time(), os.getpid()), 'w') _starttime = None # If false, notes also go to stdout; should replace this with --silent # at some point. silent = False # fix this if we ever fork within python _mypid = os.getpid() _logprefix = '[%d] ' % _mypid def _write_trace(msg): _tracefile.write(_logprefix + msg + '\n') def warning(msg): sys.stderr.write('bzr: warning: ' + msg + '\n') _write_trace('warning: ' + msg) mutter = _write_trace def note(msg): b = '* ' + str(msg) + '\n' if not silent: sys.stderr.write(b) _write_trace('note: ' + msg) def log_error(msg): sys.stderr.write(msg + '\n') _write_trace(msg) def _rollover_trace_maybe(trace_fname): try: size = os.stat(trace_fname)[stat.ST_SIZE] if size <= 4 << 20: return old_fname = trace_fname + '.old' try: # must remove before rename on windows os.remove(old_fname) except OSError: pass try: # might fail if in use on windows os.rename(trace_fname, old_fname) except OSError: pass except OSError: return def create_tracefile(argv): # Messages are always written to here, so that we have some # information if something goes wrong. In a future version this # file will be removed on successful completion. global _starttime, _tracefile _starttime = os.times()[4] trace_fname = os.path.join(os.path.expanduser('~/.bzr.log')) _rollover_trace_maybe(trace_fname) # buffering=1 means line buffered _tracefile = codecs.open(trace_fname, 'at', 'utf8', buffering=1) t = _tracefile if os.fstat(t.fileno())[stat.ST_SIZE] == 0: t.write("\nthis is a debug log for diagnosing/reporting problems in bzr\n") t.write("you can delete or truncate this file, or include sections in\n") t.write("bug reports to bazaar-ng@lists.canonical.com\n\n") # TODO: If we failed to create the file, perhaps give a warning # but don't abort; send things to /dev/null instead? _write_trace('bzr %s invoked on python %s (%s)' % (bzrlib.__version__, '.'.join(map(str, sys.version_info)), sys.platform)) _write_trace(' arguments: %r' % argv) _write_trace(' working dir: ' + os.getcwdu()) def close_trace(): times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum, %.3f elapsed" % (times[:4] + ((times[4] - _starttime),))) def log_exception(e): import traceback, cStringIO s = cStringIO.StringIO() traceback.print_exc(None, s) for l in s.getvalue().split('\n'): _write_trace(l) commit refs/heads/master mark :335 committer Martin Pool 1115107478 +1000 data 42 - add new failing test for command parsing from :334 M 644 inline testbzr data 4833 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback TESTDIR = "testbzr.tmp" LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): logfile.write('$ %s\n' % cmd) cmd = cmd.split() else: logfile.write('$ %r\n' % cmd) return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open(LOGFILENAME, 'wt', buffering=1) try: runcmd(['mkdir', TESTDIR]) cd(TESTDIR) progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == '''? test.txt\n''' out = backtick("bzr status --all") assert out == "? test.txt\n" progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == "A test.txt\n" progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' cd('..') progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :336 committer Martin Pool 1115107646 +1000 data 24 - fix up 'bzr --version' from :335 M 644 inline bzrlib/commands.py data 29371 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool http://bazaar-ng.org/ **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** * Metadata format is not stable yet -- you may need to discard history in the future. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr rename FROM TO Rename one file. bzr move FROM... DESTDIR Move one or more files to a different directory. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export [-r REVNO] DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. bzr check Verify history is stored safely. (for more type 'bzr help commands') """ import sys, os, time, types, shutil, tempfile, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' CMD_ALIASES = { '?': 'help', 'ci': 'commit', 'checkin': 'commit', 'di': 'diff', 'st': 'status', 'stat': 'status', } def get_cmd_class(cmd): cmd = str(cmd) cmd = CMD_ALIASES.get(cmd, cmd) try: cmd_class = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: raise BzrError("unknown command %r" % cmd) return cmd, cmd_class class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return True if the command was successful, False if not. """ return True class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_options = ['all'] def run(self, all=False): #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) def Relpath(Command): """Show path of a file relative to root""" takes_args = ('filename') def run(self): print Branch(self.args['filename']).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) class cmd_log(Command): """Show log of this branch. TODO: Options to show ids; to limit range; etc. """ takes_options = ['timezone', 'verbose'] def run(self, timezone='original', verbose=False): Branch('.').write_log(show_timezone=timezone, verbose=verbose) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern""" takes_args = ['name_pattern'] def run(self, name_pattern): b = Branch('.') # XXX: This will fail if it's a hardlink; should use an AtomicFile class. f = open(b.abspath('.bzrignore'), 'at') f.write(name_pattern + '\n') f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them.""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True def run(self, revno): try: revno = int(revno) except ValueError: raise BzrError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) or NONE_STRING class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revno=None): b = Branch('.') if revno == None: rh = b.revision_history[-1] else: rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'verbose'] def run(self, message=None, verbose=False): if not message: raise BzrCommandError("please specify a commit message") Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] def run(self, topic=None): help(topic) def help(topic=None): if topic == None: print __doc__ elif topic == 'commands': help_commands() else: help_on_command(topic) def help_on_command(cmdname): cmdname = str(cmdname) from inspect import getdoc topic, cmdclass = get_cmd_class(cmdname) doc = getdoc(cmdclass) if doc == None: raise NotImplementedError("sorry, no detailed help yet for %r" % cmdname) if '\n' in doc: short, rest = doc.split('\n', 1) else: short = doc rest = '' print 'usage: bzr ' + topic, for aname in cmdclass.takes_args: aname = aname.upper() if aname[-1] in ['$', '+']: aname = aname[:-1] + '...' elif aname[-1] == '?': aname = '[' + aname[:-1] + ']' elif aname[-1] == '*': aname = '[' + aname[:-1] + '...]' print aname, print print short if rest: print rest help_on_option(cmdclass.takes_options) def help_on_option(options): if not options: return print print 'options:' for on in options: l = ' --' + on for shortname, longname in SHORT_OPTIONS.items(): if longname == on: l += ', -' + shortname break print l def help_commands(): """List all commands""" import inspect accu = [] for k, v in globals().items(): if k.startswith('cmd_'): accu.append((k[4:].replace('_','-'), v)) accu.sort() for cmdname, cmdclass in accu: if cmdclass.hidden: continue print cmdname help = inspect.getdoc(cmdclass) if help: print " " + help.split('\n', 1)[0] ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: if args: help(args[0]) else: help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option %r is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs) or 0 def _report_exception(e, summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception(e) if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.trace.create_tracefile(argv) try: try: ret = run_bzr(argv) # do this here to catch EPIPE sys.stdout.flush() return ret except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception(e, 'error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(e, msg) return 2 except KeyboardInterrupt, e: _report_exception(e, 'interrupted', quiet=True) return 2 except Exception, e: quiet = False if isinstance(e, IOError) and e.errno == errno.EPIPE: quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(e, msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :337 committer Martin Pool 1115107995 +1000 data 43 - Clarify return codes from command objects from :336 M 644 inline bzrlib/commands.py data 29464 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool http://bazaar-ng.org/ **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** * Metadata format is not stable yet -- you may need to discard history in the future. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr rename FROM TO Rename one file. bzr move FROM... DESTDIR Move one or more files to a different directory. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export [-r REVNO] DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. bzr check Verify history is stored safely. (for more type 'bzr help commands') """ import sys, os, time, types, shutil, tempfile, fnmatch, difflib, os.path from sets import Set from pprint import pprint from stat import * from glob import glob import bzrlib from bzrlib.store import ImmutableStore from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date BZR_DIFF_FORMAT = "## Bazaar-NG diff, format 0 ##\n" BZR_PATCHNAME_FORMAT = 'cset:sha1:%s' ## standard representation NONE_STRING = '(none)' EMPTY = 'empty' CMD_ALIASES = { '?': 'help', 'ci': 'commit', 'checkin': 'commit', 'di': 'diff', 'st': 'status', 'stat': 'status', } def get_cmd_class(cmd): cmd = str(cmd) cmd = CMD_ALIASES.get(cmd, cmd) try: cmd_class = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: raise BzrError("unknown command %r" % cmd) return cmd, cmd_class class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_options = ['all'] def run(self, all=False): #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) def Relpath(Command): """Show path of a file relative to root""" takes_args = ('filename') def run(self): print Branch(self.args['filename']).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) class cmd_log(Command): """Show log of this branch. TODO: Options to show ids; to limit range; etc. """ takes_options = ['timezone', 'verbose'] def run(self, timezone='original', verbose=False): Branch('.').write_log(show_timezone=timezone, verbose=verbose) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern""" takes_args = ['name_pattern'] def run(self, name_pattern): b = Branch('.') # XXX: This will fail if it's a hardlink; should use an AtomicFile class. f = open(b.abspath('.bzrignore'), 'at') f.write(name_pattern + '\n') f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them.""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True def run(self, revno): try: revno = int(revno) except ValueError: raise BzrError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) or NONE_STRING class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revno=None): b = Branch('.') if revno == None: rh = b.revision_history[-1] else: rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'verbose'] def run(self, message=None, verbose=False): if not message: raise BzrCommandError("please specify a commit message") Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] def run(self, topic=None): help(topic) def help(topic=None): if topic == None: print __doc__ elif topic == 'commands': help_commands() else: help_on_command(topic) def help_on_command(cmdname): cmdname = str(cmdname) from inspect import getdoc topic, cmdclass = get_cmd_class(cmdname) doc = getdoc(cmdclass) if doc == None: raise NotImplementedError("sorry, no detailed help yet for %r" % cmdname) if '\n' in doc: short, rest = doc.split('\n', 1) else: short = doc rest = '' print 'usage: bzr ' + topic, for aname in cmdclass.takes_args: aname = aname.upper() if aname[-1] in ['$', '+']: aname = aname[:-1] + '...' elif aname[-1] == '?': aname = '[' + aname[:-1] + ']' elif aname[-1] == '*': aname = '[' + aname[:-1] + '...]' print aname, print print short if rest: print rest help_on_option(cmdclass.takes_options) def help_on_option(options): if not options: return print print 'options:' for on in options: l = ' --' + on for shortname, longname in SHORT_OPTIONS.items(): if longname == on: l += ', -' + shortname break print l def help_commands(): """List all commands""" import inspect accu = [] for k, v in globals().items(): if k.startswith('cmd_'): accu.append((k[4:].replace('_','-'), v)) accu.sort() for cmdname, cmdclass in accu: if cmdclass.hidden: continue print cmdname help = inspect.getdoc(cmdclass) if help: print " " + help.split('\n', 1)[0] ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: if args: help(args[0]) else: help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option %r is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(e, summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception(e) if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.trace.create_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception(e, 'error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(e, msg) return 2 except KeyboardInterrupt, e: _report_exception(e, 'interrupted', quiet=True) return 2 except Exception, e: quiet = False if isinstance(e, IOError) and e.errno == errno.EPIPE: quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(e, msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :338 committer Martin Pool 1115108263 +1000 data 25 - cleanup of some imports from :337 M 644 inline bzrlib/commands.py data 29188 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool http://bazaar-ng.org/ **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** * Metadata format is not stable yet -- you may need to discard history in the future. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr rename FROM TO Rename one file. bzr move FROM... DESTDIR Move one or more files to a different directory. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export [-r REVNO] DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. bzr check Verify history is stored safely. (for more type 'bzr help commands') """ import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date CMD_ALIASES = { '?': 'help', 'ci': 'commit', 'checkin': 'commit', 'di': 'diff', 'st': 'status', 'stat': 'status', } def get_cmd_class(cmd): cmd = str(cmd) cmd = CMD_ALIASES.get(cmd, cmd) try: cmd_class = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: raise BzrError("unknown command %r" % cmd) return cmd, cmd_class class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_options = ['all'] def run(self, all=False): #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) def Relpath(Command): """Show path of a file relative to root""" takes_args = ('filename') def run(self): print Branch(self.args['filename']).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) class cmd_log(Command): """Show log of this branch. TODO: Options to show ids; to limit range; etc. """ takes_options = ['timezone', 'verbose'] def run(self, timezone='original', verbose=False): Branch('.').write_log(show_timezone=timezone, verbose=verbose) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern""" takes_args = ['name_pattern'] def run(self, name_pattern): b = Branch('.') # XXX: This will fail if it's a hardlink; should use an AtomicFile class. f = open(b.abspath('.bzrignore'), 'at') f.write(name_pattern + '\n') f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them.""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revno=None): b = Branch('.') if revno == None: rh = b.revision_history[-1] else: rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'verbose'] def run(self, message=None, verbose=False): if not message: raise BzrCommandError("please specify a commit message") Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] def run(self, topic=None): help(topic) def help(topic=None): if topic == None: print __doc__ elif topic == 'commands': help_commands() else: help_on_command(topic) def help_on_command(cmdname): cmdname = str(cmdname) from inspect import getdoc topic, cmdclass = get_cmd_class(cmdname) doc = getdoc(cmdclass) if doc == None: raise NotImplementedError("sorry, no detailed help yet for %r" % cmdname) if '\n' in doc: short, rest = doc.split('\n', 1) else: short = doc rest = '' print 'usage: bzr ' + topic, for aname in cmdclass.takes_args: aname = aname.upper() if aname[-1] in ['$', '+']: aname = aname[:-1] + '...' elif aname[-1] == '?': aname = '[' + aname[:-1] + ']' elif aname[-1] == '*': aname = '[' + aname[:-1] + '...]' print aname, print print short if rest: print rest help_on_option(cmdclass.takes_options) def help_on_option(options): if not options: return print print 'options:' for on in options: l = ' --' + on for shortname, longname in SHORT_OPTIONS.items(): if longname == on: l += ', -' + shortname break print l def help_commands(): """List all commands""" import inspect accu = [] for k, v in globals().items(): if k.startswith('cmd_'): accu.append((k[4:].replace('_','-'), v)) accu.sort() for cmdname, cmdclass in accu: if cmdclass.hidden: continue print cmdname help = inspect.getdoc(cmdclass) if help: print " " + help.split('\n', 1)[0] ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: if args: help(args[0]) else: help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option %r is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(e, summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception(e) if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.trace.create_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception(e, 'error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(e, msg) return 2 except KeyboardInterrupt, e: _report_exception(e, 'interrupted', quiet=True) return 2 except Exception, e: quiet = False if isinstance(e, IOError) and e.errno == errno.EPIPE: quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(e, msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :339 committer Martin Pool 1115112448 +1000 data 15 many more diffs from :338 M 644 inline TODO data 7396 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be ``bzr export -r REV``. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * ``bzr help commands`` should give a one-line summary of each command. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Read and write locks on branch while it's open. * Separate read and write version checks? * ``bzr status FILE...`` * Check all commands have decent help. * Autogenerate argument/option help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. Medium things ------------- * Display command grammar in help messages rather than hardcoding it. * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` M 644 inline testbzr data 5983 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): logfile.write('$ %s\n' % cmd) cmd = cmd.split() else: logfile.write('$ %r\n' % cmd) return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open(LOGFILENAME, 'wt', buffering=1) try: runcmd(['mkdir', TESTDIR]) cd(TESTDIR) progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == '''? test.txt\n''' out = backtick("bzr status --all") assert out == "? test.txt\n" progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == "A test.txt\n" progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') runcmd('bzr move ../hello.txt .') assert exists('../hello.txt') runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') cd('..') progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :340 committer Martin Pool 1115123867 +1000 data 59 - more testcase fixes - remove test.sh in favour of testbzr from :339 D test.sh M 644 inline NEWS data 4743 bzr-0.0.5 NOT RELEASED YET ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New 'bzr ignore PATTERN' command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on mango-sorbet by Scott James Remnant. * Better help messages for many commands. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/tests.py data 6092 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # XXX: We might prefer these to be in a text file rather than Python # source, but that only works in doctest from Python 2.4 and later, # which is not present in Warty. r""" Bazaar-NG test cases ******************** These are run by ``bzr.doctest``. >>> import bzrlib, os >>> from bzrlib import ScratchBranch >>> from bzrlib.osutils import isdir, isfile The basic object is a Branch. We have a special helper class ScratchBranch that automatically makes a directory and cleans itself up, but is in other respects identical. ScratchBranches are initially empty: >>> b = bzrlib.ScratchBranch() >>> b.show_status() New files in that directory are, it is initially unknown: >>> file(b.base + '/hello.c', 'wt').write('int main() {}') >>> b.show_status() ? hello.c That's not quite true; some files (like editor backups) are ignored by default: >>> file(b.base + '/hello.c~', 'wt').write('int main() {}') >>> b.show_status() ? hello.c >>> list(b.unknowns()) ['hello.c'] The ``add`` command marks a file to be added in the next revision: >>> b.add('hello.c') >>> b.show_status() A hello.c You can also add files that otherwise would be ignored. The ignore patterns only apply to files that would be otherwise unknown, so they have no effect once it's added. >>> b.add('hello.c~') >>> b.show_status() A hello.c A hello.c~ It is an error to add a file that isn't present in the working copy: >>> b.add('nothere') Traceback (most recent call last): ... BzrError: ('cannot add: not a regular file or directory: nothere', []) If we add a file and then change our mind, we can either revert it or remove the file. If we revert, we are left with the working copy (in either I or ? state). If we remove, the working copy is gone. Let's do that to the backup, presumably added accidentally. >>> b.remove('hello.c~') >>> b.show_status() A hello.c Now to commit, creating a new revision. (Fake the date and name for reproducibility.) >>> b.commit('start hello world', timestamp=0, committer='foo@nowhere') >>> b.show_status() >>> b.show_status(show_all=True) . hello.c I hello.c~ We can look back at history >>> r = b.get_revision(b.lookup_revision(1)) >>> r.message 'start hello world' >>> b.write_log(show_timezone='utc') ---------------------------------------- revno: 1 committer: foo@nowhere timestamp: Thu 1970-01-01 00:00:00 +0000 message: start hello world (The other fields will be a bit unpredictable, depending on who ran this test and when.) As of 2005-02-21, we can also add subdirectories to the revision! >>> os.mkdir(b.base + "/lib") >>> b.show_status() ? lib/ >>> b.add('lib') >>> b.show_status() A lib/ >>> b.commit('add subdir') >>> b.show_status() >>> b.show_status(show_all=True) . hello.c I hello.c~ . lib/ and we can also add files within subdirectories: >>> file(b.base + '/lib/hello', 'w').write('hello!\n') >>> b.show_status() ? lib/hello Tests for adding subdirectories, etc. >>> b = bzrlib.branch.ScratchBranch() >>> os.mkdir(b.abspath('d1')) >>> os.mkdir(b.abspath('d2')) >>> os.mkdir(b.abspath('d2/d3')) >>> list(b.working_tree().unknowns()) ['d1', 'd2'] Create some files, but they're not seen as unknown yet: >>> file(b.abspath('d1/f1'), 'w').close() >>> file(b.abspath('d2/f2'), 'w').close() >>> file(b.abspath('d2/f3'), 'w').close() >>> [v[0] for v in b.inventory.directories()] [''] >>> list(b.working_tree().unknowns()) ['d1', 'd2'] Adding a directory, and we see the file underneath: >>> b.add('d1') >>> [v[0] for v in b.inventory.directories()] ['', 'd1'] >>> list(b.working_tree().unknowns()) ['d2', 'd1/f1'] >>> # d2 comes first because it's in the top directory >>> b.add('d2') >>> b.commit('add some stuff') >>> list(b.working_tree().unknowns()) ['d1/f1', 'd2/d3', 'd2/f2', 'd2/f3'] >>> b.add('d1/f1') >>> list(b.working_tree().unknowns()) ['d2/d3', 'd2/f2', 'd2/f3'] Tests for ignored files and patterns: >>> b = ScratchBranch(dirs=['src', 'doc'], ... files=['configure.in', 'configure', ... 'doc/configure', 'foo.c', ... 'foo']) >>> list(b.unknowns()) ['configure', 'configure.in', 'doc', 'foo', 'foo.c', 'src'] >>> b.add(['doc', 'foo.c', 'src', 'configure.in']) >>> list(b.unknowns()) ['configure', 'foo', 'doc/configure'] >>> f = file(b.abspath('.bzrignore'), 'w') >>> f.write('./configure\n' ... './foo\n') >>> f.close() >>> b.add('.bzrignore') >>> list(b.unknowns()) ['doc/configure'] >>> b.commit("commit 1") >>> list(b.unknowns()) ['doc/configure'] >>> b.add("doc/configure") >>> b.commit("commit more") >>> del b Renames, etc: >>> b = ScratchBranch(files=['foo'], dirs=['subdir']) >>> b.add(['foo', 'subdir']) >>> b.commit('add foo') >>> list(b.unknowns()) [] >>> b.move(['foo'], 'subdir') foo => subdir/foo >>> b.show_status() R foo => subdir/foo >>> b.commit("move foo to subdir") >>> isfile(b.abspath('foo')) False >>> isfile(b.abspath('subdir/foo')) True """ M 644 inline testbzr data 6060 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): logfile.write('$ %s\n' % cmd) cmd = cmd.split() else: logfile.write('$ %r\n' % cmd) return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open(LOGFILENAME, 'wt', buffering=1) try: runcmd(['mkdir', TESTDIR]) cd(TESTDIR) progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == '''? test.txt\n''' out = backtick("bzr status --all") assert out == "? test.txt\n" progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == "A test.txt\n" progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') cd('..') progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :341 committer Martin Pool 1115256586 +1000 data 4 todo from :340 M 644 inline TODO data 7502 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be ``bzr export -r REV``. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * ``bzr help commands`` should give a one-line summary of each command. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Read and write locks on branch while it's open. * Separate read and write version checks? * ``bzr status FILE...`` * Check all commands have decent help. * Autogenerate argument/option help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. * Split BzrError into various more specific subclasses for different errors people might want to catch. Medium things ------------- * Display command grammar in help messages rather than hardcoding it. * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :342 committer Martin Pool 1115259487 +1000 data 4 todo from :341 M 644 inline TODO data 7616 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be ``bzr export -r REV``. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * ``bzr help commands`` should give a one-line summary of each command. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Read and write locks on branch while it's open. * Separate read and write version checks? * ``bzr status FILE...`` * Check all commands have decent help. * Autogenerate argument/option help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. * Split BzrError into various more specific subclasses for different errors people might want to catch. Medium things ------------- * Display command grammar in help messages rather than hardcoding it. * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :343 committer Martin Pool 1115259556 +1000 data 3 doc from :342 M 644 inline bzrlib/branch.py data 34406 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. base Base directory of the branch. """ def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. """ ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) self.append_revision(rev_id) if verbose: note("commited r%d" % self.revno()) def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] else: return None def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original', verbose=False): """Write out human-readable log of commits to this branch utc -- If true, show dates in universal time, not local time.""" ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l if verbose == True and precursor != None: print 'changed files:' tree = self.revision_tree(p) prevtree = self.revision_tree(precursor) for file_state, fid, old_name, new_name, kind in \ diff_trees(prevtree, tree, ): if file_state == 'A' or file_state == 'M': show_status(file_state, kind, new_name) elif file_state == 'D': show_status(file_state, kind, old_name) elif file_state == 'R': show_status(file_state, kind, old_name + ' => ' + new_name) revno += 1 precursor = p def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(self, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo TODO: Get state for single files. """ # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = self.basis_tree() new = self.working_tree() for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("weird file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" try: shutil.rmtree(self.base) except OSError: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :344 committer Martin Pool 1115260178 +1000 data 71 - It's not an error to use the library without opening the trace file from :343 M 644 inline NEWS data 4867 bzr-0.0.5 NOT RELEASED YET ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New 'bzr ignore PATTERN' command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on mango-sorbet by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/__init__.py data 1604 # (C) 2005 Canonical Development Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr library""" from inventory import Inventory, InventoryEntry from branch import Branch, ScratchBranch from osutils import format_date from tree import Tree from diff import diff_trees from trace import mutter, warning, open_tracefile import add BZRDIR = ".bzr" DEFAULT_IGNORE = ['.bzr.log', '*~', '#*#', '*$', '.#*', '*.tmp', '*.bak', '*.BAK', '*.orig', '*.o', '*.obj', '*.a', '*.py[oc]', '*.so', '*.exe', '*.elc', '{arch}', 'CVS', '.svn', '_darcs', 'SCCS', 'RCS', 'BitKeeper', 'TAGS', '.make.state', '.sconsign', '.tmp*'] IGNORE_FILENAME = ".bzrignore" import locale user_encoding = locale.getpreferredencoding() __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __version__ = '0.0.5pre' M 644 inline bzrlib/commands.py data 29180 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool http://bazaar-ng.org/ **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** * Metadata format is not stable yet -- you may need to discard history in the future. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr rename FROM TO Rename one file. bzr move FROM... DESTDIR Move one or more files to a different directory. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export [-r REVNO] DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. bzr check Verify history is stored safely. (for more type 'bzr help commands') """ import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date CMD_ALIASES = { '?': 'help', 'ci': 'commit', 'checkin': 'commit', 'di': 'diff', 'st': 'status', 'stat': 'status', } def get_cmd_class(cmd): cmd = str(cmd) cmd = CMD_ALIASES.get(cmd, cmd) try: cmd_class = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: raise BzrError("unknown command %r" % cmd) return cmd, cmd_class class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_options = ['all'] def run(self, all=False): #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) def Relpath(Command): """Show path of a file relative to root""" takes_args = ('filename') def run(self): print Branch(self.args['filename']).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) class cmd_log(Command): """Show log of this branch. TODO: Options to show ids; to limit range; etc. """ takes_options = ['timezone', 'verbose'] def run(self, timezone='original', verbose=False): Branch('.').write_log(show_timezone=timezone, verbose=verbose) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern""" takes_args = ['name_pattern'] def run(self, name_pattern): b = Branch('.') # XXX: This will fail if it's a hardlink; should use an AtomicFile class. f = open(b.abspath('.bzrignore'), 'at') f.write(name_pattern + '\n') f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them.""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revno=None): b = Branch('.') if revno == None: rh = b.revision_history[-1] else: rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'verbose'] def run(self, message=None, verbose=False): if not message: raise BzrCommandError("please specify a commit message") Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] def run(self, topic=None): help(topic) def help(topic=None): if topic == None: print __doc__ elif topic == 'commands': help_commands() else: help_on_command(topic) def help_on_command(cmdname): cmdname = str(cmdname) from inspect import getdoc topic, cmdclass = get_cmd_class(cmdname) doc = getdoc(cmdclass) if doc == None: raise NotImplementedError("sorry, no detailed help yet for %r" % cmdname) if '\n' in doc: short, rest = doc.split('\n', 1) else: short = doc rest = '' print 'usage: bzr ' + topic, for aname in cmdclass.takes_args: aname = aname.upper() if aname[-1] in ['$', '+']: aname = aname[:-1] + '...' elif aname[-1] == '?': aname = '[' + aname[:-1] + ']' elif aname[-1] == '*': aname = '[' + aname[:-1] + '...]' print aname, print print short if rest: print rest help_on_option(cmdclass.takes_options) def help_on_option(options): if not options: return print print 'options:' for on in options: l = ' --' + on for shortname, longname in SHORT_OPTIONS.items(): if longname == on: l += ', -' + shortname break print l def help_commands(): """List all commands""" import inspect accu = [] for k, v in globals().items(): if k.startswith('cmd_'): accu.append((k[4:].replace('_','-'), v)) accu.sort() for cmdname, cmdclass in accu: if cmdclass.hidden: continue print cmdname help = inspect.getdoc(cmdclass) if help: print " " + help.split('\n', 1)[0] ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: if args: help(args[0]) else: help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option %r is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(e, summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception(e) if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception(e, 'error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(e, msg) return 2 except KeyboardInterrupt, e: _report_exception(e, 'interrupted', quiet=True) return 2 except Exception, e: quiet = False if isinstance(e, IOError) and e.errno == errno.EPIPE: quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(e, msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/trace.py data 3911 # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " """Messages and logging for bazaar-ng Nothing is actually logged unless you call bzrlib.open_tracefile(). """ import sys, os, time, socket, stat, codecs import bzrlib ###################################################################### # messages and logging global _tracefile, _starttime _tracefile = None # used to have % (os.environ['USER'], time.time(), os.getpid()), 'w') _starttime = None # If false, notes also go to stdout; should replace this with --silent # at some point. silent = False # fix this if we ever fork within python _mypid = os.getpid() _logprefix = '[%d] ' % _mypid def _write_trace(msg): if _tracefile: _tracefile.write(_logprefix + msg + '\n') def warning(msg): sys.stderr.write('bzr: warning: ' + msg + '\n') _write_trace('warning: ' + msg) mutter = _write_trace def note(msg): b = '* ' + str(msg) + '\n' if not silent: sys.stderr.write(b) _write_trace('note: ' + msg) def log_error(msg): sys.stderr.write(msg + '\n') _write_trace(msg) def _rollover_trace_maybe(trace_fname): try: size = os.stat(trace_fname)[stat.ST_SIZE] if size <= 4 << 20: return old_fname = trace_fname + '.old' try: # must remove before rename on windows os.remove(old_fname) except OSError: pass try: # might fail if in use on windows os.rename(trace_fname, old_fname) except OSError: pass except OSError: return def open_tracefile(argv): # Messages are always written to here, so that we have some # information if something goes wrong. In a future version this # file will be removed on successful completion. global _starttime, _tracefile _starttime = os.times()[4] trace_fname = os.path.join(os.path.expanduser('~/.bzr.log')) _rollover_trace_maybe(trace_fname) # buffering=1 means line buffered _tracefile = codecs.open(trace_fname, 'at', 'utf8', buffering=1) t = _tracefile if os.fstat(t.fileno())[stat.ST_SIZE] == 0: t.write("\nthis is a debug log for diagnosing/reporting problems in bzr\n") t.write("you can delete or truncate this file, or include sections in\n") t.write("bug reports to bazaar-ng@lists.canonical.com\n\n") # TODO: If we failed to create the file, perhaps give a warning # but don't abort; send things to /dev/null instead? _write_trace('bzr %s invoked on python %s (%s)' % (bzrlib.__version__, '.'.join(map(str, sys.version_info)), sys.platform)) _write_trace(' arguments: %r' % argv) _write_trace(' working dir: ' + os.getcwdu()) def close_trace(): times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum, %.3f elapsed" % (times[:4] + ((times[4] - _starttime),))) def log_exception(e): import traceback, cStringIO s = cStringIO.StringIO() traceback.print_exc(None, s) for l in s.getvalue().split('\n'): _write_trace(l) commit refs/heads/master mark :345 committer Martin Pool 1115260252 +1000 data 30 - ReST formatting in news file from :344 M 644 inline NEWS data 5034 bzr-0.0.5 NOT RELEASED YET *************************** ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New 'bzr ignore PATTERN' command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on mango-sorbet by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. bzr-0.0.4 2005-04-22 ********************* ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ********************* ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 *********** PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 *********************************** ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ********************* ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ************************ ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. commit refs/heads/master mark :346 committer Martin Pool 1115260380 +1000 data 4 todo from :345 M 644 inline TODO data 7805 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be ``bzr export -r REV``. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * ``bzr help commands`` should give a one-line summary of each command. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Read and write locks on branch while it's open. * Separate read and write version checks? * ``bzr status FILE...`` * Check all commands have decent help. * Autogenerate argument/option help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. * Split BzrError into various more specific subclasses for different errors people might want to catch. Medium things ------------- * Display command grammar in help messages rather than hardcoding it. * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :347 committer Martin Pool 1115261027 +1000 data 31 - add-bzr-to-baz utility script from :346 M 644 inline contrib/add-bzr-to-baz data 181 #! /bin/sh -e # Add a file that's version by bzr to baz with the same file-id if [ $# -ne 1 ] then echo "usage: $0 FILE" >&2 exit 1 fi baz add -i $( bzr file-id $1 ) $1 commit refs/heads/master mark :348 committer Martin Pool 1115261177 +1000 data 37 - Add vim and RCS ignorefile patterns from :347 M 644 inline NEWS data 4928 bzr-0.0.5 NOT RELEASED YET ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New 'bzr ignore PATTERN' command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on mango-sorbet by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/__init__.py data 1667 # (C) 2005 Canonical Development Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr library""" from inventory import Inventory, InventoryEntry from branch import Branch, ScratchBranch from osutils import format_date from tree import Tree from diff import diff_trees from trace import mutter, warning, open_tracefile import add BZRDIR = ".bzr" DEFAULT_IGNORE = ['.bzr.log', '*~', '#*#', '*$', '.#*', '.*.swp', '.*.tmp', '*.tmp', '*.bak', '*.BAK', '*.orig', '*.o', '*.obj', '*.a', '*.py[oc]', '*.so', '*.exe', '*.elc', '{arch}', 'CVS', '.svn', '_darcs', 'SCCS', 'RCS', '*,v', 'BitKeeper', 'TAGS', '.make.state', '.sconsign', '.tmp*'] IGNORE_FILENAME = ".bzrignore" import locale user_encoding = locale.getpreferredencoding() __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __version__ = '0.0.5pre' commit refs/heads/master mark :349 committer Martin Pool 1115261214 +1000 data 35 Better error for incorrect commands from :348 M 644 inline bzrlib/commands.py data 29187 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool http://bazaar-ng.org/ **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** * Metadata format is not stable yet -- you may need to discard history in the future. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr rename FROM TO Rename one file. bzr move FROM... DESTDIR Move one or more files to a different directory. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export [-r REVNO] DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. bzr check Verify history is stored safely. (for more type 'bzr help commands') """ import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date CMD_ALIASES = { '?': 'help', 'ci': 'commit', 'checkin': 'commit', 'di': 'diff', 'st': 'status', 'stat': 'status', } def get_cmd_class(cmd): cmd = str(cmd) cmd = CMD_ALIASES.get(cmd, cmd) try: cmd_class = globals()['cmd_' + cmd.replace('-', '_')] except KeyError: raise BzrCommandError("unknown command %r" % cmd) return cmd, cmd_class class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_options = ['all'] def run(self, all=False): #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) def Relpath(Command): """Show path of a file relative to root""" takes_args = ('filename') def run(self): print Branch(self.args['filename']).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) class cmd_log(Command): """Show log of this branch. TODO: Options to show ids; to limit range; etc. """ takes_options = ['timezone', 'verbose'] def run(self, timezone='original', verbose=False): Branch('.').write_log(show_timezone=timezone, verbose=verbose) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern""" takes_args = ['name_pattern'] def run(self, name_pattern): b = Branch('.') # XXX: This will fail if it's a hardlink; should use an AtomicFile class. f = open(b.abspath('.bzrignore'), 'at') f.write(name_pattern + '\n') f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them.""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revno=None): b = Branch('.') if revno == None: rh = b.revision_history[-1] else: rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'verbose'] def run(self, message=None, verbose=False): if not message: raise BzrCommandError("please specify a commit message") Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] def run(self, topic=None): help(topic) def help(topic=None): if topic == None: print __doc__ elif topic == 'commands': help_commands() else: help_on_command(topic) def help_on_command(cmdname): cmdname = str(cmdname) from inspect import getdoc topic, cmdclass = get_cmd_class(cmdname) doc = getdoc(cmdclass) if doc == None: raise NotImplementedError("sorry, no detailed help yet for %r" % cmdname) if '\n' in doc: short, rest = doc.split('\n', 1) else: short = doc rest = '' print 'usage: bzr ' + topic, for aname in cmdclass.takes_args: aname = aname.upper() if aname[-1] in ['$', '+']: aname = aname[:-1] + '...' elif aname[-1] == '?': aname = '[' + aname[:-1] + ']' elif aname[-1] == '*': aname = '[' + aname[:-1] + '...]' print aname, print print short if rest: print rest help_on_option(cmdclass.takes_options) def help_on_option(options): if not options: return print print 'options:' for on in options: l = ' --' + on for shortname, longname in SHORT_OPTIONS.items(): if longname == on: l += ', -' + shortname break print l def help_commands(): """List all commands""" import inspect accu = [] for k, v in globals().items(): if k.startswith('cmd_'): accu.append((k[4:].replace('_','-'), v)) accu.sort() for cmdname, cmdclass in accu: if cmdclass.hidden: continue print cmdname help = inspect.getdoc(cmdclass) if help: print " " + help.split('\n', 1)[0] ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: if args: help(args[0]) else: help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option %r is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(e, summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception(e) if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception(e, 'error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(e, msg) return 2 except KeyboardInterrupt, e: _report_exception(e, 'interrupted', quiet=True) return 2 except Exception, e: quiet = False if isinstance(e, IOError) and e.errno == errno.EPIPE: quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(e, msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :350 committer Martin Pool 1115261745 +1000 data 113 - refactor command aliases into command classes - fix edge bugs in aliases and help command - test cases for this from :349 M 644 inline bzrlib/commands.py data 29752 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Bazaar-NG -- a free distributed version-control tool http://bazaar-ng.org/ **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** * Metadata format is not stable yet -- you may need to discard history in the future. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr rename FROM TO Rename one file. bzr move FROM... DESTDIR Move one or more files to a different directory. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export [-r REVNO] DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. bzr check Verify history is stored safely. (for more type 'bzr help commands') """ import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def _get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in _get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass else: raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False): #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) def Relpath(Command): """Show path of a file relative to root""" takes_args = ('filename') def run(self): print Branch(self.args['filename']).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) class cmd_log(Command): """Show log of this branch. TODO: Options to show ids; to limit range; etc. """ takes_options = ['timezone', 'verbose'] def run(self, timezone='original', verbose=False): Branch('.').write_log(show_timezone=timezone, verbose=verbose) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern""" takes_args = ['name_pattern'] def run(self, name_pattern): b = Branch('.') # XXX: This will fail if it's a hardlink; should use an AtomicFile class. f = open(b.abspath('.bzrignore'), 'at') f.write(name_pattern + '\n') f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them.""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revno=None): b = Branch('.') if revno == None: rh = b.revision_history[-1] else: rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, verbose=False): if not message: raise BzrCommandError("please specify a commit message") Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): help(topic) def help(topic=None): if topic == None: print __doc__ elif topic == 'commands': help_commands() else: help_on_command(topic) def help_on_command(cmdname): cmdname = str(cmdname) from inspect import getdoc topic, cmdclass = _get_cmd_class(cmdname) doc = getdoc(cmdclass) if doc == None: raise NotImplementedError("sorry, no detailed help yet for %r" % cmdname) if '\n' in doc: short, rest = doc.split('\n', 1) else: short = doc rest = '' print 'usage: bzr ' + topic, for aname in cmdclass.takes_args: aname = aname.upper() if aname[-1] in ['$', '+']: aname = aname[:-1] + '...' elif aname[-1] == '?': aname = '[' + aname[:-1] + ']' elif aname[-1] == '*': aname = '[' + aname[:-1] + '...]' print aname, print print short if rest: print rest help_on_option(cmdclass.takes_options) def help_on_option(options): if not options: return print print 'options:' for on in options: l = ' --' + on for shortname, longname in SHORT_OPTIONS.items(): if longname == on: l += ', -' + shortname break print l def help_commands(): """List all commands""" import inspect accu = [] for cmdname, cmdclass in _get_all_cmds(): accu.append((cmdname, cmdclass)) accu.sort() for cmdname, cmdclass in accu: if cmdclass.hidden: continue print cmdname help = inspect.getdoc(cmdclass) if help: print " " + help.split('\n', 1)[0] ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: if args: help(args[0]) else: help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = _get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option %r is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(e, summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception(e) if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception(e, 'error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(e, msg) return 2 except KeyboardInterrupt, e: _report_exception(e, 'interrupted', quiet=True) return 2 except Exception, e: quiet = False if isinstance(e, IOError) and e.errno == errno.EPIPE: quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(e, msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline testbzr data 6419 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): logfile.write('$ %s\n' % cmd) cmd = cmd.split() else: logfile.write('$ %r\n' % cmd) return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open(LOGFILENAME, 'wt', buffering=1) try: runcmd(['mkdir', TESTDIR]) cd(TESTDIR) progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == '''? test.txt\n''' out = backtick("bzr status --all") assert out == "? test.txt\n" progress("command aliases") out = backtick("bzr st --all") assert out == "? test.txt\n" out = backtick("bzr stat") assert out == "? test.txt\n" progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help ci") runcmd("bzr help slartibartfast", 1) progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == "A test.txt\n" progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') cd('..') progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :351 committer Martin Pool 1115261979 +1000 data 43 - Split out help functions into bzrlib.help from :350 M 644 inline bzrlib/help.py data 3802 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA global_help = \ """Bazaar-NG -- a free distributed version-control tool http://bazaar-ng.org/ **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** * Metadata format is not stable yet -- you may need to discard history in the future. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr rename FROM TO Rename one file. bzr move FROM... DESTDIR Move one or more files to a different directory. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export [-r REVNO] DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. bzr check Verify history is stored safely. (for more type 'bzr help commands') """ def help(topic=None): if topic == None: print global_help elif topic == 'commands': help_commands() else: help_on_command(topic) def help_on_command(cmdname): cmdname = str(cmdname) from inspect import getdoc import commands topic, cmdclass = commands.get_cmd_class(cmdname) doc = getdoc(cmdclass) if doc == None: raise NotImplementedError("sorry, no detailed help yet for %r" % cmdname) if '\n' in doc: short, rest = doc.split('\n', 1) else: short = doc rest = '' print 'usage: bzr ' + topic, for aname in cmdclass.takes_args: aname = aname.upper() if aname[-1] in ['$', '+']: aname = aname[:-1] + '...' elif aname[-1] == '?': aname = '[' + aname[:-1] + ']' elif aname[-1] == '*': aname = '[' + aname[:-1] + '...]' print aname, print print short if rest: print rest help_on_option(cmdclass.takes_options) def help_on_option(options): import commands if not options: return print print 'options:' for on in options: l = ' --' + on for shortname, longname in commands.SHORT_OPTIONS.items(): if longname == on: l += ', -' + shortname break print l def help_commands(): """List all commands""" import inspect import commands accu = [] for cmdname, cmdclass in commands.get_all_cmds(): accu.append((cmdname, cmdclass)) accu.sort() for cmdname, cmdclass in accu: if cmdclass.hidden: continue print cmdname help = inspect.getdoc(cmdclass) if help: print " " + help.split('\n', 1)[0] M 644 inline bzrlib/commands.py data 26858 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass else: raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False): #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) def Relpath(Command): """Show path of a file relative to root""" takes_args = ('filename') def run(self): print Branch(self.args['filename']).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) class cmd_log(Command): """Show log of this branch. TODO: Options to show ids; to limit range; etc. """ takes_options = ['timezone', 'verbose'] def run(self, timezone='original', verbose=False): Branch('.').write_log(show_timezone=timezone, verbose=verbose) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern""" takes_args = ['name_pattern'] def run(self, name_pattern): b = Branch('.') # XXX: This will fail if it's a hardlink; should use an AtomicFile class. f = open(b.abspath('.bzrignore'), 'at') f.write(name_pattern + '\n') f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them.""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revno=None): b = Branch('.') if revno == None: rh = b.revision_history[-1] else: rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, verbose=False): if not message: raise BzrCommandError("please specify a commit message") Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option %r is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(e, summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception(e) if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception(e, 'error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(e, msg) return 2 except KeyboardInterrupt, e: _report_exception(e, 'interrupted', quiet=True) return 2 except Exception, e: quiet = False if isinstance(e, IOError) and e.errno == errno.EPIPE: quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(e, msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :352 committer Martin Pool 1115262158 +1000 data 30 - Show aliases in command help from :351 M 644 inline bzrlib/help.py data 3889 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA global_help = \ """Bazaar-NG -- a free distributed version-control tool http://bazaar-ng.org/ **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** * Metadata format is not stable yet -- you may need to discard history in the future. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. Interesting commands: bzr help [COMMAND] Show help screen bzr version Show software version/licence/non-warranty. bzr init Start versioning the current directory bzr add FILE... Make files versioned. bzr log Show revision history. bzr rename FROM TO Rename one file. bzr move FROM... DESTDIR Move one or more files to a different directory. bzr diff [FILE...] Show changes from last revision to working copy. bzr commit -m 'MESSAGE' Store current state as new revision. bzr export [-r REVNO] DESTINATION Export the branch state at a previous version. bzr status Show summary of pending changes. bzr remove FILE... Make a file not versioned. bzr info Show statistics about this branch. bzr check Verify history is stored safely. (for more type 'bzr help commands') """ def help(topic=None): if topic == None: print global_help elif topic == 'commands': help_commands() else: help_on_command(topic) def help_on_command(cmdname): cmdname = str(cmdname) from inspect import getdoc import commands topic, cmdclass = commands.get_cmd_class(cmdname) doc = getdoc(cmdclass) if doc == None: raise NotImplementedError("sorry, no detailed help yet for %r" % cmdname) if '\n' in doc: short, rest = doc.split('\n', 1) else: short = doc rest = '' print 'usage: bzr ' + topic, for aname in cmdclass.takes_args: aname = aname.upper() if aname[-1] in ['$', '+']: aname = aname[:-1] + '...' elif aname[-1] == '?': aname = '[' + aname[:-1] + ']' elif aname[-1] == '*': aname = '[' + aname[:-1] + '...]' print aname, print print short if cmdclass.aliases: print 'aliases: ' + ', '.join(cmdclass.aliases) if rest: print rest help_on_option(cmdclass.takes_options) def help_on_option(options): import commands if not options: return print print 'options:' for on in options: l = ' --' + on for shortname, longname in commands.SHORT_OPTIONS.items(): if longname == on: l += ', -' + shortname break print l def help_commands(): """List all commands""" import inspect import commands accu = [] for cmdname, cmdclass in commands.get_all_cmds(): accu.append((cmdname, cmdclass)) accu.sort() for cmdname, cmdclass in accu: if cmdclass.hidden: continue print cmdname help = inspect.getdoc(cmdclass) if help: print " " + help.split('\n', 1)[0] M 644 inline testbzr data 6451 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): logfile.write('$ %s\n' % cmd) cmd = cmd.split() else: logfile.write('$ %r\n' % cmd) return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open(LOGFILENAME, 'wt', buffering=1) try: runcmd(['mkdir', TESTDIR]) cd(TESTDIR) progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == '''? test.txt\n''' out = backtick("bzr status --all") assert out == "? test.txt\n" progress("command aliases") out = backtick("bzr st --all") assert out == "? test.txt\n" out = backtick("bzr stat") assert out == "? test.txt\n" progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == "A test.txt\n" progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') cd('..') progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :353 committer Martin Pool 1115264074 +1000 data 67 - Per-branch locks in read and write modes. (Not on Windows yet.) from :352 M 644 inline NEWS data 5029 bzr-0.0.5 NOT RELEASED YET ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New 'bzr ignore PATTERN' command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on mango-sorbet by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/branch.py data 36294 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. base Base directory of the branch. """ _lockmode = None def __init__(self, base, init=False, find_root=True, lock_mode='w'): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.lock(lock_mode) self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def lock(self, mode='w'): """Lock the on-disk branch, excluding other processes.""" try: import fcntl if mode == 'w': lm = fcntl.LOCK_EX om = os.O_WRONLY | os.O_CREAT elif mode == 'r': lm = fcntl.LOCK_SH om = os.O_RDONLY else: raise BzrError("invalid locking mode %r" % mode) lockfile = os.open(self.controlfilename('branch-lock'), om) fcntl.lockf(lockfile, lm) def unlock(self): fcntl.lockf(lockfile, fcntl.LOCK_UN) os.close(lockfile) self._lockmode = None self.unlock = unlock self._lockmode = mode except ImportError: warning("please write a locking method for platform %r" % sys.platform) def unlock(self): self._lockmode = None self.unlock = unlock self._lockmode = mode def _need_readlock(self): if self._lockmode not in ['r', 'w']: raise BzrError('need read lock on branch, only have %r' % self._lockmode) def _need_writelock(self): if self._lockmode not in ['w']: raise BzrError('need write lock on branch, only have %r' % self._lockmode) def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" self._need_readlock() before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self._need_writelock() ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ self._need_writelock() # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" self._need_readlock() tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability self._need_writelock() if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. """ self._need_writelock() ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) self.append_revision(rev_id) if verbose: note("commited r%d" % self.revno()) def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" self._need_readlock() r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" self._need_readlock() i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" self._need_readlock() if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self._need_readlock() return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] else: return None def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" self._need_readlock() if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original', verbose=False): """Write out human-readable log of commits to this branch utc -- If true, show dates in universal time, not local time.""" self._need_readlock() ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l if verbose == True and precursor != None: print 'changed files:' tree = self.revision_tree(p) prevtree = self.revision_tree(precursor) for file_state, fid, old_name, new_name, kind in \ diff_trees(prevtree, tree, ): if file_state == 'A' or file_state == 'M': show_status(file_state, kind, new_name) elif file_state == 'D': show_status(file_state, kind, old_name) elif file_state == 'R': show_status(file_state, kind, old_name + ' => ' + new_name) revno += 1 precursor = p def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self._need_writelock() tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self._need_writelock() ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(self, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo TODO: Get state for single files. """ self._need_readlock() # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = self.basis_tree() new = self.working_tree() for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("weird file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" try: shutil.rmtree(self.base) except OSError: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/commands.py data 26873 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass else: raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False): #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) def Relpath(Command): """Show path of a file relative to root""" takes_args = ('filename') def run(self): print Branch(self.args['filename']).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) class cmd_log(Command): """Show log of this branch. TODO: Options to show ids; to limit range; etc. """ takes_options = ['timezone', 'verbose'] def run(self, timezone='original', verbose=False): Branch('.', lock_mode='r').write_log(show_timezone=timezone, verbose=verbose) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern""" takes_args = ['name_pattern'] def run(self, name_pattern): b = Branch('.') # XXX: This will fail if it's a hardlink; should use an AtomicFile class. f = open(b.abspath('.bzrignore'), 'at') f.write(name_pattern + '\n') f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them.""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revno=None): b = Branch('.') if revno == None: rh = b.revision_history[-1] else: rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, verbose=False): if not message: raise BzrCommandError("please specify a commit message") Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option %r is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(e, summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception(e) if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception(e, 'error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(e, msg) return 2 except KeyboardInterrupt, e: _report_exception(e, 'interrupted', quiet=True) return 2 except Exception, e: quiet = False if isinstance(e, IOError) and e.errno == errno.EPIPE: quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(e, msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :354 committer Martin Pool 1115271295 +1000 data 3 doc from :353 M 644 inline bzrlib/branch.py data 36474 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. base Base directory of the branch. """ _lockmode = None def __init__(self, base, init=False, find_root=True, lock_mode='w'): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.lock(lock_mode) self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def lock(self, mode='w'): """Lock the on-disk branch, excluding other processes.""" try: import fcntl if mode == 'w': lm = fcntl.LOCK_EX om = os.O_WRONLY | os.O_CREAT elif mode == 'r': lm = fcntl.LOCK_SH om = os.O_RDONLY else: raise BzrError("invalid locking mode %r" % mode) # XXX: Old branches might not have the lock file, and # won't get one until someone does a write-mode command on # them or creates it by hand. lockfile = os.open(self.controlfilename('branch-lock'), om) fcntl.lockf(lockfile, lm) def unlock(self): fcntl.lockf(lockfile, fcntl.LOCK_UN) os.close(lockfile) self._lockmode = None self.unlock = unlock self._lockmode = mode except ImportError: warning("please write a locking method for platform %r" % sys.platform) def unlock(self): self._lockmode = None self.unlock = unlock self._lockmode = mode def _need_readlock(self): if self._lockmode not in ['r', 'w']: raise BzrError('need read lock on branch, only have %r' % self._lockmode) def _need_writelock(self): if self._lockmode not in ['w']: raise BzrError('need write lock on branch, only have %r' % self._lockmode) def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" self._need_readlock() before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self._need_writelock() ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ self._need_writelock() # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" self._need_readlock() tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability self._need_writelock() if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. """ self._need_writelock() ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) self.append_revision(rev_id) if verbose: note("commited r%d" % self.revno()) def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" self._need_readlock() r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" self._need_readlock() i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" self._need_readlock() if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self._need_readlock() return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] else: return None def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" self._need_readlock() if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original', verbose=False): """Write out human-readable log of commits to this branch utc -- If true, show dates in universal time, not local time.""" self._need_readlock() ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l if verbose == True and precursor != None: print 'changed files:' tree = self.revision_tree(p) prevtree = self.revision_tree(precursor) for file_state, fid, old_name, new_name, kind in \ diff_trees(prevtree, tree, ): if file_state == 'A' or file_state == 'M': show_status(file_state, kind, new_name) elif file_state == 'D': show_status(file_state, kind, old_name) elif file_state == 'R': show_status(file_state, kind, old_name + ' => ' + new_name) revno += 1 precursor = p def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self._need_writelock() tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self._need_writelock() ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(self, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo TODO: Get state for single files. """ self._need_readlock() # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = self.basis_tree() new = self.working_tree() for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("weird file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" try: shutil.rmtree(self.base) except OSError: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :355 committer Martin Pool 1115271445 +1000 data 26 - remove obsolete quickref from :354 D doc/quickref.txt M 644 inline doc/index.txt data 5512 Bazaar-NG ********* .. These documents are formatted as ReStructuredText. You can .. .. convert them to HTML, PDF, etc using the ``python-docutils`` .. .. package. .. *Bazaar-NG* (``bzr``) is a project of `Canonical Ltd`__ to develop an open source distributed version control system that is powerful, friendly, and scalable. The project is at an early stage of development. __ http://canonical.com/ **Note:** These documents are in a very preliminary state, and so may be internally or externally inconsistent or redundant. Comments are still very welcome. Please send them to . For more information, see the homepage at http://bazaar-ng.org/ User documentation ------------------ * `Project overview/introduction `__ * `Command reference `__ -- intended to be user documentation, and gives the best overview at the moment of what the system will feel like to use. Fairly complete. * `FAQ `__ -- mostly user-oriented FAQ. Requirements and general design ------------------------------- * `Various purposes of a VCS `__ -- taking snapshots and helping with merges is not the whole story. * `Requirements `__ * `Costs `__ of various factors: time, disk, network, etc. * `Deadly sins `__ that gcc maintainers suggest we avoid. * `Overview of the whole design `__ and miscellaneous small design points. * `File formats `__ * `Random observations `__ that don't fit anywhere else yet. Design of particular features ----------------------------- * `Automatic generation of ChangeLogs `__ * `Cherry picking `__ -- merge just selected non-contiguous changes from a branch. * `Common changeset format `__ for interchange format between VCS. * `Compression `__ of file text for more efficient storage. * `Config specs `__ assemble a tree from several places. * `Conflicts `_ that can occur during merge-like operations. * `Ignored files `__ * `Recovering from interrupted operations `__ * `Inventory command `__ * `Branch joins `__ represent that all the changes from one branch are integrated into another. * `Kill a version `__ to fix a broken commit or wrong message, or to remove confidential information from the history. * `Hash collisions `__ and weaknesses, and the security implications thereof. * `Layers `__ within the design * `Library interface `__ for Python. * `Merge `__ * `Mirroring `__ * `Optional edit command `__: sometimes people want to make the working copy read-only, or not present at all. * `Partial commits `__ * `Patch pools `__ to efficiently store related branches. * `Revfiles `__ store the text history of files. * `Revision syntax `__ -- ``hello.c@12``, etc. * `Roll-up commits `__ -- a single revision incorporates the changes from several others. * `Scalability `__ * `Security `__ * `Shared branches `__ maintained by more than one person * `Supportability `__ -- how to handle any bugs or problems in the field. * `Place tags on revisions for easy reference `__ * `Detecting unchanged files `__ * `Merging previously-unrelated branches `__ * `Usability principles `__ (very small at the moment) * ``__ * ``__ * ``__ Modelling/controlling flow of patches. * ``__ -- Discussion of using YAML_ as a storage or transmission format. .. _YAML: http://www.yaml.org/ Comparisons to other systems ---------------------------- * `Taxonomy `__: basic questions a VCS must answer. * `Bitkeeper `__, the proprietary system used by some kernel developers. * `Aegis `__, a tool focussed on enforcing process and workflow. * `Codeville `__ has an intruiging but scarcely-documented merge algorithm. * `CVSNT `__, with more Windows support and some merge enhancements. * `OpenCM `__, another hash-based tool with a good whitepaper. * `PRCS `__, a non-distributed inventory-based tool. * `GNU Arch `__, with many pros and cons. * `Darcs `__, a merge-focussed tool with good usability. * `Quilt `__ -- Andrew Morton's patch scripts, popular with kernel maintainers. * `Monotone `__, Graydon Hoare's hash-based distributed system. * `SVK `__ -- distributed operation stacked on Subversion. * `Sun Teamware `__ Project management and organization ----------------------------------- * `Notes on how to get a VCS adopted `__ * `Thanks `__ to various people * `Extra commands `__ for internal/developer/debugger use. * `Choice of Python as a development language `__ commit refs/heads/master mark :356 committer Martin Pool 1115271705 +1000 data 32 - pychecker fixes in bzrlib.diff from :355 M 644 inline bzrlib/diff.py data 9332 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set from trace import mutter from errors import BzrError def diff_trees(old_tree, new_tree): """Compute diff between two trees. They may be in different branches and may be working or historical trees. Yields a sequence of (state, id, old_name, new_name, kind). Each filename and each id is listed only once. """ ## TODO: Compare files before diffing; only mention those that have changed ## TODO: Set nice names in the headers, maybe include diffstat ## TODO: Perhaps make this a generator rather than using ## a callback object? ## TODO: Allow specifying a list of files to compare, rather than ## doing the whole tree? (Not urgent.) ## TODO: Allow diffing any two inventories, not just the ## current one against one. We mgiht need to specify two ## stores to look for the files if diffing two branches. That ## might imply this shouldn't be primarily a Branch method. ## XXX: This doesn't report on unknown files; that can be done ## from a separate method. old_it = old_tree.list_files() new_it = new_tree.list_files() def next(it): try: return it.next() except StopIteration: return None old_item = next(old_it) new_item = next(new_it) # We step through the two sorted iterators in parallel, trying to # keep them lined up. while (old_item != None) or (new_item != None): # OK, we still have some remaining on both, but they may be # out of step. if old_item != None: old_name, old_class, old_kind, old_id = old_item else: old_name = None if new_item != None: new_name, new_class, new_kind, new_id = new_item else: new_name = None mutter(" diff pairwise %r" % (old_item,)) mutter(" %r" % (new_item,)) if old_item: # can't handle the old tree being a WorkingTree assert old_class == 'V' if new_item and (new_class != 'V'): yield new_class, None, None, new_name, new_kind new_item = next(new_it) elif (not new_item) or (old_item and (old_name < new_name)): mutter(" extra entry in old-tree sequence") if new_tree.has_id(old_id): # will be mentioned as renamed under new name pass else: yield 'D', old_id, old_name, None, old_kind old_item = next(old_it) elif (not old_item) or (new_item and (new_name < old_name)): mutter(" extra entry in new-tree sequence") if old_tree.has_id(new_id): yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind else: yield 'A', new_id, None, new_name, new_kind new_item = next(new_it) elif old_id != new_id: assert old_name == new_name # both trees have a file of this name, but it is not the # same file. in other words, the old filename has been # overwritten by either a newly-added or a renamed file. # (should we return something about the overwritten file?) if old_tree.has_id(new_id): # renaming, overlying a deleted file yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind else: yield 'A', new_id, None, new_name, new_kind new_item = next(new_it) old_item = next(old_it) else: assert old_id == new_id assert old_id != None assert old_name == new_name assert old_kind == new_kind if old_kind == 'directory': yield '.', new_id, old_name, new_name, new_kind elif old_tree.get_file_size(old_id) != new_tree.get_file_size(old_id): mutter(" file size has changed, must be different") yield 'M', new_id, old_name, new_name, new_kind elif old_tree.get_file_sha1(old_id) == new_tree.get_file_sha1(old_id): mutter(" SHA1 indicates they're identical") ## assert compare_files(old_tree.get_file(i), new_tree.get_file(i)) yield '.', new_id, old_name, new_name, new_kind else: mutter(" quick compare shows different") yield 'M', new_id, old_name, new_name, new_kind new_item = next(new_it) old_item = next(old_it) def show_diff(b, revision, file_list): import difflib, sys, types if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. if file_list: file_list = [b.relpath(f) for f in file_list] # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in diff_trees(old_tree, new_tree): if file_list and (new_name not in file_list): continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) sys.stdout.writelines(ud) if nonl: print "\\ No newline at end of file" sys.stdout.write('\n') if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: raise BzrError("can't represent state %s {%s}" % (file_state, fid)) commit refs/heads/master mark :357 committer Martin Pool 1115272014 +1000 data 21 - remove obsolete faq from :356 D doc/faq.txt M 644 inline doc/index.txt data 5461 Bazaar-NG ********* .. These documents are formatted as ReStructuredText. You can .. .. convert them to HTML, PDF, etc using the ``python-docutils`` .. .. package. .. *Bazaar-NG* (``bzr``) is a project of `Canonical Ltd`__ to develop an open source distributed version control system that is powerful, friendly, and scalable. The project is at an early stage of development. __ http://canonical.com/ **Note:** These documents are in a very preliminary state, and so may be internally or externally inconsistent or redundant. Comments are still very welcome. Please send them to . For more information, see the homepage at http://bazaar-ng.org/ User documentation ------------------ * `Project overview/introduction `__ * `Command reference `__ -- intended to be user documentation, and gives the best overview at the moment of what the system will feel like to use. Fairly complete. Requirements and general design ------------------------------- * `Various purposes of a VCS `__ -- taking snapshots and helping with merges is not the whole story. * `Requirements `__ * `Costs `__ of various factors: time, disk, network, etc. * `Deadly sins `__ that gcc maintainers suggest we avoid. * `Overview of the whole design `__ and miscellaneous small design points. * `File formats `__ * `Random observations `__ that don't fit anywhere else yet. Design of particular features ----------------------------- * `Automatic generation of ChangeLogs `__ * `Cherry picking `__ -- merge just selected non-contiguous changes from a branch. * `Common changeset format `__ for interchange format between VCS. * `Compression `__ of file text for more efficient storage. * `Config specs `__ assemble a tree from several places. * `Conflicts `_ that can occur during merge-like operations. * `Ignored files `__ * `Recovering from interrupted operations `__ * `Inventory command `__ * `Branch joins `__ represent that all the changes from one branch are integrated into another. * `Kill a version `__ to fix a broken commit or wrong message, or to remove confidential information from the history. * `Hash collisions `__ and weaknesses, and the security implications thereof. * `Layers `__ within the design * `Library interface `__ for Python. * `Merge `__ * `Mirroring `__ * `Optional edit command `__: sometimes people want to make the working copy read-only, or not present at all. * `Partial commits `__ * `Patch pools `__ to efficiently store related branches. * `Revfiles `__ store the text history of files. * `Revision syntax `__ -- ``hello.c@12``, etc. * `Roll-up commits `__ -- a single revision incorporates the changes from several others. * `Scalability `__ * `Security `__ * `Shared branches `__ maintained by more than one person * `Supportability `__ -- how to handle any bugs or problems in the field. * `Place tags on revisions for easy reference `__ * `Detecting unchanged files `__ * `Merging previously-unrelated branches `__ * `Usability principles `__ (very small at the moment) * ``__ * ``__ * ``__ Modelling/controlling flow of patches. * ``__ -- Discussion of using YAML_ as a storage or transmission format. .. _YAML: http://www.yaml.org/ Comparisons to other systems ---------------------------- * `Taxonomy `__: basic questions a VCS must answer. * `Bitkeeper `__, the proprietary system used by some kernel developers. * `Aegis `__, a tool focussed on enforcing process and workflow. * `Codeville `__ has an intruiging but scarcely-documented merge algorithm. * `CVSNT `__, with more Windows support and some merge enhancements. * `OpenCM `__, another hash-based tool with a good whitepaper. * `PRCS `__, a non-distributed inventory-based tool. * `GNU Arch `__, with many pros and cons. * `Darcs `__, a merge-focussed tool with good usability. * `Quilt `__ -- Andrew Morton's patch scripts, popular with kernel maintainers. * `Monotone `__, Graydon Hoare's hash-based distributed system. * `SVK `__ -- distributed operation stacked on Subversion. * `Sun Teamware `__ Project management and organization ----------------------------------- * `Notes on how to get a VCS adopted `__ * `Thanks `__ to various people * `Extra commands `__ for internal/developer/debugger use. * `Choice of Python as a development language `__ commit refs/heads/master mark :358 committer Martin Pool 1115273394 +1000 data 21 - Fix Branch.unlock() from :357 M 644 inline bzrlib/branch.py data 36466 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. base Base directory of the branch. """ _lockmode = None def __init__(self, base, init=False, find_root=True, lock_mode='w'): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.lock(lock_mode) self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def lock(self, mode='w'): """Lock the on-disk branch, excluding other processes.""" try: import fcntl if mode == 'w': lm = fcntl.LOCK_EX om = os.O_WRONLY | os.O_CREAT elif mode == 'r': lm = fcntl.LOCK_SH om = os.O_RDONLY else: raise BzrError("invalid locking mode %r" % mode) # XXX: Old branches might not have the lock file, and # won't get one until someone does a write-mode command on # them or creates it by hand. lockfile = os.open(self.controlfilename('branch-lock'), om) fcntl.lockf(lockfile, lm) def unlock(): fcntl.lockf(lockfile, fcntl.LOCK_UN) os.close(lockfile) self._lockmode = None self.unlock = unlock self._lockmode = mode except ImportError: warning("please write a locking method for platform %r" % sys.platform) def unlock(): self._lockmode = None self.unlock = unlock self._lockmode = mode def _need_readlock(self): if self._lockmode not in ['r', 'w']: raise BzrError('need read lock on branch, only have %r' % self._lockmode) def _need_writelock(self): if self._lockmode not in ['w']: raise BzrError('need write lock on branch, only have %r' % self._lockmode) def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" self._need_readlock() before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self._need_writelock() ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ self._need_writelock() # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" self._need_readlock() tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability self._need_writelock() if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. """ self._need_writelock() ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) self.append_revision(rev_id) if verbose: note("commited r%d" % self.revno()) def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" self._need_readlock() r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" self._need_readlock() i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" self._need_readlock() if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self._need_readlock() return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] else: return None def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" self._need_readlock() if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original', verbose=False): """Write out human-readable log of commits to this branch utc -- If true, show dates in universal time, not local time.""" self._need_readlock() ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l if verbose == True and precursor != None: print 'changed files:' tree = self.revision_tree(p) prevtree = self.revision_tree(precursor) for file_state, fid, old_name, new_name, kind in \ diff_trees(prevtree, tree, ): if file_state == 'A' or file_state == 'M': show_status(file_state, kind, new_name) elif file_state == 'D': show_status(file_state, kind, old_name) elif file_state == 'R': show_status(file_state, kind, old_name + ' => ' + new_name) revno += 1 precursor = p def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self._need_writelock() tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self._need_writelock() ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(self, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo TODO: Get state for single files. """ self._need_readlock() # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = self.basis_tree() new = self.working_tree() for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("weird file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" try: shutil.rmtree(self.base) except OSError: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :359 committer Martin Pool 1115273693 +1000 data 18 - pychecker fixups from :358 M 644 inline bzrlib/commands.py data 26857 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass else: raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False): #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) def Relpath(Command): """Show path of a file relative to root""" takes_args = ('filename') def run(self): print Branch(self.args['filename']).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) class cmd_log(Command): """Show log of this branch. TODO: Options to show ids; to limit range; etc. """ takes_options = ['timezone', 'verbose'] def run(self, timezone='original', verbose=False): Branch('.', lock_mode='r').write_log(show_timezone=timezone, verbose=verbose) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern""" takes_args = ['name_pattern'] def run(self, name_pattern): b = Branch('.') # XXX: This will fail if it's a hardlink; should use an AtomicFile class. f = open(b.abspath('.bzrignore'), 'at') f.write(name_pattern + '\n') f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them.""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revno=None): b = Branch('.') if revno == None: rh = b.revision_history[-1] else: rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, verbose=False): if not message: raise BzrCommandError("please specify a commit message") Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option %r is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if isinstance(e, IOError) and e.errno == errno.EPIPE: quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/trace.py data 3976 # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " """Messages and logging for bazaar-ng Nothing is actually logged unless you call bzrlib.open_tracefile(). """ import sys, os import bzrlib ###################################################################### # messages and logging global _tracefile, _starttime _tracefile = None # used to have % (os.environ['USER'], time.time(), os.getpid()), 'w') _starttime = None # If false, notes also go to stdout; should replace this with --silent # at some point. silent = False # fix this if we ever fork within python _mypid = os.getpid() _logprefix = '[%d] ' % _mypid def _write_trace(msg): if _tracefile: _tracefile.write(_logprefix + msg + '\n') def warning(msg): sys.stderr.write('bzr: warning: ' + msg + '\n') _write_trace('warning: ' + msg) mutter = _write_trace def note(msg): b = '* ' + str(msg) + '\n' if not silent: sys.stderr.write(b) _write_trace('note: ' + msg) def log_error(msg): sys.stderr.write(msg + '\n') _write_trace(msg) def _rollover_trace_maybe(trace_fname): import stat try: size = os.stat(trace_fname)[stat.ST_SIZE] if size <= 4 << 20: return old_fname = trace_fname + '.old' try: # must remove before rename on windows os.remove(old_fname) except OSError: pass try: # might fail if in use on windows os.rename(trace_fname, old_fname) except OSError: pass except OSError: return def open_tracefile(argv): # Messages are always written to here, so that we have some # information if something goes wrong. In a future version this # file will be removed on successful completion. global _starttime, _tracefile import stat, codecs _starttime = os.times()[4] trace_fname = os.path.join(os.path.expanduser('~/.bzr.log')) _rollover_trace_maybe(trace_fname) # buffering=1 means line buffered _tracefile = codecs.open(trace_fname, 'at', 'utf8', buffering=1) t = _tracefile if os.fstat(t.fileno())[stat.ST_SIZE] == 0: t.write("\nthis is a debug log for diagnosing/reporting problems in bzr\n") t.write("you can delete or truncate this file, or include sections in\n") t.write("bug reports to bazaar-ng@lists.canonical.com\n\n") # TODO: If we failed to create the file, perhaps give a warning # but don't abort; send things to /dev/null instead? _write_trace('bzr %s invoked on python %s (%s)' % (bzrlib.__version__, '.'.join(map(str, sys.version_info)), sys.platform)) _write_trace(' arguments: %r' % argv) _write_trace(' working dir: ' + os.getcwdu()) def close_trace(): times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum, %.3f elapsed" % (times[:4] + ((times[4] - _starttime),))) def log_exception(): """Log the last exception into the trace file.""" import traceback, cStringIO s = cStringIO.StringIO() traceback.print_exc(None, s) for l in s.getvalue().split('\n'): _write_trace(l) commit refs/heads/master mark :360 committer Martin Pool 1115274082 +1000 data 63 - use cgitb to get more detailed traceback in the trace file from :359 M 644 inline bzrlib/trace.py data 3923 # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " """Messages and logging for bazaar-ng Nothing is actually logged unless you call bzrlib.open_tracefile(). """ import sys, os import bzrlib ###################################################################### # messages and logging global _tracefile, _starttime _tracefile = None # used to have % (os.environ['USER'], time.time(), os.getpid()), 'w') _starttime = None # If false, notes also go to stdout; should replace this with --silent # at some point. silent = False # fix this if we ever fork within python _mypid = os.getpid() _logprefix = '[%d] ' % _mypid def _write_trace(msg): if _tracefile: _tracefile.write(_logprefix + msg + '\n') def warning(msg): sys.stderr.write('bzr: warning: ' + msg + '\n') _write_trace('warning: ' + msg) mutter = _write_trace def note(msg): b = '* ' + str(msg) + '\n' if not silent: sys.stderr.write(b) _write_trace('note: ' + msg) def log_error(msg): sys.stderr.write(msg + '\n') _write_trace(msg) def _rollover_trace_maybe(trace_fname): import stat try: size = os.stat(trace_fname)[stat.ST_SIZE] if size <= 4 << 20: return old_fname = trace_fname + '.old' try: # must remove before rename on windows os.remove(old_fname) except OSError: pass try: # might fail if in use on windows os.rename(trace_fname, old_fname) except OSError: pass except OSError: return def open_tracefile(argv): # Messages are always written to here, so that we have some # information if something goes wrong. In a future version this # file will be removed on successful completion. global _starttime, _tracefile import stat, codecs _starttime = os.times()[4] trace_fname = os.path.join(os.path.expanduser('~/.bzr.log')) _rollover_trace_maybe(trace_fname) # buffering=1 means line buffered _tracefile = codecs.open(trace_fname, 'at', 'utf8', buffering=1) t = _tracefile if os.fstat(t.fileno())[stat.ST_SIZE] == 0: t.write("\nthis is a debug log for diagnosing/reporting problems in bzr\n") t.write("you can delete or truncate this file, or include sections in\n") t.write("bug reports to bazaar-ng@lists.canonical.com\n\n") # TODO: If we failed to create the file, perhaps give a warning # but don't abort; send things to /dev/null instead? _write_trace('bzr %s invoked on python %s (%s)' % (bzrlib.__version__, '.'.join(map(str, sys.version_info)), sys.platform)) _write_trace(' arguments: %r' % argv) _write_trace(' working dir: ' + os.getcwdu()) def close_trace(): times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum, %.3f elapsed" % (times[:4] + ((times[4] - _starttime),))) def log_exception(): """Log the last exception into the trace file.""" import cgitb s = cgitb.text(sys.exc_info()) for l in s.split('\n'): _write_trace(l) commit refs/heads/master mark :361 committer Martin Pool 1115274158 +1000 data 4 todo from :360 M 644 inline TODO data 7999 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be ``bzr export -r REV``. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * ``bzr help commands`` should give a one-line summary of each command. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Read and write locks on branch while it's open. * Separate read and write version checks? * ``bzr status FILE...`` * Check all commands have decent help. * Autogenerate argument/option help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. * Split BzrError into various more specific subclasses for different errors people might want to catch. Medium things ------------- * Display command grammar in help messages rather than hardcoding it. * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :362 committer Martin Pool 1115274260 +1000 data 24 - Import stat-cache code from :361 M 644 inline bzrlib/cache.py data 4539 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import stat, os, sha, time from binascii import b2a_qp, a2b_qp from trace import mutter # file fingerprints are: (path, size, mtime, ctime, ino, dev). # # if this is the same for this file as in the previous revision, we # assume the content is the same and the SHA-1 is the same. # This is stored in a fingerprint file that also contains the file-id # and the content SHA-1. # Thus for any given file we can quickly get the SHA-1, either from # the cache or if the cache is out of date. # At the moment this is stored in a simple textfile; it might be nice # to use a tdb instead. # What we need: # build a new cache from scratch # load cache, incrementally update it # TODO: Have a paranoid mode where we always compare the texts and # always recalculate the digest, to trap modification without stat # change and SHA collisions. def fingerprint(path, abspath): try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) def write_cache(branch, entry_iter): outf = branch.controlfile('work-cache.tmp', 'wt') for entry in entry_iter: outf.write(entry[0] + ' ' + entry[1] + ' ') outf.write(b2a_qp(entry[2], True)) outf.write(' %d %d %d %d %d\n' % entry[3:]) outf.close() os.rename(branch.controlfilename('work-cache.tmp'), branch.controlfilename('work-cache')) def load_cache(branch): cache = {} try: cachefile = branch.controlfile('work-cache', 'rt') except IOError: return cache for l in cachefile: f = l.split(' ') file_id = f[0] if file_id in cache: raise BzrError("duplicated file_id in cache: {%s}" % file_id) cache[file_id] = (f[0], f[1], a2b_qp(f[2])) + tuple([long(x) for x in f[3:]]) return cache def _files_from_inventory(inv): for path, ie in inv.iter_entries(): if ie.kind != 'file': continue yield ie.file_id, path def build_cache(branch): inv = branch.read_working_inventory() cache = {} _update_cache_from_list(branch, cache, _files_from_inventory(inv)) def update_cache(branch, inv): # TODO: It's supposed to be faster to stat the files in order by inum. # We don't directly know the inum of the files of course but we do # know where they were last sighted, so we can sort by that. cache = load_cache(branch) return _update_cache_from_list(branch, cache, _files_from_inventory(inv)) def _update_cache_from_list(branch, cache, to_update): """Update the cache to have info on the named files. to_update is a sequence of (file_id, path) pairs. """ hardcheck = dirty = 0 for file_id, path in to_update: fap = branch.abspath(path) fp = fingerprint(fap, path) cacheentry = cache.get(file_id) if fp == None: # not here if cacheentry: del cache[file_id] dirty += 1 continue if cacheentry and (cacheentry[3:] == fp): continue # all stat fields unchanged hardcheck += 1 dig = sha.new(file(fap, 'rb').read()).hexdigest() if cacheentry == None or dig != cacheentry[1]: # if there was no previous entry for this file, or if the # SHA has changed, then update the cache cacheentry = (file_id, dig, path) + fp cache[file_id] = cacheentry dirty += 1 mutter('work cache: read %d files, %d changed' % (hardcheck, dirty)) if dirty: write_cache(branch, cache.itervalues()) return cache M 644 inline bzrlib/status.py data 1654 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def find_modified(branch): """Return a list of files that have been modified in the working copy. This does not consider renames and does not include files added or deleted. Each modified file is returned as (PATH, ENTRY). """ import cache inv = branch.read_working_inventory() cc = cache.update_cache(branch, inv) basis_inv = branch.basis_tree().inventory for path, entry in inv.iter_entries(): if entry.kind != 'file': continue file_id = entry.file_id ce = cc.get(file_id) if not ce: continue # not in working dir if file_id not in basis_inv: continue # newly added old_entry = basis_inv[file_id] if (old_entry.text_size == ce[3] and old_entry.text_sha1 == ce[1]): continue yield path, entry commit refs/heads/master mark :363 committer Martin Pool 1115275098 +1000 data 67 - do upload CHANGELOG to web server, even though it's autogenerated from :362 M 644 inline .rsyncexclude data 152 *.pyc *.pyo *~ # arch can bite me {arch} .arch-ids ,,* ++* /doc/*.html *.tmp bzr-test.log [#]*# .#* testrev.* /tmp # do want this after all + CHANGELOG M 644 inline bzrlib/__init__.py data 1706 # (C) 2005 Canonical Development Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr library""" from inventory import Inventory, InventoryEntry from branch import Branch, ScratchBranch from osutils import format_date from tree import Tree from diff import diff_trees from trace import mutter, warning, open_tracefile import add BZRDIR = ".bzr" DEFAULT_IGNORE = ['.bzr.log', '*~', '#*#', '*$', '.#*', '.*.swp', '.*.tmp', '*.tmp', '*.bak', '*.BAK', '*.orig', '*.o', '*.obj', '*.a', '*.py[oc]', '*.so', '*.exe', '*.elc', '{arch}', 'CVS', 'CVS.adm', '.svn', '_darcs', 'SCCS', 'RCS', '*,v', 'BitKeeper', 'TAGS', '.make.state', '.sconsign', '.tmp*', '.del-*'] IGNORE_FILENAME = ".bzrignore" import locale user_encoding = locale.getpreferredencoding() __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __version__ = '0.0.5pre' commit refs/heads/master mark :364 committer Martin Pool 1115275384 +1000 data 54 - Create lockfiles in old branches that don't have one from :363 M 644 inline bzrlib/branch.py data 36659 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. base Base directory of the branch. """ _lockmode = None def __init__(self, base, init=False, find_root=True, lock_mode='w'): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.lock(lock_mode) self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def lock(self, mode='w'): """Lock the on-disk branch, excluding other processes.""" try: import fcntl, errno if mode == 'w': lm = fcntl.LOCK_EX om = os.O_WRONLY | os.O_CREAT elif mode == 'r': lm = fcntl.LOCK_SH om = os.O_RDONLY else: raise BzrError("invalid locking mode %r" % mode) try: lockfile = os.open(self.controlfilename('branch-lock'), om) except OSError, e: if e.errno == errno.ENOENT: # might not exist on branches from <0.0.4 self.controlfile('branch-lock', 'w').close() lockfile = os.open(self.controlfilename('branch-lock'), om) else: raise e fcntl.lockf(lockfile, lm) def unlock(): fcntl.lockf(lockfile, fcntl.LOCK_UN) os.close(lockfile) self._lockmode = None self.unlock = unlock self._lockmode = mode except ImportError: warning("please write a locking method for platform %r" % sys.platform) def unlock(): self._lockmode = None self.unlock = unlock self._lockmode = mode def _need_readlock(self): if self._lockmode not in ['r', 'w']: raise BzrError('need read lock on branch, only have %r' % self._lockmode) def _need_writelock(self): if self._lockmode not in ['w']: raise BzrError('need write lock on branch, only have %r' % self._lockmode) def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" self._need_readlock() before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self._need_writelock() ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ self._need_writelock() # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" self._need_readlock() tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability self._need_writelock() if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. """ self._need_writelock() ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) self.append_revision(rev_id) if verbose: note("commited r%d" % self.revno()) def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" self._need_readlock() r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" self._need_readlock() i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" self._need_readlock() if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self._need_readlock() return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] else: return None def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" self._need_readlock() if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original', verbose=False): """Write out human-readable log of commits to this branch utc -- If true, show dates in universal time, not local time.""" self._need_readlock() ## TODO: Option to choose either original, utc or local timezone revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l if verbose == True and precursor != None: print 'changed files:' tree = self.revision_tree(p) prevtree = self.revision_tree(precursor) for file_state, fid, old_name, new_name, kind in \ diff_trees(prevtree, tree, ): if file_state == 'A' or file_state == 'M': show_status(file_state, kind, new_name) elif file_state == 'D': show_status(file_state, kind, old_name) elif file_state == 'R': show_status(file_state, kind, old_name + ' => ' + new_name) revno += 1 precursor = p def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self._need_writelock() tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self._need_writelock() ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(self, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo TODO: Get state for single files. """ self._need_readlock() # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = self.basis_tree() new = self.working_tree() for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("weird file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" try: shutil.rmtree(self.base) except OSError: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :365 committer Martin Pool 1115275522 +1000 data 3 doc from :364 M 644 inline bzrlib/branch.py data 36767 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. base Base directory of the branch. """ _lockmode = None def __init__(self, base, init=False, find_root=True, lock_mode='w'): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.lock(lock_mode) self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def lock(self, mode='w'): """Lock the on-disk branch, excluding other processes.""" try: import fcntl, errno if mode == 'w': lm = fcntl.LOCK_EX om = os.O_WRONLY | os.O_CREAT elif mode == 'r': lm = fcntl.LOCK_SH om = os.O_RDONLY else: raise BzrError("invalid locking mode %r" % mode) try: lockfile = os.open(self.controlfilename('branch-lock'), om) except OSError, e: if e.errno == errno.ENOENT: # might not exist on branches from <0.0.4 self.controlfile('branch-lock', 'w').close() lockfile = os.open(self.controlfilename('branch-lock'), om) else: raise e fcntl.lockf(lockfile, lm) def unlock(): fcntl.lockf(lockfile, fcntl.LOCK_UN) os.close(lockfile) self._lockmode = None self.unlock = unlock self._lockmode = mode except ImportError: warning("please write a locking method for platform %r" % sys.platform) def unlock(): self._lockmode = None self.unlock = unlock self._lockmode = mode def _need_readlock(self): if self._lockmode not in ['r', 'w']: raise BzrError('need read lock on branch, only have %r' % self._lockmode) def _need_writelock(self): if self._lockmode not in ['w']: raise BzrError('need write lock on branch, only have %r' % self._lockmode) def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" self._need_readlock() before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self._need_writelock() ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ self._need_writelock() # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" self._need_readlock() tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability self._need_writelock() if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. """ self._need_writelock() ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) self.append_revision(rev_id) if verbose: note("commited r%d" % self.revno()) def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" self._need_readlock() r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" self._need_readlock() i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" self._need_readlock() if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self._need_readlock() return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] else: return None def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" self._need_readlock() if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original', verbose=False): """Write out human-readable log of commits to this branch. show_timezone -- may be 'original' (committer's timezone), 'utc' (universal time), or 'local' (local user's timezone) verbose -- if true, also list which files were changed in each revision. """ self._need_readlock() revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno ## TODO: Show hash if --id is given. ##print 'revision-hash:', p rev = self.get_revision(p) print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l if verbose == True and precursor != None: print 'changed files:' tree = self.revision_tree(p) prevtree = self.revision_tree(precursor) for file_state, fid, old_name, new_name, kind in \ diff_trees(prevtree, tree, ): if file_state == 'A' or file_state == 'M': show_status(file_state, kind, new_name) elif file_state == 'D': show_status(file_state, kind, old_name) elif file_state == 'R': show_status(file_state, kind, old_name + ' => ' + new_name) revno += 1 precursor = p def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self._need_writelock() tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self._need_writelock() ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(self, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo TODO: Get state for single files. """ self._need_readlock() # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = self.basis_tree() new = self.working_tree() for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("weird file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" try: shutil.rmtree(self.base) except OSError: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :366 committer Martin Pool 1115275559 +1000 data 4 todo from :365 M 644 inline TODO data 8050 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be ``bzr export -r REV``. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * ``bzr help commands`` should give a one-line summary of each command. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Read and write locks on branch while it's open. * Separate read and write version checks? * ``bzr status FILE...`` * Check all commands have decent help. * Autogenerate argument/option help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. * Split BzrError into various more specific subclasses for different errors people might want to catch. Medium things ------------- * Display command grammar in help messages rather than hardcoding it. * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :367 committer Martin Pool 1115275861 +1000 data 35 - New --show-ids option for bzr log from :366 M 644 inline NEWS data 5071 bzr-0.0.5 NOT RELEASED YET ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New 'bzr ignore PATTERN' command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids``. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on mango-sorbet by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/branch.py data 36958 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. base Base directory of the branch. """ _lockmode = None def __init__(self, base, init=False, find_root=True, lock_mode='w'): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.lock(lock_mode) self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def lock(self, mode='w'): """Lock the on-disk branch, excluding other processes.""" try: import fcntl, errno if mode == 'w': lm = fcntl.LOCK_EX om = os.O_WRONLY | os.O_CREAT elif mode == 'r': lm = fcntl.LOCK_SH om = os.O_RDONLY else: raise BzrError("invalid locking mode %r" % mode) try: lockfile = os.open(self.controlfilename('branch-lock'), om) except OSError, e: if e.errno == errno.ENOENT: # might not exist on branches from <0.0.4 self.controlfile('branch-lock', 'w').close() lockfile = os.open(self.controlfilename('branch-lock'), om) else: raise e fcntl.lockf(lockfile, lm) def unlock(): fcntl.lockf(lockfile, fcntl.LOCK_UN) os.close(lockfile) self._lockmode = None self.unlock = unlock self._lockmode = mode except ImportError: warning("please write a locking method for platform %r" % sys.platform) def unlock(): self._lockmode = None self.unlock = unlock self._lockmode = mode def _need_readlock(self): if self._lockmode not in ['r', 'w']: raise BzrError('need read lock on branch, only have %r' % self._lockmode) def _need_writelock(self): if self._lockmode not in ['w']: raise BzrError('need write lock on branch, only have %r' % self._lockmode) def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" self._need_readlock() before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self._need_writelock() ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ self._need_writelock() # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" self._need_readlock() tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability self._need_writelock() if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. """ self._need_writelock() ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) self.append_revision(rev_id) if verbose: note("commited r%d" % self.revno()) def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" self._need_readlock() r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" self._need_readlock() i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" self._need_readlock() if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self._need_readlock() return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] else: return None def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" self._need_readlock() if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original', verbose=False, show_ids=False): """Write out human-readable log of commits to this branch. show_timezone 'original' (committer's timezone), 'utc' (universal time), or 'local' (local user's timezone) verbose If true show added/changed/deleted/renamed files. show_ids If true, show revision and file ids. """ self._need_readlock() revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno rev = self.get_revision(p) if show_ids: print 'revision-id:', rev.revision_id print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: bailout("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l if verbose == True and precursor != None: # TODO: Group as added/deleted/renamed instead # TODO: Show file ids print 'changed files:' tree = self.revision_tree(p) prevtree = self.revision_tree(precursor) for file_state, fid, old_name, new_name, kind in \ diff_trees(prevtree, tree, ): if file_state == 'A' or file_state == 'M': show_status(file_state, kind, new_name) elif file_state == 'D': show_status(file_state, kind, old_name) elif file_state == 'R': show_status(file_state, kind, old_name + ' => ' + new_name) revno += 1 precursor = p def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self._need_writelock() tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self._need_writelock() ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(self, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo TODO: Get state for single files. """ self._need_readlock() # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = self.basis_tree() new = self.working_tree() for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("weird file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" try: shutil.rmtree(self.base) except OSError: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/commands.py data 27106 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass else: raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False): #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) def Relpath(Command): """Show path of a file relative to root""" takes_args = ('filename') def run(self): print Branch(self.args['filename']).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. TODO: Option to limit to only a single file or to get log for a different directory. """ takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, timezone='original', verbose=False, show_ids=False): b = Branch('.', lock_mode='r') b.write_log(show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern""" takes_args = ['name_pattern'] def run(self, name_pattern): b = Branch('.') # XXX: This will fail if it's a hardlink; should use an AtomicFile class. f = open(b.abspath('.bzrignore'), 'at') f.write(name_pattern + '\n') f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them.""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revno=None): b = Branch('.') if revno == None: rh = b.revision_history[-1] else: rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, verbose=False): if not message: raise BzrCommandError("please specify a commit message") Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option %r is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if isinstance(e, IOError) and e.errno == errno.EPIPE: quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :368 committer Martin Pool 1115275905 +1000 data 21 - remove bailout call from :367 M 644 inline bzrlib/branch.py data 36970 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. base Base directory of the branch. """ _lockmode = None def __init__(self, base, init=False, find_root=True, lock_mode='w'): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.lock(lock_mode) self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def lock(self, mode='w'): """Lock the on-disk branch, excluding other processes.""" try: import fcntl, errno if mode == 'w': lm = fcntl.LOCK_EX om = os.O_WRONLY | os.O_CREAT elif mode == 'r': lm = fcntl.LOCK_SH om = os.O_RDONLY else: raise BzrError("invalid locking mode %r" % mode) try: lockfile = os.open(self.controlfilename('branch-lock'), om) except OSError, e: if e.errno == errno.ENOENT: # might not exist on branches from <0.0.4 self.controlfile('branch-lock', 'w').close() lockfile = os.open(self.controlfilename('branch-lock'), om) else: raise e fcntl.lockf(lockfile, lm) def unlock(): fcntl.lockf(lockfile, fcntl.LOCK_UN) os.close(lockfile) self._lockmode = None self.unlock = unlock self._lockmode = mode except ImportError: warning("please write a locking method for platform %r" % sys.platform) def unlock(): self._lockmode = None self.unlock = unlock self._lockmode = mode def _need_readlock(self): if self._lockmode not in ['r', 'w']: raise BzrError('need read lock on branch, only have %r' % self._lockmode) def _need_writelock(self): if self._lockmode not in ['w']: raise BzrError('need write lock on branch, only have %r' % self._lockmode) def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" self._need_readlock() before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self._need_writelock() ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ self._need_writelock() # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" self._need_readlock() tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability self._need_writelock() if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. """ self._need_writelock() ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) self.append_revision(rev_id) if verbose: note("commited r%d" % self.revno()) def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" self._need_readlock() r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" self._need_readlock() i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" self._need_readlock() if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self._need_readlock() return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] else: return None def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" self._need_readlock() if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def write_log(self, show_timezone='original', verbose=False, show_ids=False): """Write out human-readable log of commits to this branch. show_timezone 'original' (committer's timezone), 'utc' (universal time), or 'local' (local user's timezone) verbose If true show added/changed/deleted/renamed files. show_ids If true, show revision and file ids. """ self._need_readlock() revno = 1 precursor = None for p in self.revision_history(): print '-' * 40 print 'revno:', revno rev = self.get_revision(p) if show_ids: print 'revision-id:', rev.revision_id print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: raise BzrCheckError("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l if verbose == True and precursor != None: # TODO: Group as added/deleted/renamed instead # TODO: Show file ids print 'changed files:' tree = self.revision_tree(p) prevtree = self.revision_tree(precursor) for file_state, fid, old_name, new_name, kind in \ diff_trees(prevtree, tree, ): if file_state == 'A' or file_state == 'M': show_status(file_state, kind, new_name) elif file_state == 'D': show_status(file_state, kind, old_name) elif file_state == 'R': show_status(file_state, kind, old_name + ' => ' + new_name) revno += 1 precursor = p def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self._need_writelock() tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self._need_writelock() ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(self, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo TODO: Get state for single files. """ self._need_readlock() # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = self.basis_tree() new = self.working_tree() for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("weird file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" try: shutil.rmtree(self.base) except OSError: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :369 committer Martin Pool 1115276352 +1000 data 80 - Split out log printing into new show_log function not as a method of Branch. from :368 M 644 inline bzrlib/log.py data 3103 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def show_log(branch, show_timezone='original', verbose=False, show_ids=False, to_file=None): """Write out human-readable log of commits to this branch. show_timezone 'original' (committer's timezone), 'utc' (universal time), or 'local' (local user's timezone) verbose If true show added/changed/deleted/renamed files. show_ids If true, show revision and file ids. to_file File to send log to; by default stdout. """ from osutils import format_date from errors import BzrCheckError from diff import diff_trees from textui import show_status if to_file == None: import sys to_file = sys.stdout branch._need_readlock() revno = 1 precursor = None for revision_id in branch.revision_history(): print '-' * 40 print 'revno:', revno rev = branch.get_revision(revision_id) if show_ids: print 'revision-id:', revision_id print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: raise BzrCheckError("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l if verbose and precursor: # TODO: Group as added/deleted/renamed instead # TODO: Show file ids print 'changed files:' tree = branch.revision_tree(revision_id) prevtree = branch.revision_tree(precursor) for file_state, fid, old_name, new_name, kind in \ diff_trees(prevtree, tree, ): if file_state == 'A' or file_state == 'M': show_status(file_state, kind, new_name) elif file_state == 'D': show_status(file_state, kind, old_name) elif file_state == 'R': show_status(file_state, kind, old_name + ' => ' + new_name) revno += 1 precursor = revision_id M 644 inline bzrlib/__init__.py data 1731 # (C) 2005 Canonical Development Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr library""" from inventory import Inventory, InventoryEntry from branch import Branch, ScratchBranch from osutils import format_date from tree import Tree from diff import diff_trees from trace import mutter, warning, open_tracefile from log import show_log import add BZRDIR = ".bzr" DEFAULT_IGNORE = ['.bzr.log', '*~', '#*#', '*$', '.#*', '.*.swp', '.*.tmp', '*.tmp', '*.bak', '*.BAK', '*.orig', '*.o', '*.obj', '*.a', '*.py[oc]', '*.so', '*.exe', '*.elc', '{arch}', 'CVS', 'CVS.adm', '.svn', '_darcs', 'SCCS', 'RCS', '*,v', 'BitKeeper', 'TAGS', '.make.state', '.sconsign', '.tmp*', '.del-*'] IGNORE_FILENAME = ".bzrignore" import locale user_encoding = locale.getpreferredencoding() __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __version__ = '0.0.5pre' M 644 inline bzrlib/branch.py data 34699 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. base Base directory of the branch. """ _lockmode = None def __init__(self, base, init=False, find_root=True, lock_mode='w'): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.lock(lock_mode) self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def lock(self, mode='w'): """Lock the on-disk branch, excluding other processes.""" try: import fcntl, errno if mode == 'w': lm = fcntl.LOCK_EX om = os.O_WRONLY | os.O_CREAT elif mode == 'r': lm = fcntl.LOCK_SH om = os.O_RDONLY else: raise BzrError("invalid locking mode %r" % mode) try: lockfile = os.open(self.controlfilename('branch-lock'), om) except OSError, e: if e.errno == errno.ENOENT: # might not exist on branches from <0.0.4 self.controlfile('branch-lock', 'w').close() lockfile = os.open(self.controlfilename('branch-lock'), om) else: raise e fcntl.lockf(lockfile, lm) def unlock(): fcntl.lockf(lockfile, fcntl.LOCK_UN) os.close(lockfile) self._lockmode = None self.unlock = unlock self._lockmode = mode except ImportError: warning("please write a locking method for platform %r" % sys.platform) def unlock(): self._lockmode = None self.unlock = unlock self._lockmode = mode def _need_readlock(self): if self._lockmode not in ['r', 'w']: raise BzrError('need read lock on branch, only have %r' % self._lockmode) def _need_writelock(self): if self._lockmode not in ['w']: raise BzrError('need write lock on branch, only have %r' % self._lockmode) def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" self._need_readlock() before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self._need_writelock() ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ self._need_writelock() # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" self._need_readlock() tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability self._need_writelock() if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. """ self._need_writelock() ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) self.append_revision(rev_id) if verbose: note("commited r%d" % self.revno()) def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" self._need_readlock() r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" self._need_readlock() i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" self._need_readlock() if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self._need_readlock() return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] else: return None def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" self._need_readlock() if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self._need_writelock() tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self._need_writelock() ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(self, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo TODO: Get state for single files. """ self._need_readlock() # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = self.basis_tree() new = self.working_tree() for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("weird file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" try: shutil.rmtree(self.base) except OSError: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/commands.py data 27103 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass else: raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False): #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) def Relpath(Command): """Show path of a file relative to root""" takes_args = ('filename') def run(self): print Branch(self.args['filename']).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. TODO: Option to limit to only a single file or to get log for a different directory. """ takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, timezone='original', verbose=False, show_ids=False): b = Branch('.', lock_mode='r') b.show_log(show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern""" takes_args = ['name_pattern'] def run(self, name_pattern): b = Branch('.') # XXX: This will fail if it's a hardlink; should use an AtomicFile class. f = open(b.abspath('.bzrignore'), 'at') f.write(name_pattern + '\n') f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them.""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revno=None): b = Branch('.') if revno == None: rh = b.revision_history[-1] else: rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, verbose=False): if not message: raise BzrCommandError("please specify a commit message") Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option %r is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if isinstance(e, IOError) and e.errno == errno.EPIPE: quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/tests.py data 6099 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # XXX: We might prefer these to be in a text file rather than Python # source, but that only works in doctest from Python 2.4 and later, # which is not present in Warty. r""" Bazaar-NG test cases ******************** These are run by ``bzr.doctest``. >>> import bzrlib, os >>> from bzrlib import ScratchBranch >>> from bzrlib.osutils import isdir, isfile The basic object is a Branch. We have a special helper class ScratchBranch that automatically makes a directory and cleans itself up, but is in other respects identical. ScratchBranches are initially empty: >>> b = bzrlib.ScratchBranch() >>> b.show_status() New files in that directory are, it is initially unknown: >>> file(b.base + '/hello.c', 'wt').write('int main() {}') >>> b.show_status() ? hello.c That's not quite true; some files (like editor backups) are ignored by default: >>> file(b.base + '/hello.c~', 'wt').write('int main() {}') >>> b.show_status() ? hello.c >>> list(b.unknowns()) ['hello.c'] The ``add`` command marks a file to be added in the next revision: >>> b.add('hello.c') >>> b.show_status() A hello.c You can also add files that otherwise would be ignored. The ignore patterns only apply to files that would be otherwise unknown, so they have no effect once it's added. >>> b.add('hello.c~') >>> b.show_status() A hello.c A hello.c~ It is an error to add a file that isn't present in the working copy: >>> b.add('nothere') Traceback (most recent call last): ... BzrError: ('cannot add: not a regular file or directory: nothere', []) If we add a file and then change our mind, we can either revert it or remove the file. If we revert, we are left with the working copy (in either I or ? state). If we remove, the working copy is gone. Let's do that to the backup, presumably added accidentally. >>> b.remove('hello.c~') >>> b.show_status() A hello.c Now to commit, creating a new revision. (Fake the date and name for reproducibility.) >>> b.commit('start hello world', timestamp=0, committer='foo@nowhere') >>> b.show_status() >>> b.show_status(show_all=True) . hello.c I hello.c~ We can look back at history >>> r = b.get_revision(b.lookup_revision(1)) >>> r.message 'start hello world' >>> bzrlib.show_log(b, show_timezone='utc') ---------------------------------------- revno: 1 committer: foo@nowhere timestamp: Thu 1970-01-01 00:00:00 +0000 message: start hello world (The other fields will be a bit unpredictable, depending on who ran this test and when.) As of 2005-02-21, we can also add subdirectories to the revision! >>> os.mkdir(b.base + "/lib") >>> b.show_status() ? lib/ >>> b.add('lib') >>> b.show_status() A lib/ >>> b.commit('add subdir') >>> b.show_status() >>> b.show_status(show_all=True) . hello.c I hello.c~ . lib/ and we can also add files within subdirectories: >>> file(b.base + '/lib/hello', 'w').write('hello!\n') >>> b.show_status() ? lib/hello Tests for adding subdirectories, etc. >>> b = bzrlib.branch.ScratchBranch() >>> os.mkdir(b.abspath('d1')) >>> os.mkdir(b.abspath('d2')) >>> os.mkdir(b.abspath('d2/d3')) >>> list(b.working_tree().unknowns()) ['d1', 'd2'] Create some files, but they're not seen as unknown yet: >>> file(b.abspath('d1/f1'), 'w').close() >>> file(b.abspath('d2/f2'), 'w').close() >>> file(b.abspath('d2/f3'), 'w').close() >>> [v[0] for v in b.inventory.directories()] [''] >>> list(b.working_tree().unknowns()) ['d1', 'd2'] Adding a directory, and we see the file underneath: >>> b.add('d1') >>> [v[0] for v in b.inventory.directories()] ['', 'd1'] >>> list(b.working_tree().unknowns()) ['d2', 'd1/f1'] >>> # d2 comes first because it's in the top directory >>> b.add('d2') >>> b.commit('add some stuff') >>> list(b.working_tree().unknowns()) ['d1/f1', 'd2/d3', 'd2/f2', 'd2/f3'] >>> b.add('d1/f1') >>> list(b.working_tree().unknowns()) ['d2/d3', 'd2/f2', 'd2/f3'] Tests for ignored files and patterns: >>> b = ScratchBranch(dirs=['src', 'doc'], ... files=['configure.in', 'configure', ... 'doc/configure', 'foo.c', ... 'foo']) >>> list(b.unknowns()) ['configure', 'configure.in', 'doc', 'foo', 'foo.c', 'src'] >>> b.add(['doc', 'foo.c', 'src', 'configure.in']) >>> list(b.unknowns()) ['configure', 'foo', 'doc/configure'] >>> f = file(b.abspath('.bzrignore'), 'w') >>> f.write('./configure\n' ... './foo\n') >>> f.close() >>> b.add('.bzrignore') >>> list(b.unknowns()) ['doc/configure'] >>> b.commit("commit 1") >>> list(b.unknowns()) ['doc/configure'] >>> b.add("doc/configure") >>> b.commit("commit more") >>> del b Renames, etc: >>> b = ScratchBranch(files=['foo'], dirs=['subdir']) >>> b.add(['foo', 'subdir']) >>> b.commit('add foo') >>> list(b.unknowns()) [] >>> b.move(['foo'], 'subdir') foo => subdir/foo >>> b.show_status() R foo => subdir/foo >>> b.commit("move foo to subdir") >>> isfile(b.abspath('foo')) False >>> isfile(b.abspath('subdir/foo')) True """ commit refs/heads/master mark :370 committer Martin Pool 1115276417 +1000 data 4 todo from :369 M 644 inline TODO data 8280 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be ``bzr export -r REV``. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * ``bzr help commands`` should give a one-line summary of each command. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Read and write locks on branch while it's open. * Separate read and write version checks? * ``bzr status FILE...`` * Check all commands have decent help. * Autogenerate argument/option help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. * Split BzrError into various more specific subclasses for different errors people might want to catch. Medium things ------------- * Display command grammar in help messages rather than hardcoding it. * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :371 committer Martin Pool 1115276455 +1000 data 24 - Fix up bzr log command from :370 M 644 inline bzrlib/commands.py data 27145 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass else: raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False): #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) def Relpath(Command): """Show path of a file relative to root""" takes_args = ('filename') def run(self): print Branch(self.args['filename']).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. TODO: Option to limit to only a single file or to get log for a different directory. """ takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, timezone='original', verbose=False, show_ids=False): b = Branch('.', lock_mode='r') bzrlib.show_log(b, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern""" takes_args = ['name_pattern'] def run(self, name_pattern): b = Branch('.') # XXX: This will fail if it's a hardlink; should use an AtomicFile class. f = open(b.abspath('.bzrignore'), 'at') f.write(name_pattern + '\n') f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them.""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revno=None): b = Branch('.') if revno == None: rh = b.revision_history[-1] else: rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, verbose=False): if not message: raise BzrCommandError("please specify a commit message") Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option %r is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if isinstance(e, IOError) and e.errno == errno.EPIPE: quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :372 committer Martin Pool 1115276514 +1000 data 41 - Add consistency check when checking log from :371 M 644 inline bzrlib/log.py data 3276 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def show_log(branch, show_timezone='original', verbose=False, show_ids=False, to_file=None): """Write out human-readable log of commits to this branch. show_timezone 'original' (committer's timezone), 'utc' (universal time), or 'local' (local user's timezone) verbose If true show added/changed/deleted/renamed files. show_ids If true, show revision and file ids. to_file File to send log to; by default stdout. """ from osutils import format_date from errors import BzrCheckError from diff import diff_trees from textui import show_status if to_file == None: import sys to_file = sys.stdout branch._need_readlock() revno = 1 precursor = None for revision_id in branch.revision_history(): print '-' * 40 print 'revno:', revno rev = branch.get_revision(revision_id) if show_ids: print 'revision-id:', revision_id print 'committer:', rev.committer print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) if revision_id != rev.revision_id: raise BzrCheckError("retrieved wrong revision: %r" % (revision_id, rev.revision_id)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: raise BzrCheckError("mismatched precursor!") print 'message:' if not rev.message: print ' (no message)' else: for l in rev.message.split('\n'): print ' ' + l if verbose and precursor: # TODO: Group as added/deleted/renamed instead # TODO: Show file ids print 'changed files:' tree = branch.revision_tree(revision_id) prevtree = branch.revision_tree(precursor) for file_state, fid, old_name, new_name, kind in \ diff_trees(prevtree, tree, ): if file_state == 'A' or file_state == 'M': show_status(file_state, kind, new_name) elif file_state == 'D': show_status(file_state, kind, old_name) elif file_state == 'R': show_status(file_state, kind, old_name + ' => ' + new_name) revno += 1 precursor = revision_id commit refs/heads/master mark :373 committer Martin Pool 1115276946 +1000 data 56 - show_log() can send output to a file other than stdout from :372 M 644 inline bzrlib/log.py data 3383 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def show_log(branch, show_timezone='original', verbose=False, show_ids=False, to_file=None): """Write out human-readable log of commits to this branch. show_timezone 'original' (committer's timezone), 'utc' (universal time), or 'local' (local user's timezone) verbose If true show added/changed/deleted/renamed files. show_ids If true, show revision and file ids. to_file File to send log to; by default stdout. """ from osutils import format_date from errors import BzrCheckError from diff import diff_trees from textui import show_status if to_file == None: import sys to_file = sys.stdout branch._need_readlock() revno = 1 precursor = None for revision_id in branch.revision_history(): print >>to_file, '-' * 60 print >>to_file, 'revno:', revno rev = branch.get_revision(revision_id) if show_ids: print >>to_file, 'revision-id:', revision_id print >>to_file, 'committer:', rev.committer print >>to_file, 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) if revision_id != rev.revision_id: raise BzrCheckError("retrieved wrong revision: %r" % (revision_id, rev.revision_id)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: raise BzrCheckError("mismatched precursor!") print >>to_file, 'message:' if not rev.message: print >>to_file, ' (no message)' else: for l in rev.message.split('\n'): print >>to_file, ' ' + l if verbose and precursor: # TODO: Group as added/deleted/renamed instead # TODO: Show file ids print >>to_file, 'changed files:' tree = branch.revision_tree(revision_id) prevtree = branch.revision_tree(precursor) for file_state, fid, old_name, new_name, kind in \ diff_trees(prevtree, tree, ): if file_state == 'A' or file_state == 'M': show_status(file_state, kind, new_name) elif file_state == 'D': show_status(file_state, kind, old_name) elif file_state == 'R': show_status(file_state, kind, old_name + ' => ' + new_name) revno += 1 precursor = revision_id commit refs/heads/master mark :374 committer Martin Pool 1115277109 +1000 data 44 - Better bash completion from Peter Hoffmann from :373 M 644 inline contrib/bash/bzr data 546 # -*- shell-script -*- # experimental bzr bash completion # author: Martin Pool _bzr_commands() { bzr help commands | grep -v '^ ' } _bzr() { cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]} if [ $COMP_CWORD -eq 1 ]; then COMPREPLY=( $( compgen -W "$(_bzr_commands)" $cur ) ) elif [ $COMP_CWORD -eq 2 ]; then case "$prev" in help) COMPREPLY=( $( compgen -W "$(_bzr_commands) commands" $cur ) ) ;; esac fi } complete -F _bzr -o default bzr commit refs/heads/master mark :375 committer Martin Pool 1115279022 +1000 data 81 - New command touching-revisions and function to trace changes to a given file. from :374 M 644 inline NEWS data 5204 bzr-0.0.5 NOT RELEASED YET ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New 'bzr ignore PATTERN' command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids``. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on mango-sorbet by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 27595 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass else: raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False): #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) def Relpath(Command): """Show path of a file relative to root""" takes_args = ('filename') def run(self): print Branch(self.args['filename']).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. TODO: Option to limit to only a single file or to get log for a different directory. """ takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, timezone='original', verbose=False, show_ids=False): b = Branch('.', lock_mode='r') bzrlib.show_log(b, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern""" takes_args = ['name_pattern'] def run(self, name_pattern): b = Branch('.') # XXX: This will fail if it's a hardlink; should use an AtomicFile class. f = open(b.abspath('.bzrignore'), 'at') f.write(name_pattern + '\n') f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them.""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revno=None): b = Branch('.') if revno == None: rh = b.revision_history[-1] else: rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, verbose=False): if not message: raise BzrCommandError("please specify a commit message") Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option %r is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if isinstance(e, IOError) and e.errno == errno.EPIPE: quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/log.py data 5012 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def find_touching_revisions(branch, file_id): """Yield a description of revisions which affect the file_id. Each returned element is (revno, revision_id, description) This is the list of revisions where the file is either added, modified, renamed or deleted. Revisions are returned in chronological order. TODO: Perhaps some way to limit this to only particular revisions, or to traverse a non-branch set of revisions? """ last_ie = None last_path = None revno = 1 for revision_id in branch.revision_history(): this_inv = branch.get_revision_inventory(revision_id) if file_id in this_inv: this_ie = this_inv[file_id] this_path = this_inv.id2path(file_id) else: this_ie = this_path = None # now we know how it was last time, and how it is in this revision. # are those two states effectively the same or not? if not this_ie and not last_ie: # not present in either pass elif this_ie and not last_ie: yield revno, revision_id, "added " + this_path elif not this_ie and last_ie: # deleted here yield revno, revision_id, "deleted " + last_path elif this_path != last_path: yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path)) elif (this_ie.text_size != last_ie.text_size or this_ie.text_sha1 != last_ie.text_sha1): yield revno, revision_id, "modified " + this_path last_ie = this_ie last_path = this_path revno += 1 def show_log(branch, show_timezone='original', verbose=False, show_ids=False, to_file=None): """Write out human-readable log of commits to this branch. show_timezone 'original' (committer's timezone), 'utc' (universal time), or 'local' (local user's timezone) verbose If true show added/changed/deleted/renamed files. show_ids If true, show revision and file ids. to_file File to send log to; by default stdout. """ from osutils import format_date from errors import BzrCheckError from diff import diff_trees from textui import show_status if to_file == None: import sys to_file = sys.stdout branch._need_readlock() revno = 1 precursor = None for revision_id in branch.revision_history(): print >>to_file, '-' * 60 print >>to_file, 'revno:', revno rev = branch.get_revision(revision_id) if show_ids: print >>to_file, 'revision-id:', revision_id print >>to_file, 'committer:', rev.committer print >>to_file, 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) if revision_id != rev.revision_id: raise BzrCheckError("retrieved wrong revision: %r" % (revision_id, rev.revision_id)) ## opportunistic consistency check, same as check_patch_chaining if rev.precursor != precursor: raise BzrCheckError("mismatched precursor!") print >>to_file, 'message:' if not rev.message: print >>to_file, ' (no message)' else: for l in rev.message.split('\n'): print >>to_file, ' ' + l if verbose and precursor: # TODO: Group as added/deleted/renamed instead # TODO: Show file ids print >>to_file, 'changed files:' tree = branch.revision_tree(revision_id) prevtree = branch.revision_tree(precursor) for file_state, fid, old_name, new_name, kind in \ diff_trees(prevtree, tree, ): if file_state == 'A' or file_state == 'M': show_status(file_state, kind, new_name) elif file_state == 'D': show_status(file_state, kind, old_name) elif file_state == 'R': show_status(file_state, kind, old_name + ' => ' + new_name) revno += 1 precursor = revision_id commit refs/heads/master mark :376 committer Martin Pool 1115279822 +1000 data 120 - fix slow invariant check when reading in InventoryEntry objects - new default value pattern for InventoryEntry objects from :375 M 644 inline bzrlib/inventory.py data 19235 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # TODO: Maybe store inventory_id in the file? Not really needed. # This should really be an id randomly assigned when the tree is # created, but it's not for now. ROOT_ID = "TREE_ROOT" import sys, os.path, types, re from sets import Set try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from xml import XMLMixin from errors import bailout, BzrError import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') 'TREE_ROOT' >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID)) >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123')) >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' TODO: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ # TODO: split InventoryEntry into subclasses for files, # directories, etc etc. text_sha1 = None text_size = None def __init__(self, file_id, name, kind, parent_id, text_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID) >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID) Traceback (most recent call last): BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", []) """ if '/' in name or '\\' in name: raise BzrCheckError('InventoryEntry name %r is invalid' % name) self.file_id = file_id self.name = name self.kind = kind self.text_id = text_id self.parent_id = parent_id if kind == 'directory': self.children = {} elif kind == 'file': pass else: raise BzrError("unhandled entry kind %r" % kind) def sorted_children(self): l = self.children.items() l.sort() return l def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.parent_id, text_id=self.text_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size != None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1']: v = getattr(self, f) if v != None: e.set(f, v) # to be conservative, we don't externalize the root pointers # for now, leaving them as null in the xml form. in a future # version it will be implied by nested elements. if self.parent_id != ROOT_ID: assert isinstance(self.parent_id, basestring) e.set('parent_id', self.parent_id) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' ## original format inventories don't have a parent_id for ## nodes in the root directory, but it's cleaner to use one ## internally. parent_id = elt.get('parent_id') if parent_id == None: parent_id = ROOT_ID self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __cmp__(self, other): if self is other: return 0 if not isinstance(other, InventoryEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.name, other.name) \ or cmp(self.text_sha1, other.text_sha1) \ or cmp(self.text_size, other.text_size) \ or cmp(self.text_id, other.text_id) \ or cmp(self.parent_id, other.parent_id) \ or cmp(self.kind, other.kind) class RootEntry(InventoryEntry): def __init__(self, file_id): self.file_id = file_id self.children = {} self.kind = 'root_directory' self.parent_id = None self.name = '' def __cmp__(self, other): if self is other: return 0 if not isinstance(other, RootEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.children, other.children) class Inventory(XMLMixin): """Inventory of versioned files in a tree. This describes which file_id is present at each point in the tree, and possibly the SHA-1 or other information about the file. Entries can be looked up either by path or by file_id. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted, other than through the Inventory API. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID)) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ ## TODO: Make sure only canonical filenames are stored. ## TODO: Do something sensible about the possible collisions on ## case-losing filesystems. Perhaps we should just always forbid ## such collisions. ## TODO: No special cases for root, rather just give it a file id ## like everything else. ## TODO: Probably change XML serialization to use nesting rather ## than parent_id pointers. ## TODO: Perhaps hold the ElementTree in memory and work directly ## on that rather than converting into Python objects every time? def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. The inventory is created with a default root directory, with an id of None. """ self.root = RootEntry(ROOT_ID) self._byid = {self.root.file_id: self.root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, from_dir=None): """Return (path, entry) pairs, in order by name.""" if from_dir == None: assert self.root from_dir = self.root elif isinstance(from_dir, basestring): from_dir = self._byid[from_dir] kids = from_dir.children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(from_dir=ie.file_id): yield os.path.join(name, cn), cie def directories(self): """Return (path, entry) pairs for all directories. """ def descend(parent_ie): parent_name = parent_ie.name yield parent_name, parent_ie # directory children in sorted order dn = [] for ie in parent_ie.children.itervalues(): if ie.kind == 'directory': dn.append((ie.name, ie)) dn.sort() for name, child_ie in dn: for sub_name, sub_ie in descend(child_ie): yield appendpath(parent_name, sub_name), sub_ie for name, ie in descend(self.root): yield name, ie def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID)) >>> inv['123123'].name 'hello.c' """ if file_id == None: raise BzrError("can't look up file_id None") try: return self._byid[file_id] except KeyError: raise BzrError("file_id {%s} not in inventory" % file_id) def get_child(self, parent_id, filename): return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self._byid: bailout("inventory already contains entry with id {%s}" % entry.file_id) try: parent = self._byid[entry.parent_id] except KeyError: bailout("parent_id {%s} not in inventory" % entry.parent_id) if parent.children.has_key(entry.name): bailout("%s is already versioned" % appendpath(self.id2path(parent.file_id), entry.name)) self._byid[entry.file_id] = entry parent.children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: bailout("cannot re-add root of inventory") if file_id == None: file_id = bzrlib.branch.gen_file_id(relpath) parent_id = self.path2id(parts[:-1]) assert parent_id != None ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def id_set(self): return Set(self._byid) def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID)) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __cmp__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 True """ if self is other: return 0 if not isinstance(other, Inventory): return NotImplemented if self.id_set() ^ other.id_set(): return 1 for file_id in self._byid: c = cmp(self[file_id], other[file_id]) if c: return c return 0 def get_idpath(self, file_id): """Return a list of file_ids for the path to an entry. The list contains one element for each directory followed by the id of the file itself. So the length of the returned list is equal to the depth of the file in the tree, counting the root directory as depth 1. """ p = [] while file_id != None: try: ie = self._byid[file_id] except KeyError: bailout("file_id {%s} not found in inventory" % file_id) p.insert(0, ie.file_id) file_id = ie.parent_id return p def id2path(self, file_id): """Return as a list the path to file_id.""" # get all names, skipping root p = [self[fid].name for fid in self.get_idpath(file_id)[1:]] return os.sep.join(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. Returns None iff the path is not found. """ if isinstance(name, types.StringTypes): name = splitpath(name) mutter("lookup path %r" % name) parent = self.root for f in name: try: cie = parent.children[f] assert cie.name == f assert cie.parent_id == parent.file_id parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): return self._byid.has_key(file_id) def rename(self, file_id, new_parent_id, new_name): """Move a file within the inventory. This can change either the name, or the parent, or both. This does not move the working file.""" if not is_valid_name(new_name): bailout("not an acceptable filename: %r" % new_name) new_parent = self._byid[new_parent_id] if new_name in new_parent.children: bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id))) new_parent_idpath = self.get_idpath(new_parent_id) if file_id in new_parent_idpath: bailout("cannot move directory %r into a subdirectory of itself, %r" % (self.id2path(file_id), self.id2path(new_parent_id))) file_ie = self._byid[file_id] old_parent = self._byid[file_ie.parent_id] # TODO: Don't leave things messed up if this fails del old_parent.children[file_ie.name] new_parent.children[new_name] = file_ie file_ie.name = new_name file_ie.parent_id = new_parent_id _NAME_RE = re.compile(r'^[^/\\]+$') def is_valid_name(name): return bool(_NAME_RE.match(name)) commit refs/heads/master mark :377 committer Martin Pool 1115280398 +1000 data 25 - todo notes on inventory from :376 M 644 inline TODO data 8853 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be ``bzr export -r REV``. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * ``bzr help commands`` should give a one-line summary of each command. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Read and write locks on branch while it's open. * Separate read and write version checks? * ``bzr status FILE...`` * Check all commands have decent help. * Autogenerate argument/option help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. * Split BzrError into various more specific subclasses for different errors people might want to catch. Medium things ------------- * Display command grammar in help messages rather than hardcoding it. * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still expose it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` M 644 inline bzrlib/inventory.py data 18599 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # This should really be an id randomly assigned when the tree is # created, but it's not for now. ROOT_ID = "TREE_ROOT" import sys, os.path, types, re from sets import Set try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from xml import XMLMixin from errors import bailout, BzrError import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') 'TREE_ROOT' >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID)) >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123')) >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' TODO: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ # TODO: split InventoryEntry into subclasses for files, # directories, etc etc. text_sha1 = None text_size = None def __init__(self, file_id, name, kind, parent_id, text_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID) >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID) Traceback (most recent call last): BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", []) """ if '/' in name or '\\' in name: raise BzrCheckError('InventoryEntry name %r is invalid' % name) self.file_id = file_id self.name = name self.kind = kind self.text_id = text_id self.parent_id = parent_id if kind == 'directory': self.children = {} elif kind == 'file': pass else: raise BzrError("unhandled entry kind %r" % kind) def sorted_children(self): l = self.children.items() l.sort() return l def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.parent_id, text_id=self.text_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size != None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1']: v = getattr(self, f) if v != None: e.set(f, v) # to be conservative, we don't externalize the root pointers # for now, leaving them as null in the xml form. in a future # version it will be implied by nested elements. if self.parent_id != ROOT_ID: assert isinstance(self.parent_id, basestring) e.set('parent_id', self.parent_id) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' ## original format inventories don't have a parent_id for ## nodes in the root directory, but it's cleaner to use one ## internally. parent_id = elt.get('parent_id') if parent_id == None: parent_id = ROOT_ID self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __cmp__(self, other): if self is other: return 0 if not isinstance(other, InventoryEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.name, other.name) \ or cmp(self.text_sha1, other.text_sha1) \ or cmp(self.text_size, other.text_size) \ or cmp(self.text_id, other.text_id) \ or cmp(self.parent_id, other.parent_id) \ or cmp(self.kind, other.kind) class RootEntry(InventoryEntry): def __init__(self, file_id): self.file_id = file_id self.children = {} self.kind = 'root_directory' self.parent_id = None self.name = '' def __cmp__(self, other): if self is other: return 0 if not isinstance(other, RootEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.children, other.children) class Inventory(XMLMixin): """Inventory of versioned files in a tree. This describes which file_id is present at each point in the tree, and possibly the SHA-1 or other information about the file. Entries can be looked up either by path or by file_id. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted, other than through the Inventory API. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID)) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. The inventory is created with a default root directory, with an id of None. """ self.root = RootEntry(ROOT_ID) self._byid = {self.root.file_id: self.root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, from_dir=None): """Return (path, entry) pairs, in order by name.""" if from_dir == None: assert self.root from_dir = self.root elif isinstance(from_dir, basestring): from_dir = self._byid[from_dir] kids = from_dir.children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(from_dir=ie.file_id): yield os.path.join(name, cn), cie def directories(self): """Return (path, entry) pairs for all directories. """ def descend(parent_ie): parent_name = parent_ie.name yield parent_name, parent_ie # directory children in sorted order dn = [] for ie in parent_ie.children.itervalues(): if ie.kind == 'directory': dn.append((ie.name, ie)) dn.sort() for name, child_ie in dn: for sub_name, sub_ie in descend(child_ie): yield appendpath(parent_name, sub_name), sub_ie for name, ie in descend(self.root): yield name, ie def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID)) >>> inv['123123'].name 'hello.c' """ if file_id == None: raise BzrError("can't look up file_id None") try: return self._byid[file_id] except KeyError: raise BzrError("file_id {%s} not in inventory" % file_id) def get_child(self, parent_id, filename): return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self._byid: bailout("inventory already contains entry with id {%s}" % entry.file_id) try: parent = self._byid[entry.parent_id] except KeyError: bailout("parent_id {%s} not in inventory" % entry.parent_id) if parent.children.has_key(entry.name): bailout("%s is already versioned" % appendpath(self.id2path(parent.file_id), entry.name)) self._byid[entry.file_id] = entry parent.children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: bailout("cannot re-add root of inventory") if file_id == None: file_id = bzrlib.branch.gen_file_id(relpath) parent_id = self.path2id(parts[:-1]) assert parent_id != None ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def id_set(self): return Set(self._byid) def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID)) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __cmp__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 True """ if self is other: return 0 if not isinstance(other, Inventory): return NotImplemented if self.id_set() ^ other.id_set(): return 1 for file_id in self._byid: c = cmp(self[file_id], other[file_id]) if c: return c return 0 def get_idpath(self, file_id): """Return a list of file_ids for the path to an entry. The list contains one element for each directory followed by the id of the file itself. So the length of the returned list is equal to the depth of the file in the tree, counting the root directory as depth 1. """ p = [] while file_id != None: try: ie = self._byid[file_id] except KeyError: bailout("file_id {%s} not found in inventory" % file_id) p.insert(0, ie.file_id) file_id = ie.parent_id return p def id2path(self, file_id): """Return as a list the path to file_id.""" # get all names, skipping root p = [self[fid].name for fid in self.get_idpath(file_id)[1:]] return os.sep.join(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. Returns None iff the path is not found. """ if isinstance(name, types.StringTypes): name = splitpath(name) mutter("lookup path %r" % name) parent = self.root for f in name: try: cie = parent.children[f] assert cie.name == f assert cie.parent_id == parent.file_id parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): return self._byid.has_key(file_id) def rename(self, file_id, new_parent_id, new_name): """Move a file within the inventory. This can change either the name, or the parent, or both. This does not move the working file.""" if not is_valid_name(new_name): bailout("not an acceptable filename: %r" % new_name) new_parent = self._byid[new_parent_id] if new_name in new_parent.children: bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id))) new_parent_idpath = self.get_idpath(new_parent_id) if file_id in new_parent_idpath: bailout("cannot move directory %r into a subdirectory of itself, %r" % (self.id2path(file_id), self.id2path(new_parent_id))) file_ie = self._byid[file_id] old_parent = self._byid[file_ie.parent_id] # TODO: Don't leave things messed up if this fails del old_parent.children[file_ie.name] new_parent.children[new_name] = file_ie file_ie.name = new_name file_ie.parent_id = new_parent_id _NAME_RE = re.compile(r'^[^/\\]+$') def is_valid_name(name): return bool(_NAME_RE.match(name)) commit refs/heads/master mark :378 committer Martin Pool 1115281580 +1000 data 28 - New usage bzr log FILENAME from :377 M 644 inline NEWS data 5294 bzr-0.0.5 NOT RELEASED YET ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New 'bzr ignore PATTERN' command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids``. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on mango-sorbet by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 27629 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass else: raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False): #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) def Relpath(Command): """Show path of a file relative to root""" takes_args = ('filename') def run(self): print Branch(self.args['filename']).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): b = Branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern""" takes_args = ['name_pattern'] def run(self, name_pattern): b = Branch('.') # XXX: This will fail if it's a hardlink; should use an AtomicFile class. f = open(b.abspath('.bzrignore'), 'at') f.write(name_pattern + '\n') f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them.""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revno=None): b = Branch('.') if revno == None: rh = b.revision_history[-1] else: rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, verbose=False): if not message: raise BzrCommandError("please specify a commit message") Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option %r is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if isinstance(e, IOError) and e.errno == errno.EPIPE: quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/log.py data 5549 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def find_touching_revisions(branch, file_id): """Yield a description of revisions which affect the file_id. Each returned element is (revno, revision_id, description) This is the list of revisions where the file is either added, modified, renamed or deleted. Revisions are returned in chronological order. TODO: Perhaps some way to limit this to only particular revisions, or to traverse a non-branch set of revisions? TODO: If a directory is given, then by default look for all changes under that directory. """ last_ie = None last_path = None revno = 1 for revision_id in branch.revision_history(): this_inv = branch.get_revision_inventory(revision_id) if file_id in this_inv: this_ie = this_inv[file_id] this_path = this_inv.id2path(file_id) else: this_ie = this_path = None # now we know how it was last time, and how it is in this revision. # are those two states effectively the same or not? if not this_ie and not last_ie: # not present in either pass elif this_ie and not last_ie: yield revno, revision_id, "added " + this_path elif not this_ie and last_ie: # deleted here yield revno, revision_id, "deleted " + last_path elif this_path != last_path: yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path)) elif (this_ie.text_size != last_ie.text_size or this_ie.text_sha1 != last_ie.text_sha1): yield revno, revision_id, "modified " + this_path last_ie = this_ie last_path = this_path revno += 1 def show_log(branch, filename=None, show_timezone='original', verbose=False, show_ids=False, to_file=None): """Write out human-readable log of commits to this branch. filename If true, list only the commits affecting the specified file, rather than all commits. show_timezone 'original' (committer's timezone), 'utc' (universal time), or 'local' (local user's timezone) verbose If true show added/changed/deleted/renamed files. show_ids If true, show revision and file ids. to_file File to send log to; by default stdout. """ from osutils import format_date from errors import BzrCheckError from diff import diff_trees from textui import show_status if to_file == None: import sys to_file = sys.stdout if filename: file_id = branch.read_working_inventory().path2id(filename) def which_revs(): for revno, revid, why in find_touching_revisions(branch, file_id): yield revno, revid else: def which_revs(): for i, revid in enumerate(branch.revision_history()): yield i+1, revid branch._need_readlock() precursor = None for revno, revision_id in which_revs(): print >>to_file, '-' * 60 print >>to_file, 'revno:', revno rev = branch.get_revision(revision_id) if show_ids: print >>to_file, 'revision-id:', revision_id print >>to_file, 'committer:', rev.committer print >>to_file, 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) if revision_id != rev.revision_id: raise BzrCheckError("retrieved wrong revision: %r" % (revision_id, rev.revision_id)) print >>to_file, 'message:' if not rev.message: print >>to_file, ' (no message)' else: for l in rev.message.split('\n'): print >>to_file, ' ' + l # Don't show a list of changed files if we were asked about # one specific file. if verbose and precursor and not filename: # TODO: Group as added/deleted/renamed instead # TODO: Show file ids print >>to_file, 'changed files:' tree = branch.revision_tree(revision_id) prevtree = branch.revision_tree(precursor) for file_state, fid, old_name, new_name, kind in \ diff_trees(prevtree, tree, ): if file_state == 'A' or file_state == 'M': show_status(file_state, kind, new_name) elif file_state == 'D': show_status(file_state, kind, old_name) elif file_state == 'R': show_status(file_state, kind, old_name + ' => ' + new_name) precursor = revision_id commit refs/heads/master mark :379 committer Martin Pool 1115285187 +1000 data 110 - Simpler compare_inventories() to possibly replace diff_trees - New TreeDelta class - Use this in show_log() from :378 M 644 inline NEWS data 5416 bzr-0.0.5 NOT RELEASED YET ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New 'bzr ignore PATTERN' command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids``. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on mango-sorbet by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/diff.py data 10984 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set from trace import mutter from errors import BzrError def diff_trees(old_tree, new_tree): """Compute diff between two trees. They may be in different branches and may be working or historical trees. Yields a sequence of (state, id, old_name, new_name, kind). Each filename and each id is listed only once. """ ## TODO: Compare files before diffing; only mention those that have changed ## TODO: Set nice names in the headers, maybe include diffstat ## TODO: Perhaps make this a generator rather than using ## a callback object? ## TODO: Allow specifying a list of files to compare, rather than ## doing the whole tree? (Not urgent.) ## TODO: Allow diffing any two inventories, not just the ## current one against one. We mgiht need to specify two ## stores to look for the files if diffing two branches. That ## might imply this shouldn't be primarily a Branch method. ## XXX: This doesn't report on unknown files; that can be done ## from a separate method. old_it = old_tree.list_files() new_it = new_tree.list_files() def next(it): try: return it.next() except StopIteration: return None old_item = next(old_it) new_item = next(new_it) # We step through the two sorted iterators in parallel, trying to # keep them lined up. while (old_item != None) or (new_item != None): # OK, we still have some remaining on both, but they may be # out of step. if old_item != None: old_name, old_class, old_kind, old_id = old_item else: old_name = None if new_item != None: new_name, new_class, new_kind, new_id = new_item else: new_name = None mutter(" diff pairwise %r" % (old_item,)) mutter(" %r" % (new_item,)) if old_item: # can't handle the old tree being a WorkingTree assert old_class == 'V' if new_item and (new_class != 'V'): yield new_class, None, None, new_name, new_kind new_item = next(new_it) elif (not new_item) or (old_item and (old_name < new_name)): mutter(" extra entry in old-tree sequence") if new_tree.has_id(old_id): # will be mentioned as renamed under new name pass else: yield 'D', old_id, old_name, None, old_kind old_item = next(old_it) elif (not old_item) or (new_item and (new_name < old_name)): mutter(" extra entry in new-tree sequence") if old_tree.has_id(new_id): yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind else: yield 'A', new_id, None, new_name, new_kind new_item = next(new_it) elif old_id != new_id: assert old_name == new_name # both trees have a file of this name, but it is not the # same file. in other words, the old filename has been # overwritten by either a newly-added or a renamed file. # (should we return something about the overwritten file?) if old_tree.has_id(new_id): # renaming, overlying a deleted file yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind else: yield 'A', new_id, None, new_name, new_kind new_item = next(new_it) old_item = next(old_it) else: assert old_id == new_id assert old_id != None assert old_name == new_name assert old_kind == new_kind if old_kind == 'directory': yield '.', new_id, old_name, new_name, new_kind elif old_tree.get_file_size(old_id) != new_tree.get_file_size(old_id): mutter(" file size has changed, must be different") yield 'M', new_id, old_name, new_name, new_kind elif old_tree.get_file_sha1(old_id) == new_tree.get_file_sha1(old_id): mutter(" SHA1 indicates they're identical") ## assert compare_files(old_tree.get_file(i), new_tree.get_file(i)) yield '.', new_id, old_name, new_name, new_kind else: mutter(" quick compare shows different") yield 'M', new_id, old_name, new_name, new_kind new_item = next(new_it) old_item = next(old_it) def show_diff(b, revision, file_list): import difflib, sys, types if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. if file_list: file_list = [b.relpath(f) for f in file_list] # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in diff_trees(old_tree, new_tree): if file_list and (new_name not in file_list): continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' # FIXME: Something about the diff format makes patch unhappy # with newly-added files. def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) sys.stdout.writelines(ud) if nonl: print "\\ No newline at end of file" sys.stdout.write('\n') if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: raise BzrError("can't represent state %s {%s}" % (file_state, fid)) class TreeDelta: """Describes changes from one tree to another. Contains four lists: added (path, id) removed (path, id) renamed (oldpath, newpath, id) modified (path, id) A path may occur in more than one list if it was e.g. deleted under an old id and renamed into place in a new id. Files are listed in either modified or renamed, not both. In other words, renamed files may also be modified. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] def compare_inventories(old_inv, new_inv): """Return a TreeDelta object describing changes between inventories. This only describes changes in the shape of the tree, not the actual texts. This is an alternative to diff_trees() and should probably eventually replace it. """ old_ids = old_inv.id_set() new_ids = new_inv.id_set() delta = TreeDelta() delta.removed = [(old_inv.id2path(fid), fid) for fid in (old_ids - new_ids)] delta.removed.sort() delta.added = [(new_inv.id2path(fid), fid) for fid in (new_ids - old_ids)] delta.added.sort() for fid in old_ids & new_ids: old_ie = old_inv[fid] new_ie = new_inv[fid] old_path = old_inv.id2path(fid) new_path = new_inv.id2path(fid) if old_path != new_path: delta.renamed.append((old_path, new_path, fid)) elif old_ie.text_sha1 != new_ie.text_sha1: delta.modified.append((new_path, fid)) delta.modified.sort() delta.renamed.sort() return delta M 644 inline bzrlib/log.py data 5782 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def find_touching_revisions(branch, file_id): """Yield a description of revisions which affect the file_id. Each returned element is (revno, revision_id, description) This is the list of revisions where the file is either added, modified, renamed or deleted. Revisions are returned in chronological order. TODO: Perhaps some way to limit this to only particular revisions, or to traverse a non-branch set of revisions? TODO: If a directory is given, then by default look for all changes under that directory. """ last_ie = None last_path = None revno = 1 for revision_id in branch.revision_history(): this_inv = branch.get_revision_inventory(revision_id) if file_id in this_inv: this_ie = this_inv[file_id] this_path = this_inv.id2path(file_id) else: this_ie = this_path = None # now we know how it was last time, and how it is in this revision. # are those two states effectively the same or not? if not this_ie and not last_ie: # not present in either pass elif this_ie and not last_ie: yield revno, revision_id, "added " + this_path elif not this_ie and last_ie: # deleted here yield revno, revision_id, "deleted " + last_path elif this_path != last_path: yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path)) elif (this_ie.text_size != last_ie.text_size or this_ie.text_sha1 != last_ie.text_sha1): yield revno, revision_id, "modified " + this_path last_ie = this_ie last_path = this_path revno += 1 def show_log(branch, filename=None, show_timezone='original', verbose=False, show_ids=False, to_file=None): """Write out human-readable log of commits to this branch. filename If true, list only the commits affecting the specified file, rather than all commits. show_timezone 'original' (committer's timezone), 'utc' (universal time), or 'local' (local user's timezone) verbose If true show added/changed/deleted/renamed files. show_ids If true, show revision and file ids. to_file File to send log to; by default stdout. """ from osutils import format_date from errors import BzrCheckError from diff import compare_inventories from textui import show_status from inventory import Inventory if to_file == None: import sys to_file = sys.stdout if filename: file_id = branch.read_working_inventory().path2id(filename) def which_revs(): for revno, revid, why in find_touching_revisions(branch, file_id): yield revno, revid else: def which_revs(): for i, revid in enumerate(branch.revision_history()): yield i+1, revid branch._need_readlock() precursor = None if verbose: prev_inv = Inventory() for revno, revision_id in which_revs(): print >>to_file, '-' * 60 print >>to_file, 'revno:', revno rev = branch.get_revision(revision_id) if show_ids: print >>to_file, 'revision-id:', revision_id print >>to_file, 'committer:', rev.committer print >>to_file, 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) if revision_id != rev.revision_id: raise BzrCheckError("retrieved wrong revision: %r" % (revision_id, rev.revision_id)) print >>to_file, 'message:' if not rev.message: print >>to_file, ' (no message)' else: for l in rev.message.split('\n'): print >>to_file, ' ' + l # Don't show a list of changed files if we were asked about # one specific file. if verbose and not filename: this_inv = branch.get_inventory(rev.inventory_id) delta = compare_inventories(prev_inv, this_inv) if delta.removed: print >>to_file, 'removed files:' for path, fid in delta.removed: print >>to_file, ' ' + path if delta.added: print >>to_file, 'added files:' for path, fid in delta.added: print >>to_file, ' ' + path if delta.renamed: print >>to_file, 'renamed files:' for oldpath, newpath, fid in delta.renamed: print >>to_file, ' %s => %s' % (oldpath, newpath) if delta.modified: print >>to_file, 'modified files:' for path, fid in delta.modified: print >>to_file, ' ' + path prev_inv = this_inv precursor = revision_id commit refs/heads/master mark :380 committer Martin Pool 1115286548 +1000 data 47 - Slight optimization for Inventory.__getitem__ from :379 M 644 inline bzrlib/inventory.py data 18616 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # This should really be an id randomly assigned when the tree is # created, but it's not for now. ROOT_ID = "TREE_ROOT" import sys, os.path, types, re from sets import Set try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from xml import XMLMixin from errors import bailout, BzrError import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') 'TREE_ROOT' >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID)) >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123')) >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' TODO: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ # TODO: split InventoryEntry into subclasses for files, # directories, etc etc. text_sha1 = None text_size = None def __init__(self, file_id, name, kind, parent_id, text_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID) >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID) Traceback (most recent call last): BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", []) """ if '/' in name or '\\' in name: raise BzrCheckError('InventoryEntry name %r is invalid' % name) self.file_id = file_id self.name = name self.kind = kind self.text_id = text_id self.parent_id = parent_id if kind == 'directory': self.children = {} elif kind == 'file': pass else: raise BzrError("unhandled entry kind %r" % kind) def sorted_children(self): l = self.children.items() l.sort() return l def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.parent_id, text_id=self.text_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size != None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1']: v = getattr(self, f) if v != None: e.set(f, v) # to be conservative, we don't externalize the root pointers # for now, leaving them as null in the xml form. in a future # version it will be implied by nested elements. if self.parent_id != ROOT_ID: assert isinstance(self.parent_id, basestring) e.set('parent_id', self.parent_id) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' ## original format inventories don't have a parent_id for ## nodes in the root directory, but it's cleaner to use one ## internally. parent_id = elt.get('parent_id') if parent_id == None: parent_id = ROOT_ID self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __cmp__(self, other): if self is other: return 0 if not isinstance(other, InventoryEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.name, other.name) \ or cmp(self.text_sha1, other.text_sha1) \ or cmp(self.text_size, other.text_size) \ or cmp(self.text_id, other.text_id) \ or cmp(self.parent_id, other.parent_id) \ or cmp(self.kind, other.kind) class RootEntry(InventoryEntry): def __init__(self, file_id): self.file_id = file_id self.children = {} self.kind = 'root_directory' self.parent_id = None self.name = '' def __cmp__(self, other): if self is other: return 0 if not isinstance(other, RootEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.children, other.children) class Inventory(XMLMixin): """Inventory of versioned files in a tree. This describes which file_id is present at each point in the tree, and possibly the SHA-1 or other information about the file. Entries can be looked up either by path or by file_id. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted, other than through the Inventory API. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID)) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. The inventory is created with a default root directory, with an id of None. """ self.root = RootEntry(ROOT_ID) self._byid = {self.root.file_id: self.root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, from_dir=None): """Return (path, entry) pairs, in order by name.""" if from_dir == None: assert self.root from_dir = self.root elif isinstance(from_dir, basestring): from_dir = self._byid[from_dir] kids = from_dir.children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(from_dir=ie.file_id): yield os.path.join(name, cn), cie def directories(self): """Return (path, entry) pairs for all directories. """ def descend(parent_ie): parent_name = parent_ie.name yield parent_name, parent_ie # directory children in sorted order dn = [] for ie in parent_ie.children.itervalues(): if ie.kind == 'directory': dn.append((ie.name, ie)) dn.sort() for name, child_ie in dn: for sub_name, sub_ie in descend(child_ie): yield appendpath(parent_name, sub_name), sub_ie for name, ie in descend(self.root): yield name, ie def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID)) >>> inv['123123'].name 'hello.c' """ try: return self._byid[file_id] except KeyError: if file_id == None: raise BzrError("can't look up file_id None") else: raise BzrError("file_id {%s} not in inventory" % file_id) def get_child(self, parent_id, filename): return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self._byid: bailout("inventory already contains entry with id {%s}" % entry.file_id) try: parent = self._byid[entry.parent_id] except KeyError: bailout("parent_id {%s} not in inventory" % entry.parent_id) if parent.children.has_key(entry.name): bailout("%s is already versioned" % appendpath(self.id2path(parent.file_id), entry.name)) self._byid[entry.file_id] = entry parent.children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: bailout("cannot re-add root of inventory") if file_id == None: file_id = bzrlib.branch.gen_file_id(relpath) parent_id = self.path2id(parts[:-1]) assert parent_id != None ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def id_set(self): return Set(self._byid) def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID)) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __cmp__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 True """ if self is other: return 0 if not isinstance(other, Inventory): return NotImplemented if self.id_set() ^ other.id_set(): return 1 for file_id in self._byid: c = cmp(self[file_id], other[file_id]) if c: return c return 0 def get_idpath(self, file_id): """Return a list of file_ids for the path to an entry. The list contains one element for each directory followed by the id of the file itself. So the length of the returned list is equal to the depth of the file in the tree, counting the root directory as depth 1. """ p = [] while file_id != None: try: ie = self._byid[file_id] except KeyError: bailout("file_id {%s} not found in inventory" % file_id) p.insert(0, ie.file_id) file_id = ie.parent_id return p def id2path(self, file_id): """Return as a list the path to file_id.""" # get all names, skipping root p = [self[fid].name for fid in self.get_idpath(file_id)[1:]] return os.sep.join(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. Returns None iff the path is not found. """ if isinstance(name, types.StringTypes): name = splitpath(name) mutter("lookup path %r" % name) parent = self.root for f in name: try: cie = parent.children[f] assert cie.name == f assert cie.parent_id == parent.file_id parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): return self._byid.has_key(file_id) def rename(self, file_id, new_parent_id, new_name): """Move a file within the inventory. This can change either the name, or the parent, or both. This does not move the working file.""" if not is_valid_name(new_name): bailout("not an acceptable filename: %r" % new_name) new_parent = self._byid[new_parent_id] if new_name in new_parent.children: bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id))) new_parent_idpath = self.get_idpath(new_parent_id) if file_id in new_parent_idpath: bailout("cannot move directory %r into a subdirectory of itself, %r" % (self.id2path(file_id), self.id2path(new_parent_id))) file_ie = self._byid[file_id] old_parent = self._byid[file_ie.parent_id] # TODO: Don't leave things messed up if this fails del old_parent.children[file_ie.name] new_parent.children[new_name] = file_ie file_ie.name = new_name file_ie.parent_id = new_parent_id _NAME_RE = re.compile(r'^[^/\\]+$') def is_valid_name(name): return bool(_NAME_RE.match(name)) commit refs/heads/master mark :381 committer Martin Pool 1115286617 +1000 data 47 - Better message when a wrong argument is given from :380 M 644 inline bzrlib/commands.py data 27633 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass else: raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False): #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) def Relpath(Command): """Show path of a file relative to root""" takes_args = ('filename') def run(self): print Branch(self.args['filename']).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): b = Branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern""" takes_args = ['name_pattern'] def run(self, name_pattern): b = Branch('.') # XXX: This will fail if it's a hardlink; should use an AtomicFile class. f = open(b.abspath('.bzrignore'), 'at') f.write(name_pattern + '\n') f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them.""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revno=None): b = Branch('.') if revno == None: rh = b.revision_history[-1] else: rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, verbose=False): if not message: raise BzrCommandError("please specify a commit message") Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if isinstance(e, IOError) and e.errno == errno.EPIPE: quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :382 committer Martin Pool 1115286661 +1000 data 22 - test previous commit from :381 M 644 inline testbzr data 6499 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): logfile.write('$ %s\n' % cmd) cmd = cmd.split() else: logfile.write('$ %r\n' % cmd) return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open(LOGFILENAME, 'wt', buffering=1) try: runcmd(['mkdir', TESTDIR]) cd(TESTDIR) progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) runcmd("bzr diff --message foo", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == '''? test.txt\n''' out = backtick("bzr status --all") assert out == "? test.txt\n" progress("command aliases") out = backtick("bzr st --all") assert out == "? test.txt\n" out = backtick("bzr stat") assert out == "? test.txt\n" progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == "A test.txt\n" progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') cd('..') progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :383 committer Martin Pool 1115287037 +1000 data 38 - Show file ids too for log --show-ids from :382 M 644 inline bzrlib/log.py data 6319 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def find_touching_revisions(branch, file_id): """Yield a description of revisions which affect the file_id. Each returned element is (revno, revision_id, description) This is the list of revisions where the file is either added, modified, renamed or deleted. Revisions are returned in chronological order. TODO: Perhaps some way to limit this to only particular revisions, or to traverse a non-branch set of revisions? TODO: If a directory is given, then by default look for all changes under that directory. """ last_ie = None last_path = None revno = 1 for revision_id in branch.revision_history(): this_inv = branch.get_revision_inventory(revision_id) if file_id in this_inv: this_ie = this_inv[file_id] this_path = this_inv.id2path(file_id) else: this_ie = this_path = None # now we know how it was last time, and how it is in this revision. # are those two states effectively the same or not? if not this_ie and not last_ie: # not present in either pass elif this_ie and not last_ie: yield revno, revision_id, "added " + this_path elif not this_ie and last_ie: # deleted here yield revno, revision_id, "deleted " + last_path elif this_path != last_path: yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path)) elif (this_ie.text_size != last_ie.text_size or this_ie.text_sha1 != last_ie.text_sha1): yield revno, revision_id, "modified " + this_path last_ie = this_ie last_path = this_path revno += 1 def show_log(branch, filename=None, show_timezone='original', verbose=False, show_ids=False, to_file=None): """Write out human-readable log of commits to this branch. filename If true, list only the commits affecting the specified file, rather than all commits. show_timezone 'original' (committer's timezone), 'utc' (universal time), or 'local' (local user's timezone) verbose If true show added/changed/deleted/renamed files. show_ids If true, show revision and file ids. to_file File to send log to; by default stdout. """ from osutils import format_date from errors import BzrCheckError from diff import compare_inventories from textui import show_status from inventory import Inventory if to_file == None: import sys to_file = sys.stdout if filename: file_id = branch.read_working_inventory().path2id(filename) def which_revs(): for revno, revid, why in find_touching_revisions(branch, file_id): yield revno, revid else: def which_revs(): for i, revid in enumerate(branch.revision_history()): yield i+1, revid branch._need_readlock() precursor = None if verbose: prev_inv = Inventory() for revno, revision_id in which_revs(): print >>to_file, '-' * 60 print >>to_file, 'revno:', revno rev = branch.get_revision(revision_id) if show_ids: print >>to_file, 'revision-id:', revision_id print >>to_file, 'committer:', rev.committer print >>to_file, 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) if revision_id != rev.revision_id: raise BzrCheckError("retrieved wrong revision: %r" % (revision_id, rev.revision_id)) print >>to_file, 'message:' if not rev.message: print >>to_file, ' (no message)' else: for l in rev.message.split('\n'): print >>to_file, ' ' + l # Don't show a list of changed files if we were asked about # one specific file. if verbose and not filename: this_inv = branch.get_inventory(rev.inventory_id) delta = compare_inventories(prev_inv, this_inv) if delta.removed: print >>to_file, 'removed files:' for path, fid in delta.removed: if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if delta.added: print >>to_file, 'added files:' for path, fid in delta.added: if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ' + path if delta.renamed: print >>to_file, 'renamed files:' for oldpath, newpath, fid in delta.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if delta.modified: print >>to_file, 'modified files:' for path, fid in delta.modified: if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ' + path prev_inv = this_inv precursor = revision_id commit refs/heads/master mark :384 committer Martin Pool 1115287083 +1000 data 3 doc from :383 M 644 inline NEWS data 5444 bzr-0.0.5 NOT RELEASED YET ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New 'bzr ignore PATTERN' command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on mango-sorbet by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. commit refs/heads/master mark :385 committer Martin Pool 1115335683 +1000 data 32 - New Branch.enum_history method from :384 M 644 inline bzrlib/branch.py data 35327 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. base Base directory of the branch. """ _lockmode = None def __init__(self, base, init=False, find_root=True, lock_mode='w'): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.lock(lock_mode) self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def lock(self, mode='w'): """Lock the on-disk branch, excluding other processes.""" try: import fcntl, errno if mode == 'w': lm = fcntl.LOCK_EX om = os.O_WRONLY | os.O_CREAT elif mode == 'r': lm = fcntl.LOCK_SH om = os.O_RDONLY else: raise BzrError("invalid locking mode %r" % mode) try: lockfile = os.open(self.controlfilename('branch-lock'), om) except OSError, e: if e.errno == errno.ENOENT: # might not exist on branches from <0.0.4 self.controlfile('branch-lock', 'w').close() lockfile = os.open(self.controlfilename('branch-lock'), om) else: raise e fcntl.lockf(lockfile, lm) def unlock(): fcntl.lockf(lockfile, fcntl.LOCK_UN) os.close(lockfile) self._lockmode = None self.unlock = unlock self._lockmode = mode except ImportError: warning("please write a locking method for platform %r" % sys.platform) def unlock(): self._lockmode = None self.unlock = unlock self._lockmode = mode def _need_readlock(self): if self._lockmode not in ['r', 'w']: raise BzrError('need read lock on branch, only have %r' % self._lockmode) def _need_writelock(self): if self._lockmode not in ['w']: raise BzrError('need write lock on branch, only have %r' % self._lockmode) def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" self._need_readlock() before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self._need_writelock() ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ self._need_writelock() # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" self._need_readlock() tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability self._need_writelock() if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. """ self._need_writelock() ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) self.append_revision(rev_id) if verbose: note("commited r%d" % self.revno()) def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" self._need_readlock() r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" self._need_readlock() i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" self._need_readlock() if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self._need_readlock() return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise BzrError('invalid history direction %r' % direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] else: return None def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" self._need_readlock() if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self._need_writelock() tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self._need_writelock() ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(self, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo TODO: Get state for single files. """ self._need_readlock() # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = self.basis_tree() new = self.working_tree() for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("weird file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> del b >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): """Destroy the test branch, removing the scratch directory.""" try: shutil.rmtree(self.base) except OSError: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :386 committer Martin Pool 1115335707 +1000 data 24 - Typo (reported by uws) from :385 M 644 inline bzrlib/commands.py data 27639 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass else: raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False): #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ('filename') def run(self): print Branch(self.args['filename']).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): b = Branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern""" takes_args = ['name_pattern'] def run(self, name_pattern): b = Branch('.') # XXX: This will fail if it's a hardlink; should use an AtomicFile class. f = open(b.abspath('.bzrignore'), 'at') f.write(name_pattern + '\n') f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them.""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revno=None): b = Branch('.') if revno == None: rh = b.revision_history[-1] else: rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, verbose=False): if not message: raise BzrCommandError("please specify a commit message") Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if isinstance(e, IOError) and e.errno == errno.EPIPE: quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :387 committer Martin Pool 1115346894 +1000 data 44 - ignore any diff files lying around in tree from :386 M 644 inline .bzrignore data 117 *.diff ./doc/*.html *.py[oc] *~ .arch-ids .bzr.profile .arch-inventory {arch} CHANGELOG bzr-test.log ,,* testbzr.log commit refs/heads/master mark :388 committer Martin Pool 1115346953 +1000 data 3 doc from :387 M 644 inline TODO data 9040 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be ``bzr export -r REV``. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * ``bzr help commands`` should give a one-line summary of each command. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Read and write locks on branch while it's open. * Separate read and write version checks? * ``bzr status FILE...`` * Check all commands have decent help. * Autogenerate argument/option help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. * Split BzrError into various more specific subclasses for different errors people might want to catch. Medium things ------------- * Display command grammar in help messages rather than hardcoding it. * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still expose it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :389 committer Martin Pool 1115347744 +1000 data 27 - new commit --file option! from :388 M 644 inline NEWS data 5534 bzr-0.0.5 NOT RELEASED YET ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New 'bzr ignore PATTERN' command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 28100 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass else: raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False): #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ('filename') def run(self): print Branch(self.args['filename']).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): b = Branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern""" takes_args = ['name_pattern'] def run(self, name_pattern): b = Branch('.') # XXX: This will fail if it's a hardlink; should use an AtomicFile class. f = open(b.abspath('.bzrignore'), 'at') f.write(name_pattern + '\n') f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them.""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revno=None): b = Branch('.') if revno == None: rh = b.revision_history[-1] else: rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False): ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if isinstance(e, IOError) and e.errno == errno.EPIPE: quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline testbzr data 6701 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): logfile.write('$ %s\n' % cmd) cmd = cmd.split() else: logfile.write('$ %r\n' % cmd) return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open(LOGFILENAME, 'wt', buffering=1) try: runcmd(['mkdir', TESTDIR]) cd(TESTDIR) progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) runcmd("bzr diff --message foo", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == '''? test.txt\n''' out = backtick("bzr status --all") assert out == "? test.txt\n" progress("command aliases") out = backtick("bzr st --all") assert out == "? test.txt\n" out = backtick("bzr stat") assert out == "? test.txt\n" progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == "A test.txt\n" progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') cd('..') progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :390 committer Martin Pool 1115349206 +1000 data 95 - Update revfile docs; most of what's in there is speculative about storage with annotations. from :389 M 644 inline doc/revfile.txt data 11038 ******** Revfiles ******** The unit for compressed storage in bzr is a *revfile*, whose design was suggested by Matt Mackall. This document describes version 1 of the file, and has some notes on what might be done in version 2. Requirements ============ Compressed storage is a tradeoff between several goals: * Reasonably compact storage of long histories. * Robustness and simplicity. * Fast extraction of versions and addition of new versions (preferably without rewriting the whole file, or reading the whole history.) * Fast and precise annotations. * Storage of files of at least a few hundred MB. * Lossless in useful ways: we can extract a series of texts and write them back out without losing any information. Design ====== revfiles store the history of a single logical file, which is identified in bzr by its file-id. In this sense they are similar to an RCS or CVS ``,v`` file or an SCCS sfile. Each state of the file is called a *text*. Renaming, adding and deleting this file is handled at a higher level by the inventory system, and is outside the scope of the revfile. The revfile name is typically based on the file id which is itself typically based on the name the file had when it was first added. But this is purely cosmetic. For example a file now called ``frob.c`` may have the id ``frobber.c-12873`` because it was originally called ``frobber.c``. Its texts are kept in the revfile ``.bzr/revfiles/frobber.c-12873.revs``. When the file is deleted from the inventory the revfile does not change. It's just not used in reproducing trees from that point onwards. The revfile does not record the date when the text was added, a commit message, properties, or any other metadata. That is handled in the higher-level revision history. Inventories and other metadata files that vary from one version to the next can themselves be stored in revfiles. revfiles store files as simple byte streams, with no consideration of translating character sets, line endings, or keywords. Those are also handled at a higher level. However, the revfile may make use of knowledge that a file is line-based in generating a diff. (The Python builtin difflib is too slow when generating a purely byte-by-byte delta so we always make a line-by-line diff; when this is fixed it may be feasible to use line-by-line diffs for all files.) Files whose text does not change from one revision to the next are stored as just a single text in the revfile. This can happen even if the file was renamed or other properties were changed in the inventory. The revfile is held on disk as two files: an *index* and a *data* file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. This design is similar to that of Netscape `mail summary files`_, in that there is a small index which can always be read into memory and that quickly identifies where to look in the main file. They differ in many other ways though, most particularly that the index is not just a cache but holds precious data in its own right. .. _`mail summary files`: http://www.jwz.org/doc/mailsum.html This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. On the other hand that would compromise the append-only indexing, and 100,000 revs is a fairly extreme case. This performs pretty well except when trying to calculate deltas of really large files. For that the main thing would be to plug in something faster than difflib, which is after all pure Python. Another approach is to just store the gzipped full text of big files, though perhaps that's too perverse? Identifying texts ----------------- In the current version, texts are identified by their SHA-1. Skip-deltas ----------- Because the basis of a delta does not need to be the text's logical predecessor, we can adjust the deltas to avoid ever needing to apply too many deltas to reproduce a particular file. Tools ----- The revfile module can be invoked as a program to give low-level access for data recovery, debugging, etc. Extension to store annotations ============================== We might extend the revfile format in a future version to also store annotations. *This is not implemented yet.* In previous versions, the index file identified texts by their SHA-1 digest. This was unsatisfying for two reasons. Firstly it assumes that SHA-1 will not collide, which is not an assumption we wish to make in long-lived files. Secondly for annotations we need to be able to map from file versions back to a revision. Texts are identified by the name of the revfile and a UUID corresponding to the first revision in which they were first introduced. This means that given a text we can identify which revision it belongs to, and annotations can use the index within the revfile to identify where a region was first introduced. We cannot identify texts by the integer revision number, because that would limit us to only referring to a file in a particular branch. I'd like to just use the revision-id, but those are variable-length strings, and I'd like the revfile index to be fixed-length and relatively short. UUIDs can be encoded in binary as only 16 bytes. Perhaps we should just use UUIDs for revisions and be done? Annotations ----------- Annotations indicate which revision of a file first inserted a line (or region of bytes). Given a string, we can write annotations on it like so: a sequence of *(index, length)* pairs, giving the *index* of the revision which introduced the next run of *length* bytes. The sum of the lengths must equal the length of the string. For text files the regions will typically fall on line breaks. This can be transformed in memory to other structures, such as a list of *(index, content)* pairs. When a line was inserted from a merge revision then the annotation for that line should still be the source in the merged branch, rather than just being the revision in which the merge took place. They can cheaply be calculated when inserting a new text, but are expensive to calculate after the fact because that requires searching back through all previous text and all texts which were merged in. It therefore seems sensible to calculate them once and store them. To do this we need two operators which update an existing annotated file: A. Given an annotated file and a working text, update the annotation to mark regions inserted in the working file as new in this revision. B. Given two annotated files, merge them to produce an annotated result. When there are conflicts, both texts should be included and annotated. These may be repeated: after a merge there may be another merge, or there may be manual fixups or conflict resolutions. So what we require is given a diff or a diff3 between two files, map the regions of bytes changed into corresponding updates to the origin annotations. Annotations can also be delta-compressed; we only need to add new annotation data when there is a text insertion. (It is possible in a merge to have a change of annotation when there is no text change, though this seems unlikely. This can still be represented as a "pointless" delta, plus an update to the annotations.) Index file ---------- In a proposed (not implemented) storage with annotations, the index file is a series of fixed-length records:: byte[16] UUID of revision byte[20] SHA-1 of expanded text (as binary, not hex) uint32 flags: 1=zlib compressed uint32 sequence number this is based on, or -1 for full text uint32 offset in text file of start uint32 length of compressed delta in text file uint32[3] reserved Total 64 bytes. The header is also 64 bytes, for tidyness and easy calculation. For this format the header must be ``bzr revfile v2\n`` padded with ``\xff`` to 64 bytes. The first record after the header is index 0. A record's base index must be less than its own index. The SHA-1 is redundant with the inventory but stored just as a check on the compression methods and so that the file can be validated without reference to any other information. Each byte in the text file should be included by at most one delta. Deltas ------ In a proposed (not implemented) storage with annotations, deltas to the text are stored as a series of variable-length records:: uint32 idx uint32 m uint32 n uint32 l byte[l] new This describes a change originally introduced in the revision described by *idx* in the index. This indicates that the region [m:n] of the input file should be replaced by the text *new*. If m==n this is a pure insertion of l bytes. If l==0 this is a pure deletion of (n-m) bytes. Open issues =========== * revfiles use unsigned 32-bit integers both in diffs and the index. This should be more than enough for any reasonable source file but perhaps not enough for large binaries that are frequently committed. Perhaps for those files there should be an option to continue to use the text-store. There is unlikely to be any benefit in holding deltas between them, and deltas will anyhow be hard to calculate. * The append-only design does not allow for destroying committed data, as when confidential information is accidentally added. That could be fixed by creating the fixed repository as a separate branch, into which only the preserved revisions are exported. * Should annotations also indicate where text was deleted? * This design calls for only one annotation per line, which seems standard. However, this is lacking in at least two cases: - Lines which originate in the same way in more than one revision, through being independently introduced. In this case we would apparently have to make an arbitrary choice; I suppose branches could prefer to assume lines originated in their own history. - It might be useful to directly indicate which mergers included which lines. We do have that information in the revision history though, so there seems no need to store it for every line. * Should we also store full-texts as a transitional step? * Storing the annotations with the text is reasonably simple and compact, but means that we always need to process the annotation structure even when we only want the text. In particular it means that full-texts cannot just simply be copied out but rather composed from chunks. That seems inefficient since it is probably common to only want the text. commit refs/heads/master mark :391 committer Martin Pool 1115349615 +1000 data 52 - split out notes on storing annotations in revfiles from :390 M 644 inline doc/revfile-annotation.txt data 5797 ============================== Extension to store annotations ============================== We might extend the revfile format in a future version to also store annotations. *This is not implemented yet.* In previous versions, the index file identified texts by their SHA-1 digest. This was unsatisfying for two reasons. Firstly it assumes that SHA-1 will not collide, which is not an assumption we wish to make in long-lived files. Secondly for annotations we need to be able to map from file versions back to a revision. Texts are identified by the name of the revfile and a UUID corresponding to the first revision in which they were first introduced. This means that given a text we can identify which revision it belongs to, and annotations can use the index within the revfile to identify where a region was first introduced. We cannot identify texts by the integer revision number, because that would limit us to only referring to a file in a particular branch. I'd like to just use the revision-id, but those are variable-length strings, and I'd like the revfile index to be fixed-length and relatively short. UUIDs can be encoded in binary as only 16 bytes. Perhaps we should just use UUIDs for revisions and be done? Annotations ----------- Annotations indicate which revision of a file first inserted a line (or region of bytes). Given a string, we can write annotations on it like so: a sequence of *(index, length)* pairs, giving the *index* of the revision which introduced the next run of *length* bytes. The sum of the lengths must equal the length of the string. For text files the regions will typically fall on line breaks. This can be transformed in memory to other structures, such as a list of *(index, content)* pairs. When a line was inserted from a merge revision then the annotation for that line should still be the source in the merged branch, rather than just being the revision in which the merge took place. They can cheaply be calculated when inserting a new text, but are expensive to calculate after the fact because that requires searching back through all previous text and all texts which were merged in. It therefore seems sensible to calculate them once and store them. To do this we need two operators which update an existing annotated file: A. Given an annotated file and a working text, update the annotation to mark regions inserted in the working file as new in this revision. B. Given two annotated files, merge them to produce an annotated result. When there are conflicts, both texts should be included and annotated. These may be repeated: after a merge there may be another merge, or there may be manual fixups or conflict resolutions. So what we require is given a diff or a diff3 between two files, map the regions of bytes changed into corresponding updates to the origin annotations. Annotations can also be delta-compressed; we only need to add new annotation data when there is a text insertion. (It is possible in a merge to have a change of annotation when there is no text change, though this seems unlikely. This can still be represented as a "pointless" delta, plus an update to the annotations.) Index file ---------- In a proposed (not implemented) storage with annotations, the index file is a series of fixed-length records:: byte[16] UUID of revision byte[20] SHA-1 of expanded text (as binary, not hex) uint32 flags: 1=zlib compressed uint32 sequence number this is based on, or -1 for full text uint32 offset in text file of start uint32 length of compressed delta in text file uint32[3] reserved Total 64 bytes. The header is also 64 bytes, for tidyness and easy calculation. For this format the header must be ``bzr revfile v2\n`` padded with ``\xff`` to 64 bytes. The first record after the header is index 0. A record's base index must be less than its own index. The SHA-1 is redundant with the inventory but stored just as a check on the compression methods and so that the file can be validated without reference to any other information. Each byte in the text file should be included by at most one delta. Deltas ------ In a proposed (not implemented) storage with annotations, deltas to the text are stored as a series of variable-length records:: uint32 idx uint32 m uint32 n uint32 l byte[l] new This describes a change originally introduced in the revision described by *idx* in the index. This indicates that the region [m:n] of the input file should be replaced by the text *new*. If m==n this is a pure insertion of l bytes. If l==0 this is a pure deletion of (n-m) bytes. Open issues ----------- * Storing the annotations with the text is reasonably simple and compact, but means that we always need to process the annotation structure even when we only want the text. In particular it means that full-texts cannot just simply be copied out but rather composed from chunks. That seems inefficient since it is probably common to only want the text. * Should annotations also indicate where text was deleted? * This design calls for only one annotation per line, which seems standard. However, this is lacking in at least two cases: - Lines which originate in the same way in more than one revision, through being independently introduced. In this case we would apparently have to make an arbitrary choice; I suppose branches could prefer to assume lines originated in their own history. - It might be useful to directly indicate which mergers included which lines. We do have that information in the revision history though, so there seems no need to store it for every line. M 644 inline doc/index.txt data 5522 Bazaar-NG ********* .. These documents are formatted as ReStructuredText. You can .. .. convert them to HTML, PDF, etc using the ``python-docutils`` .. .. package. .. *Bazaar-NG* (``bzr``) is a project of `Canonical Ltd`__ to develop an open source distributed version control system that is powerful, friendly, and scalable. The project is at an early stage of development. __ http://canonical.com/ **Note:** These documents are in a very preliminary state, and so may be internally or externally inconsistent or redundant. Comments are still very welcome. Please send them to . For more information, see the homepage at http://bazaar-ng.org/ User documentation ------------------ * `Project overview/introduction `__ * `Command reference `__ -- intended to be user documentation, and gives the best overview at the moment of what the system will feel like to use. Fairly complete. Requirements and general design ------------------------------- * `Various purposes of a VCS `__ -- taking snapshots and helping with merges is not the whole story. * `Requirements `__ * `Costs `__ of various factors: time, disk, network, etc. * `Deadly sins `__ that gcc maintainers suggest we avoid. * `Overview of the whole design `__ and miscellaneous small design points. * `File formats `__ * `Random observations `__ that don't fit anywhere else yet. Design of particular features ----------------------------- * `Automatic generation of ChangeLogs `__ * `Cherry picking `__ -- merge just selected non-contiguous changes from a branch. * `Common changeset format `__ for interchange format between VCS. * `Compression `__ of file text for more efficient storage. * `Config specs `__ assemble a tree from several places. * `Conflicts `_ that can occur during merge-like operations. * `Ignored files `__ * `Recovering from interrupted operations `__ * `Inventory command `__ * `Branch joins `__ represent that all the changes from one branch are integrated into another. * `Kill a version `__ to fix a broken commit or wrong message, or to remove confidential information from the history. * `Hash collisions `__ and weaknesses, and the security implications thereof. * `Layers `__ within the design * `Library interface `__ for Python. * `Merge `__ * `Mirroring `__ * `Optional edit command `__: sometimes people want to make the working copy read-only, or not present at all. * `Partial commits `__ * `Patch pools `__ to efficiently store related branches. * `Revfiles `__ store the text history of files. * `Revfiles storing annotations `__ * `Revision syntax `__ -- ``hello.c@12``, etc. * `Roll-up commits `__ -- a single revision incorporates the changes from several others. * `Scalability `__ * `Security `__ * `Shared branches `__ maintained by more than one person * `Supportability `__ -- how to handle any bugs or problems in the field. * `Place tags on revisions for easy reference `__ * `Detecting unchanged files `__ * `Merging previously-unrelated branches `__ * `Usability principles `__ (very small at the moment) * ``__ * ``__ * ``__ Modelling/controlling flow of patches. * ``__ -- Discussion of using YAML_ as a storage or transmission format. .. _YAML: http://www.yaml.org/ Comparisons to other systems ---------------------------- * `Taxonomy `__: basic questions a VCS must answer. * `Bitkeeper `__, the proprietary system used by some kernel developers. * `Aegis `__, a tool focussed on enforcing process and workflow. * `Codeville `__ has an intruiging but scarcely-documented merge algorithm. * `CVSNT `__, with more Windows support and some merge enhancements. * `OpenCM `__, another hash-based tool with a good whitepaper. * `PRCS `__, a non-distributed inventory-based tool. * `GNU Arch `__, with many pros and cons. * `Darcs `__, a merge-focussed tool with good usability. * `Quilt `__ -- Andrew Morton's patch scripts, popular with kernel maintainers. * `Monotone `__, Graydon Hoare's hash-based distributed system. * `SVK `__ -- distributed operation stacked on Subversion. * `Sun Teamware `__ Project management and organization ----------------------------------- * `Notes on how to get a VCS adopted `__ * `Thanks `__ to various people * `Extra commands `__ for internal/developer/debugger use. * `Choice of Python as a development language `__ M 644 inline doc/revfile.txt data 5298 ******** Revfiles ******** The unit for compressed storage in bzr is a *revfile*, whose design was suggested by Matt Mackall. This document describes version 1 of the file, and has some notes on what might be done in version 2. Requirements ============ Compressed storage is a tradeoff between several goals: * Reasonably compact storage of long histories. * Robustness and simplicity. * Fast extraction of versions and addition of new versions (preferably without rewriting the whole file, or reading the whole history.) * Fast and precise annotations. * Storage of files of at least a few hundred MB. * Lossless in useful ways: we can extract a series of texts and write them back out without losing any information. Design ====== revfiles store the history of a single logical file, which is identified in bzr by its file-id. In this sense they are similar to an RCS or CVS ``,v`` file or an SCCS sfile. Each state of the file is called a *text*. Renaming, adding and deleting this file is handled at a higher level by the inventory system, and is outside the scope of the revfile. The revfile name is typically based on the file id which is itself typically based on the name the file had when it was first added. But this is purely cosmetic. For example a file now called ``frob.c`` may have the id ``frobber.c-12873`` because it was originally called ``frobber.c``. Its texts are kept in the revfile ``.bzr/revfiles/frobber.c-12873.revs``. When the file is deleted from the inventory the revfile does not change. It's just not used in reproducing trees from that point onwards. The revfile does not record the date when the text was added, a commit message, properties, or any other metadata. That is handled in the higher-level revision history. Inventories and other metadata files that vary from one version to the next can themselves be stored in revfiles. revfiles store files as simple byte streams, with no consideration of translating character sets, line endings, or keywords. Those are also handled at a higher level. However, the revfile may make use of knowledge that a file is line-based in generating a diff. (The Python builtin difflib is too slow when generating a purely byte-by-byte delta so we always make a line-by-line diff; when this is fixed it may be feasible to use line-by-line diffs for all files.) Files whose text does not change from one revision to the next are stored as just a single text in the revfile. This can happen even if the file was renamed or other properties were changed in the inventory. The revfile is held on disk as two files: an *index* and a *data* file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. This design is similar to that of Netscape `mail summary files`_, in that there is a small index which can always be read into memory and that quickly identifies where to look in the main file. They differ in many other ways though, most particularly that the index is not just a cache but holds precious data in its own right. .. _`mail summary files`: http://www.jwz.org/doc/mailsum.html This is meant to scale to hold 100,000 revisions of a single file, by which time the index file will be ~4.8MB and a bit big to read sequentially. Some of the reserved fields could be used to implement a (semi?) balanced tree indexed by SHA1 so we can much more efficiently find the index associated with a particular hash. For 100,000 revs we would be able to find it in about 17 random reads, which is not too bad. On the other hand that would compromise the append-only indexing, and 100,000 revs is a fairly extreme case. This performs pretty well except when trying to calculate deltas of really large files. For that the main thing would be to plug in something faster than difflib, which is after all pure Python. Another approach is to just store the gzipped full text of big files, though perhaps that's too perverse? Identifying texts ----------------- In the current version, texts are identified by their SHA-1. Skip-deltas ----------- Because the basis of a delta does not need to be the text's logical predecessor, we can adjust the deltas to avoid ever needing to apply too many deltas to reproduce a particular file. Tools ----- The revfile module can be invoked as a program to give low-level access for data recovery, debugging, etc. Open issues =========== * revfiles use unsigned 32-bit integers both in diffs and the index. This should be more than enough for any reasonable source file but perhaps not enough for large binaries that are frequently committed. Perhaps for those files there should be an option to continue to use the text-store. There is unlikely to be any benefit in holding deltas between them, and deltas will anyhow be hard to calculate. * The append-only design does not allow for destroying committed data, as when confidential information is accidentally added. That could be fixed by creating the fixed repository as a separate branch, into which only the preserved revisions are exported. * Should we also store full-texts as a transitional step? commit refs/heads/master mark :392 committer Martin Pool 1115351117 +1000 data 27 - fix relpath and add tests from :391 M 644 inline bzrlib/commands.py data 28097 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass else: raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False): #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): b = Branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern""" takes_args = ['name_pattern'] def run(self, name_pattern): b = Branch('.') # XXX: This will fail if it's a hardlink; should use an AtomicFile class. f = open(b.abspath('.bzrignore'), 'at') f.write(name_pattern + '\n') f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them.""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revno=None): b = Branch('.') if revno == None: rh = b.revision_history[-1] else: rh = b.lookup_revision(int(revno)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False): ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if isinstance(e, IOError) and e.errno == errno.EPIPE: quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline testbzr data 7010 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): logfile.write('$ %s\n' % cmd) cmd = cmd.split() else: logfile.write('$ %r\n' % cmd) return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open(LOGFILENAME, 'wt', buffering=1) try: runcmd(['mkdir', TESTDIR]) cd(TESTDIR) progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) runcmd("bzr diff --message foo", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == '''? test.txt\n''' out = backtick("bzr status --all") assert out == "? test.txt\n" progress("command aliases") out = backtick("bzr st --all") assert out == "? test.txt\n" out = backtick("bzr stat") assert out == "? test.txt\n" progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == "A test.txt\n" progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == "sub2/hello.txt\n" assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == 'sub1/sub2/hello.txt\n' assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') assert backtick('bzr relpath sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') cd('..') progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :393 committer Martin Pool 1115511709 +1000 data 23 todo: export to tarball from :392 M 644 inline TODO data 9280 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be ``bzr export -r REV``. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * ``bzr help commands`` should give a one-line summary of each command. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Read and write locks on branch while it's open. * Separate read and write version checks? * ``bzr status FILE...`` * Check all commands have decent help. * Autogenerate argument/option help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html Medium things ------------- * Display command grammar in help messages rather than hardcoding it. * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still expose it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :394 committer Martin Pool 1115523237 +1000 data 81 - Fix argument handling in export command - Add test Thanks to Aaron and Wilk from :393 M 644 inline TODO data 9474 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be ``bzr export -r REV``. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * ``bzr help commands`` should give a one-line summary of each command. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Read and write locks on branch while it's open. * Separate read and write version checks? * ``bzr status FILE...`` * Check all commands have decent help. * Autogenerate argument/option help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * testbzr should by default test the bzr binary in the same directory as the testbzr script, or take a path to it as a first parameter. Should show the version from bzr and the path name. Medium things ------------- * Display command grammar in help messages rather than hardcoding it. * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still expose it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` M 644 inline bzrlib/commands.py data 28108 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass else: raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False): #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): b = Branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern""" takes_args = ['name_pattern'] def run(self, name_pattern): b = Branch('.') # XXX: This will fail if it's a hardlink; should use an AtomicFile class. f = open(b.abspath('.bzrignore'), 'at') f.write(name_pattern + '\n') f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them.""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False): ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if isinstance(e, IOError) and e.errno == errno.EPIPE: quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline testbzr data 7132 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): logfile.write('$ %s\n' % cmd) cmd = cmd.split() else: logfile.write('$ %r\n' % cmd) return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open(LOGFILENAME, 'wt', buffering=1) try: runcmd(['mkdir', TESTDIR]) cd(TESTDIR) progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) runcmd("bzr diff --message foo", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == '''? test.txt\n''' out = backtick("bzr status --all") assert out == "? test.txt\n" progress("command aliases") out = backtick("bzr st --all") assert out == "? test.txt\n" out = backtick("bzr stat") assert out == "? test.txt\n" progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == "A test.txt\n" progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == "sub2/hello.txt\n" assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == 'sub1/sub2/hello.txt\n' assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') assert backtick('bzr relpath sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') cd('..') progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :395 committer Martin Pool 1115593905 +1000 data 51 - fix error raised from invalid InventoryEntry name from :394 M 644 inline TODO data 9476 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be ``bzr export -r REV``. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * ``bzr help commands`` should give a one-line summary of each command. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Read and write locks on branch while it's open. * Separate read and write version checks? * ``bzr status FILE...`` * Check all commands have decent help. * Autogenerate argument/option help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * testbzr should by default test the bzr binary in the same directory as the testbzr script, or take a path to it as a first parameter. Should show the version from bzr and the path name. Medium things ------------- * Display command grammar in help messages rather than hardcoding it. * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still expose it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` M 644 inline bzrlib/inventory.py data 18613 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # This should really be an id randomly assigned when the tree is # created, but it's not for now. ROOT_ID = "TREE_ROOT" import sys, os.path, types, re from sets import Set try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from xml import XMLMixin from errors import bailout, BzrError, BzrCheckError import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') 'TREE_ROOT' >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID)) >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123')) >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' TODO: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ # TODO: split InventoryEntry into subclasses for files, # directories, etc etc. text_sha1 = None text_size = None def __init__(self, file_id, name, kind, parent_id, text_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID) >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID) Traceback (most recent call last): BzrCheckError: InventoryEntry name 'src/hello.c' is invalid """ if '/' in name or '\\' in name: raise BzrCheckError('InventoryEntry name %r is invalid' % name) self.file_id = file_id self.name = name self.kind = kind self.text_id = text_id self.parent_id = parent_id if kind == 'directory': self.children = {} elif kind == 'file': pass else: raise BzrError("unhandled entry kind %r" % kind) def sorted_children(self): l = self.children.items() l.sort() return l def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.parent_id, text_id=self.text_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size != None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1']: v = getattr(self, f) if v != None: e.set(f, v) # to be conservative, we don't externalize the root pointers # for now, leaving them as null in the xml form. in a future # version it will be implied by nested elements. if self.parent_id != ROOT_ID: assert isinstance(self.parent_id, basestring) e.set('parent_id', self.parent_id) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' ## original format inventories don't have a parent_id for ## nodes in the root directory, but it's cleaner to use one ## internally. parent_id = elt.get('parent_id') if parent_id == None: parent_id = ROOT_ID self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __cmp__(self, other): if self is other: return 0 if not isinstance(other, InventoryEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.name, other.name) \ or cmp(self.text_sha1, other.text_sha1) \ or cmp(self.text_size, other.text_size) \ or cmp(self.text_id, other.text_id) \ or cmp(self.parent_id, other.parent_id) \ or cmp(self.kind, other.kind) class RootEntry(InventoryEntry): def __init__(self, file_id): self.file_id = file_id self.children = {} self.kind = 'root_directory' self.parent_id = None self.name = '' def __cmp__(self, other): if self is other: return 0 if not isinstance(other, RootEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.children, other.children) class Inventory(XMLMixin): """Inventory of versioned files in a tree. This describes which file_id is present at each point in the tree, and possibly the SHA-1 or other information about the file. Entries can be looked up either by path or by file_id. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted, other than through the Inventory API. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID)) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. The inventory is created with a default root directory, with an id of None. """ self.root = RootEntry(ROOT_ID) self._byid = {self.root.file_id: self.root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, from_dir=None): """Return (path, entry) pairs, in order by name.""" if from_dir == None: assert self.root from_dir = self.root elif isinstance(from_dir, basestring): from_dir = self._byid[from_dir] kids = from_dir.children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(from_dir=ie.file_id): yield os.path.join(name, cn), cie def directories(self): """Return (path, entry) pairs for all directories. """ def descend(parent_ie): parent_name = parent_ie.name yield parent_name, parent_ie # directory children in sorted order dn = [] for ie in parent_ie.children.itervalues(): if ie.kind == 'directory': dn.append((ie.name, ie)) dn.sort() for name, child_ie in dn: for sub_name, sub_ie in descend(child_ie): yield appendpath(parent_name, sub_name), sub_ie for name, ie in descend(self.root): yield name, ie def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID)) >>> inv['123123'].name 'hello.c' """ try: return self._byid[file_id] except KeyError: if file_id == None: raise BzrError("can't look up file_id None") else: raise BzrError("file_id {%s} not in inventory" % file_id) def get_child(self, parent_id, filename): return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self._byid: bailout("inventory already contains entry with id {%s}" % entry.file_id) try: parent = self._byid[entry.parent_id] except KeyError: bailout("parent_id {%s} not in inventory" % entry.parent_id) if parent.children.has_key(entry.name): bailout("%s is already versioned" % appendpath(self.id2path(parent.file_id), entry.name)) self._byid[entry.file_id] = entry parent.children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: bailout("cannot re-add root of inventory") if file_id == None: file_id = bzrlib.branch.gen_file_id(relpath) parent_id = self.path2id(parts[:-1]) assert parent_id != None ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def id_set(self): return Set(self._byid) def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID)) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __cmp__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 True """ if self is other: return 0 if not isinstance(other, Inventory): return NotImplemented if self.id_set() ^ other.id_set(): return 1 for file_id in self._byid: c = cmp(self[file_id], other[file_id]) if c: return c return 0 def get_idpath(self, file_id): """Return a list of file_ids for the path to an entry. The list contains one element for each directory followed by the id of the file itself. So the length of the returned list is equal to the depth of the file in the tree, counting the root directory as depth 1. """ p = [] while file_id != None: try: ie = self._byid[file_id] except KeyError: bailout("file_id {%s} not found in inventory" % file_id) p.insert(0, ie.file_id) file_id = ie.parent_id return p def id2path(self, file_id): """Return as a list the path to file_id.""" # get all names, skipping root p = [self[fid].name for fid in self.get_idpath(file_id)[1:]] return os.sep.join(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. Returns None iff the path is not found. """ if isinstance(name, types.StringTypes): name = splitpath(name) mutter("lookup path %r" % name) parent = self.root for f in name: try: cie = parent.children[f] assert cie.name == f assert cie.parent_id == parent.file_id parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): return self._byid.has_key(file_id) def rename(self, file_id, new_parent_id, new_name): """Move a file within the inventory. This can change either the name, or the parent, or both. This does not move the working file.""" if not is_valid_name(new_name): bailout("not an acceptable filename: %r" % new_name) new_parent = self._byid[new_parent_id] if new_name in new_parent.children: bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id))) new_parent_idpath = self.get_idpath(new_parent_id) if file_id in new_parent_idpath: bailout("cannot move directory %r into a subdirectory of itself, %r" % (self.id2path(file_id), self.id2path(new_parent_id))) file_ie = self._byid[file_id] old_parent = self._byid[file_ie.parent_id] # TODO: Don't leave things messed up if this fails del old_parent.children[file_ie.name] new_parent.children[new_name] = file_ie file_ie.name = new_name file_ie.parent_id = new_parent_id _NAME_RE = re.compile(r'^[^/\\]+$') def is_valid_name(name): return bool(_NAME_RE.match(name)) commit refs/heads/master mark :396 committer Martin Pool 1115594380 +1000 data 103 - Using the destructor on a ScratchBranch is not reliable; so now there's a destroy() method as well. from :395 M 644 inline bzrlib/branch.py data 35538 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. base Base directory of the branch. """ _lockmode = None def __init__(self, base, init=False, find_root=True, lock_mode='w'): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.lock(lock_mode) self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def lock(self, mode='w'): """Lock the on-disk branch, excluding other processes.""" try: import fcntl, errno if mode == 'w': lm = fcntl.LOCK_EX om = os.O_WRONLY | os.O_CREAT elif mode == 'r': lm = fcntl.LOCK_SH om = os.O_RDONLY else: raise BzrError("invalid locking mode %r" % mode) try: lockfile = os.open(self.controlfilename('branch-lock'), om) except OSError, e: if e.errno == errno.ENOENT: # might not exist on branches from <0.0.4 self.controlfile('branch-lock', 'w').close() lockfile = os.open(self.controlfilename('branch-lock'), om) else: raise e fcntl.lockf(lockfile, lm) def unlock(): fcntl.lockf(lockfile, fcntl.LOCK_UN) os.close(lockfile) self._lockmode = None self.unlock = unlock self._lockmode = mode except ImportError: warning("please write a locking method for platform %r" % sys.platform) def unlock(): self._lockmode = None self.unlock = unlock self._lockmode = mode def _need_readlock(self): if self._lockmode not in ['r', 'w']: raise BzrError('need read lock on branch, only have %r' % self._lockmode) def _need_writelock(self): if self._lockmode not in ['w']: raise BzrError('need write lock on branch, only have %r' % self._lockmode) def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" self._need_readlock() before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self._need_writelock() ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ self._need_writelock() # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" self._need_readlock() tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability self._need_writelock() if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. """ self._need_writelock() ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) self.append_revision(rev_id) if verbose: note("commited r%d" % self.revno()) def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" self._need_readlock() r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" self._need_readlock() i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" self._need_readlock() if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self._need_readlock() return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise BzrError('invalid history direction %r' % direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] else: return None def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" self._need_readlock() if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self._need_writelock() tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self._need_writelock() ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(self, show_all=False): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo TODO: Get state for single files. """ self._need_readlock() # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = self.basis_tree() new = self.working_tree() for fs, fid, oldname, newname, kind in diff_trees(old, new): if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("weird file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :397 committer Martin Pool 1115596823 +1000 data 48 - open_tracefile takes a tracefilename parameter from :396 M 644 inline NEWS data 5598 bzr-0.0.5 NOT RELEASED YET ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New 'bzr ignore PATTERN' command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/trace.py data 3833 # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " """Messages and logging for bazaar-ng Nothing is actually logged unless you call bzrlib.open_tracefile(). """ import sys, os ###################################################################### # messages and logging global _tracefile, _starttime _tracefile = None # used to have % (os.environ['USER'], time.time(), os.getpid()), 'w') _starttime = None # If false, notes also go to stdout; should replace this with --silent # at some point. silent = False # fix this if we ever fork within python _mypid = os.getpid() _logprefix = '[%d] ' % _mypid def _write_trace(msg): if _tracefile: _tracefile.write(_logprefix + msg + '\n') def warning(msg): sys.stderr.write('bzr: warning: ' + msg + '\n') _write_trace('warning: ' + msg) mutter = _write_trace def note(msg): b = '* ' + str(msg) + '\n' if not silent: sys.stderr.write(b) _write_trace('note: ' + msg) def log_error(msg): sys.stderr.write(msg + '\n') _write_trace(msg) def _rollover_trace_maybe(trace_fname): import stat try: size = os.stat(trace_fname)[stat.ST_SIZE] if size <= 4 << 20: return old_fname = trace_fname + '.old' try: # must remove before rename on windows os.remove(old_fname) except OSError: pass try: # might fail if in use on windows os.rename(trace_fname, old_fname) except OSError: pass except OSError: return def open_tracefile(argv=[], tracefilename='~/.bzr.log'): # Messages are always written to here, so that we have some # information if something goes wrong. In a future version this # file will be removed on successful completion. global _starttime, _tracefile import stat, codecs _starttime = os.times()[4] trace_fname = os.path.join(os.path.expanduser(tracefilename)) _rollover_trace_maybe(trace_fname) # buffering=1 means line buffered _tracefile = codecs.open(trace_fname, 'at', 'utf8', buffering=1) t = _tracefile if os.fstat(t.fileno())[stat.ST_SIZE] == 0: t.write("\nthis is a debug log for diagnosing/reporting problems in bzr\n") t.write("you can delete or truncate this file, or include sections in\n") t.write("bug reports to bazaar-ng@lists.canonical.com\n\n") import bzrlib _write_trace('bzr %s invoked on python %s (%s)' % (bzrlib.__version__, '.'.join(map(str, sys.version_info)), sys.platform)) _write_trace(' arguments: %r' % argv) _write_trace(' working dir: ' + os.getcwdu()) def close_trace(): times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum, %.3f elapsed" % (times[:4] + ((times[4] - _starttime),))) def log_exception(): """Log the last exception into the trace file.""" import cgitb s = cgitb.text(sys.exc_info()) for l in s.split('\n'): _write_trace(l) commit refs/heads/master mark :398 committer Martin Pool 1115601528 +1000 data 48 - testbzr finds the right version of bzr to test from :397 M 644 inline NEWS data 5738 bzr-0.0.5 NOT RELEASED YET ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New 'bzr ignore PATTERN' command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline testbzr data 7504 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): logfile.write('$ %s\n' % cmd) cmd = cmd.split() else: logfile.write('$ %r\n' % cmd) if cmd[0] == 'bzr': cmd[0] = BZRPATH return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open(LOGFILENAME, 'wt', buffering=1) try: mypath = os.path.abspath(sys.argv[0]) print 'running tests from', mypath global BZRPATH if len(sys.argv) > 1: BZRPATH = sys.argv[1] else: BZRPATH = os.path.join(os.path.split(mypath)[0], 'bzr') print 'against bzr', BZRPATH print print backtick([BZRPATH, 'version']) runcmd(['mkdir', TESTDIR]) cd(TESTDIR) progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) runcmd("bzr diff --message foo", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == '''? test.txt\n''' out = backtick("bzr status --all") assert out == "? test.txt\n" progress("command aliases") out = backtick("bzr st --all") assert out == "? test.txt\n" out = backtick("bzr stat") assert out == "? test.txt\n" progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == "A test.txt\n" progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == "sub2/hello.txt\n" assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == 'sub1/sub2/hello.txt\n' assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') assert backtick('bzr relpath sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') cd('..') progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :399 committer Martin Pool 1115601665 +1000 data 29 - testbzr also runs selftests from :398 M 644 inline NEWS data 5850 bzr-0.0.5 NOT RELEASED YET ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New 'bzr ignore PATTERN' command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline testbzr data 7563 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): logfile.write('$ %s\n' % cmd) cmd = cmd.split() else: logfile.write('$ %r\n' % cmd) if cmd[0] == 'bzr': cmd[0] = BZRPATH return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open(LOGFILENAME, 'wt', buffering=1) try: mypath = os.path.abspath(sys.argv[0]) print 'running tests from', mypath global BZRPATH if len(sys.argv) > 1: BZRPATH = sys.argv[1] else: BZRPATH = os.path.join(os.path.split(mypath)[0], 'bzr') print 'against bzr', BZRPATH print print backtick([BZRPATH, 'version']) runcmd(['mkdir', TESTDIR]) cd(TESTDIR) progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("internal tests") runcmd("bzr selftest") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) runcmd("bzr diff --message foo", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == '''? test.txt\n''' out = backtick("bzr status --all") assert out == "? test.txt\n" progress("command aliases") out = backtick("bzr st --all") assert out == "? test.txt\n" out = backtick("bzr stat") assert out == "? test.txt\n" progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == "A test.txt\n" progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == "sub2/hello.txt\n" assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == 'sub1/sub2/hello.txt\n' assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') assert backtick('bzr relpath sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') cd('..') progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :400 committer Martin Pool 1115601797 +1000 data 42 - doc: please run tests after installation from :399 M 644 inline README data 1313 *********************************** Release notes for Bazaar-NG (pre-0) *********************************** mbp@sourcefrog.net, March 2005, Canberra Caveats ------- * There is little locking or transaction control here; if you interrupt it the tree may be arbitrarily broken. This will be fixed. * Don't use this for critical data; at the very least keep separate regular snapshots of your tree. Dependencies ------------ This is mostly developed on Linux (Ubuntu); it should work on Unix, Windows, or OS X with relatively little trouble. The only dependency is Python, at least 2.3 and preferably 2.4. On Windows, Python2.4 is required. You may optionally install cElementTree to speed up some operations. Installation ------------ The best way to install bzr is to symlink the ``bzr`` command onto a directory on your path. For example:: ln -s ~/work/bzr/bzr ~/bin/bzr If you use a symlink for this, Python will be able to automatically find the bzr libraries. Otherwise you must ensure they are listed on your $PYTHONPATH. After installing, please run the test suite to identify any problems on your platform:: ./testbzr If you use the setup.py script then bzr will be installed into the specified path. In this case you must install ElementTree or cElementTree separately. commit refs/heads/master mark :401 committer Martin Pool 1115607681 +1000 data 34 - very short tutorial on help page from :400 M 644 inline TODO data 9403 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Syntax should be ``bzr export -r REV``. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Read and write locks on branch while it's open. * Separate read and write version checks? * ``bzr status FILE...`` * Check all commands have decent help. * Autogenerate argument/option help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * testbzr should by default test the bzr binary in the same directory as the testbzr script, or take a path to it as a first parameter. Should show the version from bzr and the path name. Medium things ------------- * Display command grammar in help messages rather than hardcoding it. * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still expose it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` M 644 inline bzrlib/help.py data 3992 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA global_help = \ """Bazaar-NG -- a free distributed version-control tool http://bazaar-ng.org/ **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** * Metadata format is not stable yet -- you may need to discard history in the future. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. To make a branch, use 'bzr init' in an existing directory, then 'bzr add' to make files versioned. 'bzr add .' will recursively add all non-ignored files. 'bzr status' describes files that are unknown, ignored, or modified. 'bzr diff' shows the text changes to the tree or named files. 'bzr commit -m ' commits all changes in that branch. 'bzr move' and 'bzr rename' allow you to rename files or directories. 'bzr remove' makes a file unversioned but keeps the working copy; to delete that too simply delete the file. 'bzr log' shows a history of changes, and 'bzr info' gives summary statistical information. 'bzr check' validates all files are stored safely. Files can be ignored by giving a path or a glob in .bzrignore at the top of the tree. Use 'bzr ignored' to see what files are ignored and why, and 'bzr unknowns' to see files that are neither versioned or ignored. For more help on any command, type 'bzr help COMMAND', or 'bzr help commands' for a list. """ def help(topic=None): if topic == None: print global_help elif topic == 'commands': help_commands() else: help_on_command(topic) def help_on_command(cmdname): cmdname = str(cmdname) from inspect import getdoc import commands topic, cmdclass = commands.get_cmd_class(cmdname) doc = getdoc(cmdclass) if doc == None: raise NotImplementedError("sorry, no detailed help yet for %r" % cmdname) if '\n' in doc: short, rest = doc.split('\n', 1) else: short = doc rest = '' print 'usage: bzr ' + topic, for aname in cmdclass.takes_args: aname = aname.upper() if aname[-1] in ['$', '+']: aname = aname[:-1] + '...' elif aname[-1] == '?': aname = '[' + aname[:-1] + ']' elif aname[-1] == '*': aname = '[' + aname[:-1] + '...]' print aname, print print short if cmdclass.aliases: print 'aliases: ' + ', '.join(cmdclass.aliases) if rest: print rest help_on_option(cmdclass.takes_options) def help_on_option(options): import commands if not options: return print print 'options:' for on in options: l = ' --' + on for shortname, longname in commands.SHORT_OPTIONS.items(): if longname == on: l += ', -' + shortname break print l def help_commands(): """List all commands""" import inspect import commands accu = [] for cmdname, cmdclass in commands.get_all_cmds(): accu.append((cmdname, cmdclass)) accu.sort() for cmdname, cmdclass in accu: if cmdclass.hidden: continue print cmdname help = inspect.getdoc(cmdclass) if help: print " " + help.split('\n', 1)[0] commit refs/heads/master mark :402 committer Martin Pool 1115607743 +1000 data 18 - Update todo list from :401 M 644 inline TODO data 9322 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Read and write locks on branch while it's open. * Separate read and write version checks? * ``bzr status FILE...`` * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * testbzr should by default test the bzr binary in the same directory as the testbzr script, or take a path to it as a first parameter. Should show the version from bzr and the path name. Medium things ------------- * Display command grammar in help messages rather than hardcoding it. * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still expose it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :403 committer Martin Pool 1115607835 +1000 data 55 - Don't give an error if the trace file can't be opened from :402 M 644 inline bzrlib/trace.py data 3971 # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " """Messages and logging for bazaar-ng Nothing is actually logged unless you call bzrlib.open_tracefile(). """ import sys, os ###################################################################### # messages and logging global _tracefile, _starttime _tracefile = None # used to have % (os.environ['USER'], time.time(), os.getpid()), 'w') _starttime = None # If false, notes also go to stdout; should replace this with --silent # at some point. silent = False # fix this if we ever fork within python _mypid = os.getpid() _logprefix = '[%d] ' % _mypid def _write_trace(msg): if _tracefile: _tracefile.write(_logprefix + msg + '\n') def warning(msg): sys.stderr.write('bzr: warning: ' + msg + '\n') _write_trace('warning: ' + msg) mutter = _write_trace def note(msg): b = '* ' + str(msg) + '\n' if not silent: sys.stderr.write(b) _write_trace('note: ' + msg) def log_error(msg): sys.stderr.write(msg + '\n') _write_trace(msg) def _rollover_trace_maybe(trace_fname): import stat try: size = os.stat(trace_fname)[stat.ST_SIZE] if size <= 4 << 20: return old_fname = trace_fname + '.old' try: # must remove before rename on windows os.remove(old_fname) except OSError: pass try: # might fail if in use on windows os.rename(trace_fname, old_fname) except OSError: pass except OSError: return def open_tracefile(argv=[], tracefilename='~/.bzr.log'): # Messages are always written to here, so that we have some # information if something goes wrong. In a future version this # file will be removed on successful completion. global _starttime, _tracefile import stat, codecs _starttime = os.times()[4] trace_fname = os.path.join(os.path.expanduser(tracefilename)) _rollover_trace_maybe(trace_fname) # buffering=1 means line buffered try: _tracefile = codecs.open(trace_fname, 'at', 'utf8', buffering=1) t = _tracefile if os.fstat(t.fileno())[stat.ST_SIZE] == 0: t.write("\nthis is a debug log for diagnosing/reporting problems in bzr\n") t.write("you can delete or truncate this file, or include sections in\n") t.write("bug reports to bazaar-ng@lists.canonical.com\n\n") import bzrlib _write_trace('bzr %s invoked on python %s (%s)' % (bzrlib.__version__, '.'.join(map(str, sys.version_info)), sys.platform)) _write_trace(' arguments: %r' % argv) _write_trace(' working dir: ' + os.getcwdu()) except IOError, e: warning("failed to open trace file: %s" % (e)) def close_trace(): times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum, %.3f elapsed" % (times[:4] + ((times[4] - _starttime),))) def log_exception(): """Log the last exception into the trace file.""" import cgitb s = cgitb.text(sys.exc_info()) for l in s.split('\n'): _write_trace(l) commit refs/heads/master mark :404 committer Martin Pool 1115608281 +1000 data 52 - bzr status now optionally takes filenames to check from :403 M 644 inline NEWS data 5923 bzr-0.0.5 NOT RELEASED YET ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New 'bzr ignore PATTERN' command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline TODO data 9296 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Read and write locks on branch while it's open. * Separate read and write version checks? * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * testbzr should by default test the bzr binary in the same directory as the testbzr script, or take a path to it as a first parameter. Should show the version from bzr and the path name. Medium things ------------- * Display command grammar in help messages rather than hardcoding it. * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still expose it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` M 644 inline bzrlib/branch.py data 35734 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. base Base directory of the branch. """ _lockmode = None def __init__(self, base, init=False, find_root=True, lock_mode='w'): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.lock(lock_mode) self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def lock(self, mode='w'): """Lock the on-disk branch, excluding other processes.""" try: import fcntl, errno if mode == 'w': lm = fcntl.LOCK_EX om = os.O_WRONLY | os.O_CREAT elif mode == 'r': lm = fcntl.LOCK_SH om = os.O_RDONLY else: raise BzrError("invalid locking mode %r" % mode) try: lockfile = os.open(self.controlfilename('branch-lock'), om) except OSError, e: if e.errno == errno.ENOENT: # might not exist on branches from <0.0.4 self.controlfile('branch-lock', 'w').close() lockfile = os.open(self.controlfilename('branch-lock'), om) else: raise e fcntl.lockf(lockfile, lm) def unlock(): fcntl.lockf(lockfile, fcntl.LOCK_UN) os.close(lockfile) self._lockmode = None self.unlock = unlock self._lockmode = mode except ImportError: warning("please write a locking method for platform %r" % sys.platform) def unlock(): self._lockmode = None self.unlock = unlock self._lockmode = mode def _need_readlock(self): if self._lockmode not in ['r', 'w']: raise BzrError('need read lock on branch, only have %r' % self._lockmode) def _need_writelock(self): if self._lockmode not in ['w']: raise BzrError('need write lock on branch, only have %r' % self._lockmode) def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" self._need_readlock() before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self._need_writelock() ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ self._need_writelock() # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" self._need_readlock() tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability self._need_writelock() if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. """ self._need_writelock() ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) self.append_revision(rev_id) if verbose: note("commited r%d" % self.revno()) def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" self._need_readlock() r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" self._need_readlock() i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" self._need_readlock() if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self._need_readlock() return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise BzrError('invalid history direction %r' % direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] else: return None def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" self._need_readlock() if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self._need_writelock() tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self._need_writelock() ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(self, show_all=False, file_list=None): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo """ self._need_readlock() # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = self.basis_tree() new = self.working_tree() items = diff_trees(old, new) # We want to filter out only if any file was provided in the file_list. if isinstance(file_list, list) and len(file_list): items = [item for item in items if item[3] in file_list] for fs, fid, oldname, newname, kind in items: if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("weird file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/commands.py data 28172 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass else: raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_args = ['file*'] takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False, file_list=None): #import bzrlib.status #bzrlib.status.tree_status(Branch('.')) Branch('.').show_status(show_all=all, file_list=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): b = Branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern""" takes_args = ['name_pattern'] def run(self, name_pattern): b = Branch('.') # XXX: This will fail if it's a hardlink; should use an AtomicFile class. f = open(b.abspath('.bzrignore'), 'at') f.write(name_pattern + '\n') f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them.""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False): ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if isinstance(e, IOError) and e.errno == errno.EPIPE: quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline testbzr data 7963 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): logfile.write('$ %s\n' % cmd) cmd = cmd.split() else: logfile.write('$ %r\n' % cmd) if cmd[0] == 'bzr': cmd[0] = BZRPATH return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open(LOGFILENAME, 'wt', buffering=1) try: mypath = os.path.abspath(sys.argv[0]) print 'running tests from', mypath global BZRPATH if len(sys.argv) > 1: BZRPATH = sys.argv[1] else: BZRPATH = os.path.join(os.path.split(mypath)[0], 'bzr') print 'against bzr', BZRPATH print print backtick([BZRPATH, 'version']) runcmd(['mkdir', TESTDIR]) cd(TESTDIR) progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("internal tests") runcmd("bzr selftest") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) runcmd("bzr diff --message foo", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == '''? test.txt\n''' out = backtick("bzr status --all") assert out == "? test.txt\n" out = backtick("bzr status test.txt --all") assert out == "? test.txt\n" f = file('test2.txt', 'wt') f.write('goodbye cruel world...\n') f.close() out = backtick("bzr status test.txt") assert out == "? test.txt\n" out = backtick("bzr status") assert out == "? test.txt\n" \ + "? test2.txt\n" os.unlink('test2.txt') progress("command aliases") out = backtick("bzr st --all") assert out == "? test.txt\n" out = backtick("bzr stat") assert out == "? test.txt\n" progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == "A test.txt\n" progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == "sub2/hello.txt\n" assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == 'sub1/sub2/hello.txt\n' assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') assert backtick('bzr relpath sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') cd('..') progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :405 committer Martin Pool 1115608371 +1000 data 4 todo from :404 M 644 inline TODO data 9323 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * testbzr should by default test the bzr binary in the same directory as the testbzr script, or take a path to it as a first parameter. Should show the version from bzr and the path name. Medium things ------------- * Display command grammar in help messages rather than hardcoding it. * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still expose it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :406 committer Martin Pool 1115610715 +1000 data 35 - bzr status only needs a read-lock from :405 M 644 inline bzrlib/commands.py data 28123 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass else: raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_args = ['file*'] takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False, file_list=None): b = Branch('.', lock_mode='r') b.show_status(show_all=all, file_list=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): b = Branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern""" takes_args = ['name_pattern'] def run(self, name_pattern): b = Branch('.') # XXX: This will fail if it's a hardlink; should use an AtomicFile class. f = open(b.abspath('.bzrignore'), 'at') f.write(name_pattern + '\n') f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them.""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False): ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if isinstance(e, IOError) and e.errno == errno.EPIPE: quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :407 committer Martin Pool 1115613000 +1000 data 40 - slight improvements for zsh completion from :406 M 644 inline contrib/zsh/_bzr data 616 #compdef bzr # Rudimentary zsh completion support for bzr. # -S means there are no options after a -- and that argument is ignored # To use this you must arrange for it to be in a directory that is on # your $fpath, and also for compinit to be run. I don't understand # how to get zsh to reload this file when it changes, other than by # starting a new zsh. # This is not very useful at the moment because it only completes on # commands and breaks filename expansion for other arguments. _arguments -S "1::bzr command:($(bzr help commands | grep -v '^ '))" # Local variables: # mode: shell-script # End: commit refs/heads/master mark :408 committer Martin Pool 1115613511 +1000 data 57 - better message when refusing to add symlinks (from mpe) from :407 M 644 inline bzrlib/add.py data 2902 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, sys import bzrlib from osutils import quotefn, appendpath from errors import bailout from trace import mutter, note def smart_add(file_list, verbose=False, recurse=True): """Add files to version, optionall recursing into directories. This is designed more towards DWIM for humans than API simplicity. For the specific behaviour see the help for cmd_add(). """ assert file_list assert not isinstance(file_list, basestring) b = bzrlib.branch.Branch(file_list[0], find_root=True) inv = b.read_working_inventory() tree = b.working_tree() count = 0 for f in file_list: rf = b.relpath(f) af = b.abspath(rf) ## TODO: It's OK to add root but only in recursive mode bzrlib.mutter("smart add of %r" % f) if bzrlib.branch.is_control_file(af): bailout("cannot add control file %r" % af) kind = bzrlib.osutils.file_kind(f) if kind != 'file' and kind != 'directory': bailout("can't add file '%s' of kind %r" % (f, kind)) versioned = (inv.path2id(rf) != None) if rf == '': mutter("branch root doesn't need to be added") elif versioned: mutter("%r is already versioned" % f) else: file_id = bzrlib.branch.gen_file_id(rf) inv.add_path(rf, kind=kind, file_id=file_id) bzrlib.mutter("added %r kind %r file_id={%s}" % (rf, kind, file_id)) count += 1 if verbose: bzrlib.textui.show_status('A', kind, quotefn(f)) if kind == 'directory' and recurse: for subf in os.listdir(af): subp = appendpath(rf, subf) if subf == bzrlib.BZRDIR: mutter("skip control directory %r" % subp) elif tree.is_ignored(subp): mutter("skip ignored sub-file %r" % subp) else: mutter("queue to add sub-file %r" % (subp)) file_list.append(subp) if count > 0: if verbose: note('added %d' % count) b._write_inventory(inv) commit refs/heads/master mark :409 committer Martin Pool 1115614211 +1000 data 94 - New AtomicFile class - bzr ignore: Write out ignore list using AtomicFile to break hardlinks from :408 M 644 inline bzrlib/atomicfile.py data 1791 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA class AtomicFile: """A file that does an atomic-rename to move into place. This also causes hardlinks to break when it's written out. Open this as for a regular file, then use commit() to move into place or abort() to cancel. You may wish to wrap this in a codecs.EncodedFile to do unicode encoding. """ def __init__(self, filename, mode='wb'): if mode != 'wb' and mode != 'wt': raise ValueError("invalid AtomicFile mode %r" % mode) import os, socket self.tmpfilename = '%s.tmp.%d.%s' % (filename, os.getpid(), socket.gethostname()) self.realfilename = filename self.f = open(self.tmpfilename, mode) self.write = self.f.write def commit(self): self.f.close() if sys.platform == 'win32': os.remove(self.realfilename) os.rename(self.tmpfilename, self.realfilename) def abort(self): self.f.close() os.remove(self.tmpfilename) M 644 inline NEWS data 5951 bzr-0.0.5 NOT RELEASED YET ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New 'bzr ignore PATTERN' command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 28471 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass else: raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_args = ['file*'] takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False, file_list=None): b = Branch('.', lock_mode='r') b.show_status(show_all=all, file_list=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): b = Branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern""" takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') # FIXME: probably doesn't handle non-ascii patterns if os.path.exists(b.controlfilename('.bzrignore')): f = b.controlfile('.bzrignore', 'rt') igns = f.read() f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' f = AtomicFile(b.controlfilename('.bzrignore'), 'wt') f.write(igns) f.commit() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them.""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False): ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if isinstance(e, IOError) and e.errno == errno.EPIPE: quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :410 committer Martin Pool 1115614740 +1000 data 34 - Fix ignore command and add tests from :409 M 644 inline bzrlib/atomicfile.py data 1841 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA class AtomicFile: """A file that does an atomic-rename to move into place. This also causes hardlinks to break when it's written out. Open this as for a regular file, then use commit() to move into place or abort() to cancel. You may wish to wrap this in a codecs.EncodedFile to do unicode encoding. """ def __init__(self, filename, mode='wb'): if mode != 'wb' and mode != 'wt': raise ValueError("invalid AtomicFile mode %r" % mode) import os, socket self.tmpfilename = '%s.tmp.%d.%s' % (filename, os.getpid(), socket.gethostname()) self.realfilename = filename self.f = open(self.tmpfilename, mode) self.write = self.f.write def commit(self): import sys, os self.f.close() if sys.platform == 'win32': os.remove(self.realfilename) os.rename(self.tmpfilename, self.realfilename) def abort(self): import os self.f.close() os.remove(self.tmpfilename) M 644 inline bzrlib/commands.py data 28444 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass else: raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_args = ['file*'] takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False, file_list=None): b = Branch('.', lock_mode='r') b.show_status(show_all=all, file_list=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" print bzrlib.branch.find_branch_root(filename) class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): b = Branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern""" takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') # FIXME: probably doesn't handle non-ascii patterns if os.path.exists(ifn): f = b.controlfile(ifn, 'rt') igns = f.read() f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' f = AtomicFile(ifn, 'wt') f.write(igns) f.commit() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them.""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False): ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if isinstance(e, IOError) and e.errno == errno.EPIPE: quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline testbzr data 8616 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): logfile.write('$ %s\n' % cmd) cmd = cmd.split() else: logfile.write('$ %r\n' % cmd) if cmd[0] == 'bzr': cmd[0] = BZRPATH return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open(LOGFILENAME, 'wt', buffering=1) try: mypath = os.path.abspath(sys.argv[0]) print 'running tests from', mypath global BZRPATH if len(sys.argv) > 1: BZRPATH = sys.argv[1] else: BZRPATH = os.path.join(os.path.split(mypath)[0], 'bzr') print 'against bzr', BZRPATH print print backtick([BZRPATH, 'version']) runcmd(['mkdir', TESTDIR]) cd(TESTDIR) progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("internal tests") runcmd("bzr selftest") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) runcmd("bzr diff --message foo", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == '''? test.txt\n''' out = backtick("bzr status --all") assert out == "? test.txt\n" out = backtick("bzr status test.txt --all") assert out == "? test.txt\n" f = file('test2.txt', 'wt') f.write('goodbye cruel world...\n') f.close() out = backtick("bzr status test.txt") assert out == "? test.txt\n" out = backtick("bzr status") assert out == "? test.txt\n" \ + "? test2.txt\n" os.unlink('test2.txt') progress("command aliases") out = backtick("bzr st --all") assert out == "? test.txt\n" out = backtick("bzr stat") assert out == "? test.txt\n" progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == "A test.txt\n" progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == "sub2/hello.txt\n" assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == 'sub1/sub2/hello.txt\n' assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') assert backtick('bzr relpath sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') cd('..') cd('..') progress('ignore patterns') mkdir('ignorebranch') cd('ignorebranch') runcmd('bzr init') assert backtick('bzr unknowns') == '' file('foo.tmp', 'wt').write('tmp files are ignored') assert backtick('bzr unknowns') == '' file('foo.c', 'wt').write('int main() {}') assert backtick('bzr unknowns') == 'foo.c\n' runcmd('bzr add foo.c') assert backtick('bzr unknowns') == '' file('foo.blah', 'wt').write('blah') assert backtick('bzr unknowns') == 'foo.blah\n' runcmd('bzr ignore *.blah') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rt').read() == '*.blah\n' progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :411 committer Martin Pool 1115617326 +1000 data 47 - start adding more useful RemoteBranch() class from :410 M 644 inline bzrlib/remotebranch.py data 4118 #! /usr/bin/env python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ## XXX: This is pretty slow on high-latency connections because it ## doesn't keep the HTTP connection alive. If you have a smart local ## proxy it may be much better. Eventually I want to switch to ## urlgrabber which should use HTTP much more efficiently. import urllib2, gzip, zlib from sets import Set from cStringIO import StringIO from errors import BzrError from revision import Revision from branch import Branch from inventory import Inventory # h = HTTPConnection('localhost:8000') # h = HTTPConnection('bazaar-ng.org') # velocitynet.com.au transparently proxies connections and thereby # breaks keep-alive -- sucks! import urlgrabber.keepalive urlgrabber.keepalive.DEBUG = 2 import urlgrabber # prefix = 'http://localhost:8000' BASE_URL = 'http://bazaar-ng.org/bzr/bzr.dev/' def get_url(path, compressed=False): try: url = path if compressed: url += '.gz' url_f = urlgrabber.urlopen(url, keepalive=1, close_connection=0) if not compressed: return url_f else: return gzip.GzipFile(fileobj=StringIO(url_f.read())) except urllib2.URLError, e: raise BzrError("remote fetch failed: %r: %s" % (url, e)) class RemoteBranch(Branch): def __init__(self, baseurl): """Create new proxy for a remote branch.""" self.baseurl = baseurl self._check_format() def controlfile(self, filename, mode): if mode not in ('rb', 'rt', 'r'): raise BzrError("file mode %r not supported for remote branches" % mode) return get_url(self.baseurl + '/.bzr/' + filename, False) def _need_readlock(self): # remote branch always safe for read pass def _need_writelock(self): raise BzrError("cannot get write lock on HTTP remote branch") def simple_walk(): got_invs = Set() got_texts = Set() print 'read history' history = get_url('/.bzr/revision-history').readlines() num_revs = len(history) for i, rev_id in enumerate(history): rev_id = rev_id.rstrip() print 'read revision %d/%d' % (i, num_revs) # python gzip needs a seekable file (!!) but the HTTP response # isn't, so we need to buffer it rev_f = get_url('/.bzr/revision-store/%s' % rev_id, compressed=True) rev = Revision.read_xml(rev_f) print rev.message inv_id = rev.inventory_id if inv_id not in got_invs: print 'get inventory %s' % inv_id inv_f = get_url('/.bzr/inventory-store/%s' % inv_id, compressed=True) inv = Inventory.read_xml(inv_f) print '%4d inventory entries' % len(inv) for path, ie in inv.iter_entries(): text_id = ie.text_id if text_id == None: continue if text_id in got_texts: continue print ' fetch %s text {%s}' % (path, text_id) text_f = get_url('/.bzr/text-store/%s' % text_id, compressed=True) got_texts.add(text_id) got_invs.add(inv_id) print '----' def try_me(): b = RemoteBranch(BASE_URL) print '\n'.join(b.revision_history()) if __name__ == '__main__': try_me() commit refs/heads/master mark :412 committer Martin Pool 1115618393 +1000 data 224 - RemoteBranch displays the log of a remote branch. - Use the urllib2 library which comes with Python rather than the urlgrabber extension. For the way we use it at the moment urlgrabber is not much faster or better. from :411 M 644 inline bzrlib/remotebranch.py data 5067 #! /usr/bin/env python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Proxy object for access to remote branches. At the moment remote branches are only for HTTP and only for read access. """ ## XXX: This is pretty slow on high-latency connections because it ## doesn't keep the HTTP connection alive. If you have a smart local ## proxy it may be much better. Eventually I want to switch to ## urlgrabber which should use HTTP much more efficiently. import gzip from sets import Set from cStringIO import StringIO from errors import BzrError, BzrCheckError from revision import Revision from branch import Branch from inventory import Inventory # h = HTTPConnection('localhost:8000') # h = HTTPConnection('bazaar-ng.org') # velocitynet.com.au transparently proxies connections and thereby # breaks keep-alive -- sucks! # prefix = 'http://localhost:8000' BASE_URL = 'http://bazaar-ng.org/bzr/bzr.dev/' ENABLE_URLGRABBER = False def get_url(url, compressed=False): import urllib2 if compressed: url += '.gz' url_f = urllib2.urlopen(url) if compressed: return gzip.GzipFile(fileobj=StringIO(url_f.read())) else: return url_f if ENABLE_URLGRABBER: import urlgrabber import urlgrabber.keepalive urlgrabber.keepalive.DEBUG = 0 def get_url(path, compressed=False): try: url = path if compressed: url += '.gz' url_f = urlgrabber.urlopen(url, keepalive=1, close_connection=0) if not compressed: return url_f else: return gzip.GzipFile(fileobj=StringIO(url_f.read())) except urllib2.URLError, e: raise BzrError("remote fetch failed: %r: %s" % (url, e)) class RemoteBranch(Branch): def __init__(self, baseurl): """Create new proxy for a remote branch.""" self.baseurl = baseurl self._check_format() def controlfile(self, filename, mode): if mode not in ('rb', 'rt', 'r'): raise BzrError("file mode %r not supported for remote branches" % mode) return get_url(self.baseurl + '/.bzr/' + filename, False) def _need_readlock(self): # remote branch always safe for read pass def _need_writelock(self): raise BzrError("cannot get write lock on HTTP remote branch") def get_revision(self, revision_id): from revision import Revision revf = get_url(self.baseurl + '/.bzr/revision-store/' + revision_id, True) r = Revision.read_xml(revf) if r.revision_id != revision_id: raise BzrCheckError('revision stored as {%s} actually contains {%s}' % (revision_id, r.revision_id)) return r def simple_walk(): got_invs = Set() got_texts = Set() print 'read history' history = get_url('/.bzr/revision-history').readlines() num_revs = len(history) for i, rev_id in enumerate(history): rev_id = rev_id.rstrip() print 'read revision %d/%d' % (i, num_revs) # python gzip needs a seekable file (!!) but the HTTP response # isn't, so we need to buffer it rev_f = get_url('/.bzr/revision-store/%s' % rev_id, compressed=True) rev = Revision.read_xml(rev_f) print rev.message inv_id = rev.inventory_id if inv_id not in got_invs: print 'get inventory %s' % inv_id inv_f = get_url('/.bzr/inventory-store/%s' % inv_id, compressed=True) inv = Inventory.read_xml(inv_f) print '%4d inventory entries' % len(inv) for path, ie in inv.iter_entries(): text_id = ie.text_id if text_id == None: continue if text_id in got_texts: continue print ' fetch %s text {%s}' % (path, text_id) text_f = get_url('/.bzr/text-store/%s' % text_id, compressed=True) got_texts.add(text_id) got_invs.add(inv_id) print '----' def try_me(): b = RemoteBranch(BASE_URL) ## print '\n'.join(b.revision_history()) from log import show_log show_log(b) if __name__ == '__main__': try_me() commit refs/heads/master mark :413 committer Martin Pool 1115618982 +1000 data 68 - more indicators at top of test output - tidy up remotebranch stuff from :412 M 644 inline bzrlib/remotebranch.py data 4733 #! /usr/bin/env python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Proxy object for access to remote branches. At the moment remote branches are only for HTTP and only for read access. """ import gzip from sets import Set from cStringIO import StringIO from errors import BzrError, BzrCheckError from branch import Branch # velocitynet.com.au transparently proxies connections and thereby # breaks keep-alive -- sucks! ENABLE_URLGRABBER = False def get_url(url, compressed=False): import urllib2 if compressed: url += '.gz' url_f = urllib2.urlopen(url) if compressed: return gzip.GzipFile(fileobj=StringIO(url_f.read())) else: return url_f if ENABLE_URLGRABBER: import urlgrabber import urlgrabber.keepalive urlgrabber.keepalive.DEBUG = 0 def get_url(path, compressed=False): try: url = path if compressed: url += '.gz' url_f = urlgrabber.urlopen(url, keepalive=1, close_connection=0) if not compressed: return url_f else: return gzip.GzipFile(fileobj=StringIO(url_f.read())) except urllib2.URLError, e: raise BzrError("remote fetch failed: %r: %s" % (url, e)) class RemoteBranch(Branch): def __init__(self, baseurl): """Create new proxy for a remote branch.""" self.baseurl = baseurl self._check_format() def controlfile(self, filename, mode): if mode not in ('rb', 'rt', 'r'): raise BzrError("file mode %r not supported for remote branches" % mode) return get_url(self.baseurl + '/.bzr/' + filename, False) def _need_readlock(self): # remote branch always safe for read pass def _need_writelock(self): raise BzrError("cannot get write lock on HTTP remote branch") def get_revision(self, revision_id): from revision import Revision revf = get_url(self.baseurl + '/.bzr/revision-store/' + revision_id, True) r = Revision.read_xml(revf) if r.revision_id != revision_id: raise BzrCheckError('revision stored as {%s} actually contains {%s}' % (revision_id, r.revision_id)) return r def simple_walk(): from revision import Revision from branch import Branch from inventory import Inventory got_invs = Set() got_texts = Set() print 'read history' history = get_url('/.bzr/revision-history').readlines() num_revs = len(history) for i, rev_id in enumerate(history): rev_id = rev_id.rstrip() print 'read revision %d/%d' % (i, num_revs) # python gzip needs a seekable file (!!) but the HTTP response # isn't, so we need to buffer it rev_f = get_url('/.bzr/revision-store/%s' % rev_id, compressed=True) rev = Revision.read_xml(rev_f) print rev.message inv_id = rev.inventory_id if inv_id not in got_invs: print 'get inventory %s' % inv_id inv_f = get_url('/.bzr/inventory-store/%s' % inv_id, compressed=True) inv = Inventory.read_xml(inv_f) print '%4d inventory entries' % len(inv) for path, ie in inv.iter_entries(): text_id = ie.text_id if text_id == None: continue if text_id in got_texts: continue print ' fetch %s text {%s}' % (path, text_id) text_f = get_url('/.bzr/text-store/%s' % text_id, compressed=True) got_texts.add(text_id) got_invs.add(inv_id) print '----' def try_me(): BASE_URL = 'http://bazaar-ng.org/bzr/bzr.dev/' b = RemoteBranch(BASE_URL) ## print '\n'.join(b.revision_history()) from log import show_log show_log(b) if __name__ == '__main__': try_me() M 644 inline testbzr data 8699 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): logfile.write('$ %s\n' % cmd) cmd = cmd.split() else: logfile.write('$ %r\n' % cmd) if cmd[0] == 'bzr': cmd[0] = BZRPATH return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open(LOGFILENAME, 'wt', buffering=1) try: mypath = os.path.abspath(sys.argv[0]) print '%-30s %s' % ('running tests from', mypath) global BZRPATH if len(sys.argv) > 1: BZRPATH = sys.argv[1] else: BZRPATH = os.path.join(os.path.split(mypath)[0], 'bzr') print '%-30s %s' % ('against bzr', BZRPATH) print '%-30s %s' % ('in directory', os.getcwd()) print print backtick([BZRPATH, 'version']) runcmd(['mkdir', TESTDIR]) cd(TESTDIR) progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("internal tests") runcmd("bzr selftest") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) runcmd("bzr diff --message foo", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == '''? test.txt\n''' out = backtick("bzr status --all") assert out == "? test.txt\n" out = backtick("bzr status test.txt --all") assert out == "? test.txt\n" f = file('test2.txt', 'wt') f.write('goodbye cruel world...\n') f.close() out = backtick("bzr status test.txt") assert out == "? test.txt\n" out = backtick("bzr status") assert out == "? test.txt\n" \ + "? test2.txt\n" os.unlink('test2.txt') progress("command aliases") out = backtick("bzr st --all") assert out == "? test.txt\n" out = backtick("bzr stat") assert out == "? test.txt\n" progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == "A test.txt\n" progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == "sub2/hello.txt\n" assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == 'sub1/sub2/hello.txt\n' assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') assert backtick('bzr relpath sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') cd('..') cd('..') progress('ignore patterns') mkdir('ignorebranch') cd('ignorebranch') runcmd('bzr init') assert backtick('bzr unknowns') == '' file('foo.tmp', 'wt').write('tmp files are ignored') assert backtick('bzr unknowns') == '' file('foo.c', 'wt').write('int main() {}') assert backtick('bzr unknowns') == 'foo.c\n' runcmd('bzr add foo.c') assert backtick('bzr unknowns') == '' file('foo.blah', 'wt').write('blah') assert backtick('bzr unknowns') == 'foo.blah\n' runcmd('bzr ignore *.blah') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rt').read() == '*.blah\n' progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :414 committer Martin Pool 1115619733 +1000 data 4 todo from :413 M 644 inline TODO data 9568 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * testbzr should by default test the bzr binary in the same directory as the testbzr script, or take a path to it as a first parameter. Should show the version from bzr and the path name. * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. Medium things ------------- * Display command grammar in help messages rather than hardcoding it. * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still expose it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :415 committer Martin Pool 1115619749 +1000 data 4 todo from :414 M 644 inline TODO data 9497 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * testbzr should by default test the bzr binary in the same directory as the testbzr script, or take a path to it as a first parameter. Should show the version from bzr and the path name. * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. Medium things ------------- * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * Turn on stat cache code, and add optimization about avoiding dangerous cache entries. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still expose it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :416 committer Martin Pool 1115627269 +1000 data 107 - bzr log and bzr root now accept an http URL - new RemoteBranch.relpath() - new find_branch factory method from :415 M 644 inline NEWS data 6041 bzr-0.0.5 NOT RELEASED YET ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New 'bzr ignore PATTERN' command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. * ``bzr log`` and ``bzr root`` can be given an http URL instead of a filename. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/branch.py data 35991 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f.startswith('http://') or f.startswith('https://'): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. base Base directory of the branch. """ _lockmode = None def __init__(self, base, init=False, find_root=True, lock_mode='w'): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.lock(lock_mode) self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def lock(self, mode='w'): """Lock the on-disk branch, excluding other processes.""" try: import fcntl, errno if mode == 'w': lm = fcntl.LOCK_EX om = os.O_WRONLY | os.O_CREAT elif mode == 'r': lm = fcntl.LOCK_SH om = os.O_RDONLY else: raise BzrError("invalid locking mode %r" % mode) try: lockfile = os.open(self.controlfilename('branch-lock'), om) except OSError, e: if e.errno == errno.ENOENT: # might not exist on branches from <0.0.4 self.controlfile('branch-lock', 'w').close() lockfile = os.open(self.controlfilename('branch-lock'), om) else: raise e fcntl.lockf(lockfile, lm) def unlock(): fcntl.lockf(lockfile, fcntl.LOCK_UN) os.close(lockfile) self._lockmode = None self.unlock = unlock self._lockmode = mode except ImportError: warning("please write a locking method for platform %r" % sys.platform) def unlock(): self._lockmode = None self.unlock = unlock self._lockmode = mode def _need_readlock(self): if self._lockmode not in ['r', 'w']: raise BzrError('need read lock on branch, only have %r' % self._lockmode) def _need_writelock(self): if self._lockmode not in ['w']: raise BzrError('need write lock on branch, only have %r' % self._lockmode) def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" self._need_readlock() before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self._need_writelock() ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ self._need_writelock() # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" self._need_readlock() tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability self._need_writelock() if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. """ self._need_writelock() ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) self.append_revision(rev_id) if verbose: note("commited r%d" % self.revno()) def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" self._need_readlock() r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" self._need_readlock() i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" self._need_readlock() if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self._need_readlock() return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise BzrError('invalid history direction %r' % direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] else: return None def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" self._need_readlock() if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self._need_writelock() tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self._need_writelock() ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(self, show_all=False, file_list=None): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo """ self._need_readlock() # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = self.basis_tree() new = self.working_tree() items = diff_trees(old, new) # We want to filter out only if any file was provided in the file_list. if isinstance(file_list, list) and len(file_list): items = [item for item in items if item[3] in file_list] for fs, fid, oldname, newname, kind in items: if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("weird file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/commands.py data 28569 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass else: raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_args = ['file*'] takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False, file_list=None): b = Branch('.', lock_mode='r') b.show_status(show_all=all, file_list=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from branch import find_branch b = find_branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern""" takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') # FIXME: probably doesn't handle non-ascii patterns if os.path.exists(ifn): f = b.controlfile(ifn, 'rt') igns = f.read() f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' f = AtomicFile(ifn, 'wt') f.write(igns) f.commit() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them.""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False): ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if isinstance(e, IOError) and e.errno == errno.EPIPE: quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/remotebranch.py data 5676 #! /usr/bin/env python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Proxy object for access to remote branches. At the moment remote branches are only for HTTP and only for read access. """ import gzip from sets import Set from cStringIO import StringIO from errors import BzrError, BzrCheckError from branch import Branch # velocitynet.com.au transparently proxies connections and thereby # breaks keep-alive -- sucks! ENABLE_URLGRABBER = False def get_url(url, compressed=False): import urllib2 if compressed: url += '.gz' url_f = urllib2.urlopen(url) if compressed: return gzip.GzipFile(fileobj=StringIO(url_f.read())) else: return url_f if ENABLE_URLGRABBER: import urlgrabber import urlgrabber.keepalive urlgrabber.keepalive.DEBUG = 0 def get_url(path, compressed=False): try: url = path if compressed: url += '.gz' url_f = urlgrabber.urlopen(url, keepalive=1, close_connection=0) if not compressed: return url_f else: return gzip.GzipFile(fileobj=StringIO(url_f.read())) except urllib2.URLError, e: raise BzrError("remote fetch failed: %r: %s" % (url, e)) def _find_remote_root(url): """Return the prefix URL that corresponds to the branch root.""" orig_url = url while True: try: ff = get_url(url + '/.bzr/branch-format') ff.close() return url except urllib.URLError: pass try: idx = url.rindex('/') except ValueError: raise BzrError('no branch root found for URL %s' % orig_url) url = url[:idx] class RemoteBranch(Branch): def __init__(self, baseurl, find_root=False, lock_mode='r'): """Create new proxy for a remote branch.""" if lock_mode not in ('', 'r'): raise BzrError('lock mode %r is not supported for remote branches' % lock_mode) self.baseurl = baseurl self._check_format() def controlfile(self, filename, mode): if mode not in ('rb', 'rt', 'r'): raise BzrError("file mode %r not supported for remote branches" % mode) return get_url(self.baseurl + '/.bzr/' + filename, False) def _need_readlock(self): # remote branch always safe for read pass def _need_writelock(self): raise BzrError("cannot get write lock on HTTP remote branch") def relpath(self, path): if not path.startswith(self.baseurl): raise BzrError('path %r is not under base URL %r' % (path, self.baseurl)) pl = len(self.baseurl) return path[pl:].lstrip('/') def get_revision(self, revision_id): from revision import Revision revf = get_url(self.baseurl + '/.bzr/revision-store/' + revision_id, True) r = Revision.read_xml(revf) if r.revision_id != revision_id: raise BzrCheckError('revision stored as {%s} actually contains {%s}' % (revision_id, r.revision_id)) return r def simple_walk(): from revision import Revision from branch import Branch from inventory import Inventory got_invs = Set() got_texts = Set() print 'read history' history = get_url('/.bzr/revision-history').readlines() num_revs = len(history) for i, rev_id in enumerate(history): rev_id = rev_id.rstrip() print 'read revision %d/%d' % (i, num_revs) # python gzip needs a seekable file (!!) but the HTTP response # isn't, so we need to buffer it rev_f = get_url('/.bzr/revision-store/%s' % rev_id, compressed=True) rev = Revision.read_xml(rev_f) print rev.message inv_id = rev.inventory_id if inv_id not in got_invs: print 'get inventory %s' % inv_id inv_f = get_url('/.bzr/inventory-store/%s' % inv_id, compressed=True) inv = Inventory.read_xml(inv_f) print '%4d inventory entries' % len(inv) for path, ie in inv.iter_entries(): text_id = ie.text_id if text_id == None: continue if text_id in got_texts: continue print ' fetch %s text {%s}' % (path, text_id) text_f = get_url('/.bzr/text-store/%s' % text_id, compressed=True) got_texts.add(text_id) got_invs.add(inv_id) print '----' def try_me(): BASE_URL = 'http://bazaar-ng.org/bzr/bzr.dev/' b = RemoteBranch(BASE_URL) ## print '\n'.join(b.revision_history()) from log import show_log show_log(b) if __name__ == '__main__': try_me() commit refs/heads/master mark :417 committer Martin Pool 1115627442 +1000 data 27 - trace when we fetch a URL from :416 M 644 inline bzrlib/remotebranch.py data 5847 #! /usr/bin/env python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Proxy object for access to remote branches. At the moment remote branches are only for HTTP and only for read access. """ import gzip from sets import Set from cStringIO import StringIO from errors import BzrError, BzrCheckError from branch import Branch from trace import mutter # velocitynet.com.au transparently proxies connections and thereby # breaks keep-alive -- sucks! ENABLE_URLGRABBER = False def get_url(url, compressed=False): import urllib2 if compressed: url += '.gz' mutter("get_url %s" % url) url_f = urllib2.urlopen(url) if compressed: return gzip.GzipFile(fileobj=StringIO(url_f.read())) else: return url_f if ENABLE_URLGRABBER: import urlgrabber import urlgrabber.keepalive urlgrabber.keepalive.DEBUG = 0 def get_url(path, compressed=False): try: url = path if compressed: url += '.gz' url_f = urlgrabber.urlopen(url, keepalive=1, close_connection=0) if not compressed: return url_f else: return gzip.GzipFile(fileobj=StringIO(url_f.read())) except urllib2.URLError, e: raise BzrError("remote fetch failed: %r: %s" % (url, e)) def _find_remote_root(url): """Return the prefix URL that corresponds to the branch root.""" orig_url = url while True: try: ff = get_url(url + '/.bzr/branch-format') ff.close() return url except urllib.URLError: pass try: idx = url.rindex('/') except ValueError: raise BzrError('no branch root found for URL %s' % orig_url) url = url[:idx] class RemoteBranch(Branch): def __init__(self, baseurl, find_root=False, lock_mode='r'): """Create new proxy for a remote branch.""" if lock_mode not in ('', 'r'): raise BzrError('lock mode %r is not supported for remote branches' % lock_mode) self.baseurl = baseurl self._check_format() def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.baseurl) __repr__ = __str__ def controlfile(self, filename, mode): if mode not in ('rb', 'rt', 'r'): raise BzrError("file mode %r not supported for remote branches" % mode) return get_url(self.baseurl + '/.bzr/' + filename, False) def _need_readlock(self): # remote branch always safe for read pass def _need_writelock(self): raise BzrError("cannot get write lock on HTTP remote branch") def relpath(self, path): if not path.startswith(self.baseurl): raise BzrError('path %r is not under base URL %r' % (path, self.baseurl)) pl = len(self.baseurl) return path[pl:].lstrip('/') def get_revision(self, revision_id): from revision import Revision revf = get_url(self.baseurl + '/.bzr/revision-store/' + revision_id, True) r = Revision.read_xml(revf) if r.revision_id != revision_id: raise BzrCheckError('revision stored as {%s} actually contains {%s}' % (revision_id, r.revision_id)) return r def simple_walk(): from revision import Revision from branch import Branch from inventory import Inventory got_invs = Set() got_texts = Set() print 'read history' history = get_url('/.bzr/revision-history').readlines() num_revs = len(history) for i, rev_id in enumerate(history): rev_id = rev_id.rstrip() print 'read revision %d/%d' % (i, num_revs) # python gzip needs a seekable file (!!) but the HTTP response # isn't, so we need to buffer it rev_f = get_url('/.bzr/revision-store/%s' % rev_id, compressed=True) rev = Revision.read_xml(rev_f) print rev.message inv_id = rev.inventory_id if inv_id not in got_invs: print 'get inventory %s' % inv_id inv_f = get_url('/.bzr/inventory-store/%s' % inv_id, compressed=True) inv = Inventory.read_xml(inv_f) print '%4d inventory entries' % len(inv) for path, ie in inv.iter_entries(): text_id = ie.text_id if text_id == None: continue if text_id in got_texts: continue print ' fetch %s text {%s}' % (path, text_id) text_f = get_url('/.bzr/text-store/%s' % text_id, compressed=True) got_texts.add(text_id) got_invs.add(inv_id) print '----' def try_me(): BASE_URL = 'http://bazaar-ng.org/bzr/bzr.dev/' b = RemoteBranch(BASE_URL) ## print '\n'.join(b.revision_history()) from log import show_log show_log(b) if __name__ == '__main__': try_me() commit refs/heads/master mark :418 committer Martin Pool 1115627594 +1000 data 10 - fix typo from :417 M 644 inline bzrlib/remotebranch.py data 5848 #! /usr/bin/env python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Proxy object for access to remote branches. At the moment remote branches are only for HTTP and only for read access. """ import gzip from sets import Set from cStringIO import StringIO from errors import BzrError, BzrCheckError from branch import Branch from trace import mutter # velocitynet.com.au transparently proxies connections and thereby # breaks keep-alive -- sucks! ENABLE_URLGRABBER = False def get_url(url, compressed=False): import urllib2 if compressed: url += '.gz' mutter("get_url %s" % url) url_f = urllib2.urlopen(url) if compressed: return gzip.GzipFile(fileobj=StringIO(url_f.read())) else: return url_f if ENABLE_URLGRABBER: import urlgrabber import urlgrabber.keepalive urlgrabber.keepalive.DEBUG = 0 def get_url(path, compressed=False): try: url = path if compressed: url += '.gz' url_f = urlgrabber.urlopen(url, keepalive=1, close_connection=0) if not compressed: return url_f else: return gzip.GzipFile(fileobj=StringIO(url_f.read())) except urllib2.URLError, e: raise BzrError("remote fetch failed: %r: %s" % (url, e)) def _find_remote_root(url): """Return the prefix URL that corresponds to the branch root.""" orig_url = url while True: try: ff = get_url(url + '/.bzr/branch-format') ff.close() return url except urllib2.URLError: pass try: idx = url.rindex('/') except ValueError: raise BzrError('no branch root found for URL %s' % orig_url) url = url[:idx] class RemoteBranch(Branch): def __init__(self, baseurl, find_root=False, lock_mode='r'): """Create new proxy for a remote branch.""" if lock_mode not in ('', 'r'): raise BzrError('lock mode %r is not supported for remote branches' % lock_mode) self.baseurl = baseurl self._check_format() def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.baseurl) __repr__ = __str__ def controlfile(self, filename, mode): if mode not in ('rb', 'rt', 'r'): raise BzrError("file mode %r not supported for remote branches" % mode) return get_url(self.baseurl + '/.bzr/' + filename, False) def _need_readlock(self): # remote branch always safe for read pass def _need_writelock(self): raise BzrError("cannot get write lock on HTTP remote branch") def relpath(self, path): if not path.startswith(self.baseurl): raise BzrError('path %r is not under base URL %r' % (path, self.baseurl)) pl = len(self.baseurl) return path[pl:].lstrip('/') def get_revision(self, revision_id): from revision import Revision revf = get_url(self.baseurl + '/.bzr/revision-store/' + revision_id, True) r = Revision.read_xml(revf) if r.revision_id != revision_id: raise BzrCheckError('revision stored as {%s} actually contains {%s}' % (revision_id, r.revision_id)) return r def simple_walk(): from revision import Revision from branch import Branch from inventory import Inventory got_invs = Set() got_texts = Set() print 'read history' history = get_url('/.bzr/revision-history').readlines() num_revs = len(history) for i, rev_id in enumerate(history): rev_id = rev_id.rstrip() print 'read revision %d/%d' % (i, num_revs) # python gzip needs a seekable file (!!) but the HTTP response # isn't, so we need to buffer it rev_f = get_url('/.bzr/revision-store/%s' % rev_id, compressed=True) rev = Revision.read_xml(rev_f) print rev.message inv_id = rev.inventory_id if inv_id not in got_invs: print 'get inventory %s' % inv_id inv_f = get_url('/.bzr/inventory-store/%s' % inv_id, compressed=True) inv = Inventory.read_xml(inv_f) print '%4d inventory entries' % len(inv) for path, ie in inv.iter_entries(): text_id = ie.text_id if text_id == None: continue if text_id in got_texts: continue print ' fetch %s text {%s}' % (path, text_id) text_f = get_url('/.bzr/text-store/%s' % text_id, compressed=True) got_texts.add(text_id) got_invs.add(inv_id) print '----' def try_me(): BASE_URL = 'http://bazaar-ng.org/bzr/bzr.dev/' b = RemoteBranch(BASE_URL) ## print '\n'.join(b.revision_history()) from log import show_log show_log(b) if __name__ == '__main__': try_me() commit refs/heads/master mark :419 committer Martin Pool 1115628426 +1000 data 80 - RemoteBranch.__str__ and repr - Better code for locating root of remote branch from :418 M 644 inline bzrlib/commands.py data 28628 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass else: raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_args = ['file*'] takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False, file_list=None): b = Branch('.', lock_mode='r') b.show_status(show_all=all, file_list=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from branch import find_branch b = find_branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern""" takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') # FIXME: probably doesn't handle non-ascii patterns if os.path.exists(ifn): f = b.controlfile(ifn, 'rt') igns = f.read() f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' f = AtomicFile(ifn, 'wt') f.write(igns) f.commit() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them.""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False): ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/remotebranch.py data 6233 #! /usr/bin/env python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Proxy object for access to remote branches. At the moment remote branches are only for HTTP and only for read access. """ import gzip from sets import Set from cStringIO import StringIO import urllib2 from errors import BzrError, BzrCheckError from branch import Branch, BZR_BRANCH_FORMAT from trace import mutter # velocitynet.com.au transparently proxies connections and thereby # breaks keep-alive -- sucks! ENABLE_URLGRABBER = False def get_url(url, compressed=False): import urllib2 if compressed: url += '.gz' mutter("get_url %s" % url) url_f = urllib2.urlopen(url) if compressed: return gzip.GzipFile(fileobj=StringIO(url_f.read())) else: return url_f if ENABLE_URLGRABBER: import urlgrabber import urlgrabber.keepalive urlgrabber.keepalive.DEBUG = 0 def get_url(path, compressed=False): try: url = path if compressed: url += '.gz' url_f = urlgrabber.urlopen(url, keepalive=1, close_connection=0) if not compressed: return url_f else: return gzip.GzipFile(fileobj=StringIO(url_f.read())) except urllib2.URLError, e: raise BzrError("remote fetch failed: %r: %s" % (url, e)) def _find_remote_root(url): """Return the prefix URL that corresponds to the branch root.""" orig_url = url while True: try: ff = get_url(url + '/.bzr/branch-format') fmt = ff.read() ff.close() fmt = fmt.rstrip('\r\n') if fmt != BZR_BRANCH_FORMAT.rstrip('\r\n'): raise BzrError("sorry, branch format %r not supported at url %s" % (fmt, url)) return url except urllib2.URLError: pass try: idx = url.rindex('/') except ValueError: raise BzrError('no branch root found for URL %s' % orig_url) url = url[:idx] class RemoteBranch(Branch): def __init__(self, baseurl, find_root=True, lock_mode='r'): """Create new proxy for a remote branch.""" if lock_mode not in ('', 'r'): raise BzrError('lock mode %r is not supported for remote branches' % lock_mode) if find_root: self.baseurl = _find_remote_root(baseurl) else: self.baseurl = baseurl self._check_format() def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.baseurl) __repr__ = __str__ def controlfile(self, filename, mode): if mode not in ('rb', 'rt', 'r'): raise BzrError("file mode %r not supported for remote branches" % mode) return get_url(self.baseurl + '/.bzr/' + filename, False) def _need_readlock(self): # remote branch always safe for read pass def _need_writelock(self): raise BzrError("cannot get write lock on HTTP remote branch") def relpath(self, path): if not path.startswith(self.baseurl): raise BzrError('path %r is not under base URL %r' % (path, self.baseurl)) pl = len(self.baseurl) return path[pl:].lstrip('/') def get_revision(self, revision_id): from revision import Revision revf = get_url(self.baseurl + '/.bzr/revision-store/' + revision_id, True) r = Revision.read_xml(revf) if r.revision_id != revision_id: raise BzrCheckError('revision stored as {%s} actually contains {%s}' % (revision_id, r.revision_id)) return r def simple_walk(): from revision import Revision from branch import Branch from inventory import Inventory got_invs = Set() got_texts = Set() print 'read history' history = get_url('/.bzr/revision-history').readlines() num_revs = len(history) for i, rev_id in enumerate(history): rev_id = rev_id.rstrip() print 'read revision %d/%d' % (i, num_revs) # python gzip needs a seekable file (!!) but the HTTP response # isn't, so we need to buffer it rev_f = get_url('/.bzr/revision-store/%s' % rev_id, compressed=True) rev = Revision.read_xml(rev_f) print rev.message inv_id = rev.inventory_id if inv_id not in got_invs: print 'get inventory %s' % inv_id inv_f = get_url('/.bzr/inventory-store/%s' % inv_id, compressed=True) inv = Inventory.read_xml(inv_f) print '%4d inventory entries' % len(inv) for path, ie in inv.iter_entries(): text_id = ie.text_id if text_id == None: continue if text_id in got_texts: continue print ' fetch %s text {%s}' % (path, text_id) text_f = get_url('/.bzr/text-store/%s' % text_id, compressed=True) got_texts.add(text_id) got_invs.add(inv_id) print '----' def try_me(): BASE_URL = 'http://bazaar-ng.org/bzr/bzr.dev/' b = RemoteBranch(BASE_URL) ## print '\n'.join(b.revision_history()) from log import show_log show_log(b) if __name__ == '__main__': try_me() commit refs/heads/master mark :420 committer Martin Pool 1115686038 +1000 data 3 Doc from :419 M 644 inline NEWS data 6043 bzr-0.0.5 NOT RELEASED YET ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New ``bzr ignore PATTERN`` command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. * ``bzr log`` and ``bzr root`` can be given an http URL instead of a filename. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 29078 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass else: raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_args = ['file*'] takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False, file_list=None): b = Branch('.', lock_mode='r') b.show_status(show_all=all, file_list=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from branch import find_branch b = find_branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') # FIXME: probably doesn't handle non-ascii patterns if os.path.exists(ifn): f = b.controlfile(ifn, 'rt') igns = f.read() f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' f = AtomicFile(ifn, 'wt') f.write(igns) f.commit() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them.""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False): ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :421 committer Martin Pool 1115695732 +1000 data 3 doc from :420 M 644 inline bzrlib/commands.py data 29100 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass else: raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_args = ['file*'] takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False, file_list=None): b = Branch('.', lock_mode='r') b.show_status(show_all=all, file_list=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from branch import find_branch b = find_branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') # FIXME: probably doesn't handle non-ascii patterns if os.path.exists(ifn): f = b.controlfile(ifn, 'rt') igns = f.read() f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' f = AtomicFile(ifn, 'wt') f.write(igns) f.commit() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False): ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :422 committer Martin Pool 1115697045 +1000 data 33 - External-command patch from mpe from :421 M 644 inline NEWS data 6144 bzr-0.0.5 NOT RELEASED YET ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New ``bzr ignore PATTERN`` command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. * ``bzr log`` and ``bzr root`` can be given an http URL instead of a filename. * Commands can now be defined by external programs or scripts in a directory on $BZRPATH. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 31355 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_args = ['file*'] takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False, file_list=None): b = Branch('.', lock_mode='r') b.show_status(show_all=all, file_list=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from branch import find_branch b = find_branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') # FIXME: probably doesn't handle non-ascii patterns if os.path.exists(ifn): f = b.controlfile(ifn, 'rt') igns = f.read() f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' f = AtomicFile(ifn, 'wt') f.write(igns) f.commit() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False): ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :423 committer Martin Pool 1115697334 +1000 data 50 - Add fortune-cookie external plugin demonstration from :422 M 644 inline contrib/fortune data 306 #! /bin/sh # Put this on your $BZRPATH to use it to demonstrate bzr external plugins if [ x$1 == x--bzr-usage ] then # options echo "" # arguments echo "" exit 0 elif [ x$1 == x--bzr-help ] then echo "display a fortune cookie" echo exit 0 else /usr/games/fortune fi commit refs/heads/master mark :424 committer Martin Pool 1115697362 +1000 data 4 todo from :423 M 644 inline bzrlib/commands.py data 31488 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_args = ['file*'] takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False, file_list=None): b = Branch('.', lock_mode='r') b.show_status(show_all=all, file_list=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from branch import find_branch b = find_branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') # FIXME: probably doesn't handle non-ascii patterns if os.path.exists(ifn): f = b.controlfile(ifn, 'rt') igns = f.read() f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' f = AtomicFile(ifn, 'wt') f.write(igns) f.commit() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False): ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :425 committer Martin Pool 1115697722 +1000 data 44 - check from aaron for existence of a branch from :424 M 644 inline bzrlib/branch.py data 36078 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f.startswith('http://') or f.startswith('https://'): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. base Base directory of the branch. """ _lockmode = None def __init__(self, base, init=False, find_root=True, lock_mode='w'): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.lock(lock_mode) self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def lock(self, mode='w'): """Lock the on-disk branch, excluding other processes.""" try: import fcntl, errno if mode == 'w': lm = fcntl.LOCK_EX om = os.O_WRONLY | os.O_CREAT elif mode == 'r': lm = fcntl.LOCK_SH om = os.O_RDONLY else: raise BzrError("invalid locking mode %r" % mode) try: lockfile = os.open(self.controlfilename('branch-lock'), om) except OSError, e: if e.errno == errno.ENOENT: # might not exist on branches from <0.0.4 self.controlfile('branch-lock', 'w').close() lockfile = os.open(self.controlfilename('branch-lock'), om) else: raise e fcntl.lockf(lockfile, lm) def unlock(): fcntl.lockf(lockfile, fcntl.LOCK_UN) os.close(lockfile) self._lockmode = None self.unlock = unlock self._lockmode = mode except ImportError: warning("please write a locking method for platform %r" % sys.platform) def unlock(): self._lockmode = None self.unlock = unlock self._lockmode = mode def _need_readlock(self): if self._lockmode not in ['r', 'w']: raise BzrError('need read lock on branch, only have %r' % self._lockmode) def _need_writelock(self): if self._lockmode not in ['w']: raise BzrError('need write lock on branch, only have %r' % self._lockmode) def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" self._need_readlock() before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self._need_writelock() ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ self._need_writelock() # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" self._need_readlock() tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability self._need_writelock() if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. """ self._need_writelock() ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) self.append_revision(rev_id) if verbose: note("commited r%d" % self.revno()) def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" self._need_readlock() r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" self._need_readlock() i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" self._need_readlock() if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self._need_readlock() return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise BzrError('invalid history direction %r' % direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] else: return None def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" self._need_readlock() if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self._need_writelock() tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self._need_writelock() ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(self, show_all=False, file_list=None): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo """ self._need_readlock() # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = self.basis_tree() new = self.working_tree() items = diff_trees(old, new) # We want to filter out only if any file was provided in the file_list. if isinstance(file_list, list) and len(file_list): items = [item for item in items if item[3] in file_list] for fs, fid, oldname, newname, kind in items: if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("weird file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :426 committer Martin Pool 1115699559 +1000 data 54 - Skip symlinks during recursive add (path from aaron) from :425 M 644 inline bzrlib/add.py data 3221 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, sys import bzrlib from osutils import quotefn, appendpath from errors import bailout from trace import mutter, note def smart_add(file_list, verbose=False, recurse=True): """Add files to version, optionall recursing into directories. This is designed more towards DWIM for humans than API simplicity. For the specific behaviour see the help for cmd_add(). """ assert file_list user_list = file_list[:] assert not isinstance(file_list, basestring) b = bzrlib.branch.Branch(file_list[0], find_root=True) inv = b.read_working_inventory() tree = b.working_tree() count = 0 for f in file_list: kind = bzrlib.osutils.file_kind(f) if kind != 'file' and kind != 'directory': if f not in user_list: print "Skipping %s (can't add file of kind '%s')" % (f, kind) continue bailout("can't add file of kind %r" % kind) rf = b.relpath(f) af = b.abspath(rf) ## TODO: It's OK to add root but only in recursive mode bzrlib.mutter("smart add of %r" % f) if bzrlib.branch.is_control_file(af): bailout("cannot add control file %r" % af) kind = bzrlib.osutils.file_kind(f) if kind != 'file' and kind != 'directory': bailout("can't add file '%s' of kind %r" % (f, kind)) versioned = (inv.path2id(rf) != None) if rf == '': mutter("branch root doesn't need to be added") elif versioned: mutter("%r is already versioned" % f) else: file_id = bzrlib.branch.gen_file_id(rf) inv.add_path(rf, kind=kind, file_id=file_id) bzrlib.mutter("added %r kind %r file_id={%s}" % (rf, kind, file_id)) count += 1 if verbose: bzrlib.textui.show_status('A', kind, quotefn(f)) if kind == 'directory' and recurse: for subf in os.listdir(af): subp = appendpath(rf, subf) if subf == bzrlib.BZRDIR: mutter("skip control directory %r" % subp) elif tree.is_ignored(subp): mutter("skip ignored sub-file %r" % subp) else: mutter("queue to add sub-file %r" % (subp)) file_list.append(subp) if count > 0: if verbose: note('added %d' % count) b._write_inventory(inv) M 644 inline bzrlib/textui.py data 1145 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def show_status(state, kind, name): if kind == 'directory': # use this even on windows? kind_ch = '/' elif kind == 'symlink': kind_ch = '->' else: assert kind == 'file', ("can't handle file of type %r" % kind) kind_ch = '' assert len(state) == 1 print state + ' ' + name + kind_ch commit refs/heads/master mark :427 committer Martin Pool 1115700612 +1000 data 16 - statcache docs from :426 M 644 inline bzrlib/cache.py data 5269 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import stat, os, sha, time from binascii import b2a_qp, a2b_qp from trace import mutter """File stat cache to speed up tree comparisons. This module basically gives a quick way to find the SHA-1 and related information of a file in the working directory, without actually reading and hashing the whole file. This is done by maintaining a cache indexed by a file fingerprint of (path, size, mtime, ctime, ino, dev) pointing to the SHA-1. If the fingerprint has changed, we assume the file content has not changed either and the SHA-1 is therefore the same. If any of the fingerprint fields have changed then the file content *may* have changed, or it may not have. We need to reread the file contents to make sure, but this is not visible to the user or higher-level code (except as a delay of course). The mtime and ctime are stored with nanosecond fields, but not all filesystems give this level of precision. There is therefore a possible race: the file might be modified twice within a second without changing the size or mtime, and a SHA-1 cached from the first version would be wrong. We handle this by not recording a cached hash for any files which were modified in the current second and that therefore have the chance to change again before the second is up. The only known hole in this design is if the system clock jumps backwards crossing invocations of bzr. Please don't do that; use ntp to gradually adjust your clock or don't use bzr over the step. At the moment this is stored in a simple textfile; it might be nice to use a tdb instead. """ def fingerprint(path, abspath): try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) def write_cache(branch, entry_iter): outf = branch.controlfile('work-cache.tmp', 'wt') for entry in entry_iter: outf.write(entry[0] + ' ' + entry[1] + ' ') outf.write(b2a_qp(entry[2], True)) outf.write(' %d %d %d %d %d\n' % entry[3:]) outf.close() os.rename(branch.controlfilename('work-cache.tmp'), branch.controlfilename('work-cache')) def load_cache(branch): cache = {} try: cachefile = branch.controlfile('work-cache', 'rt') except IOError: return cache for l in cachefile: f = l.split(' ') file_id = f[0] if file_id in cache: raise BzrError("duplicated file_id in cache: {%s}" % file_id) cache[file_id] = (f[0], f[1], a2b_qp(f[2])) + tuple([long(x) for x in f[3:]]) return cache def _files_from_inventory(inv): for path, ie in inv.iter_entries(): if ie.kind != 'file': continue yield ie.file_id, path def build_cache(branch): inv = branch.read_working_inventory() cache = {} _update_cache_from_list(branch, cache, _files_from_inventory(inv)) def update_cache(branch, inv): # TODO: It's supposed to be faster to stat the files in order by inum. # We don't directly know the inum of the files of course but we do # know where they were last sighted, so we can sort by that. cache = load_cache(branch) return _update_cache_from_list(branch, cache, _files_from_inventory(inv)) def _update_cache_from_list(branch, cache, to_update): """Update the cache to have info on the named files. to_update is a sequence of (file_id, path) pairs. """ hardcheck = dirty = 0 for file_id, path in to_update: fap = branch.abspath(path) fp = fingerprint(fap, path) cacheentry = cache.get(file_id) if fp == None: # not here if cacheentry: del cache[file_id] dirty += 1 continue if cacheentry and (cacheentry[3:] == fp): continue # all stat fields unchanged hardcheck += 1 dig = sha.new(file(fap, 'rb').read()).hexdigest() if cacheentry == None or dig != cacheentry[1]: # if there was no previous entry for this file, or if the # SHA has changed, then update the cache cacheentry = (file_id, dig, path) + fp cache[file_id] = cacheentry dirty += 1 mutter('work cache: read %d files, %d changed' % (hardcheck, dirty)) if dirty: write_cache(branch, cache.itervalues()) return cache commit refs/heads/master mark :428 committer Martin Pool 1115704859 +1000 data 73 - Use AtomicFile to update statcache. - New closed property on AtomicFile from :427 M 644 inline bzrlib/atomicfile.py data 1891 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA class AtomicFile: """A file that does an atomic-rename to move into place. This also causes hardlinks to break when it's written out. Open this as for a regular file, then use commit() to move into place or abort() to cancel. You may wish to wrap this in a codecs.EncodedFile to do unicode encoding. """ def __init__(self, filename, mode='wb'): if mode != 'wb' and mode != 'wt': raise ValueError("invalid AtomicFile mode %r" % mode) import os, socket self.tmpfilename = '%s.tmp.%d.%s' % (filename, os.getpid(), socket.gethostname()) self.realfilename = filename self.f = open(self.tmpfilename, mode) self.write = self.f.write self.closed = property(f.closed) def commit(self): import sys, os self.f.close() if sys.platform == 'win32': os.remove(self.realfilename) os.rename(self.tmpfilename, self.realfilename) def abort(self): import os self.f.close() os.remove(self.tmpfilename) M 644 inline bzrlib/cache.py data 5335 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import stat, os, sha, time from binascii import b2a_qp, a2b_qp from trace import mutter from errors import BzrError """File stat cache to speed up tree comparisons. This module basically gives a quick way to find the SHA-1 and related information of a file in the working directory, without actually reading and hashing the whole file. This is done by maintaining a cache indexed by a file fingerprint of (path, size, mtime, ctime, ino, dev) pointing to the SHA-1. If the fingerprint has changed, we assume the file content has not changed either and the SHA-1 is therefore the same. If any of the fingerprint fields have changed then the file content *may* have changed, or it may not have. We need to reread the file contents to make sure, but this is not visible to the user or higher-level code (except as a delay of course). The mtime and ctime are stored with nanosecond fields, but not all filesystems give this level of precision. There is therefore a possible race: the file might be modified twice within a second without changing the size or mtime, and a SHA-1 cached from the first version would be wrong. We handle this by not recording a cached hash for any files which were modified in the current second and that therefore have the chance to change again before the second is up. The only known hole in this design is if the system clock jumps backwards crossing invocations of bzr. Please don't do that; use ntp to gradually adjust your clock or don't use bzr over the step. At the moment this is stored in a simple textfile; it might be nice to use a tdb instead. """ def fingerprint(path, abspath): try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) def write_cache(branch, entry_iter): from atomicfile import AtomicFile outf = AtomicFile(branch.controlfilename('work-cache.tmp'), 'wt') try: for entry in entry_iter: outf.write(entry[0] + ' ' + entry[1] + ' ') outf.write(b2a_qp(entry[2], True)) outf.write(' %d %d %d %d %d\n' % entry[3:]) outf.commit() finally: if not outf.closed: outf.abort() def load_cache(branch): cache = {} try: cachefile = branch.controlfile('work-cache', 'rt') except IOError: return cache for l in cachefile: f = l.split(' ') file_id = f[0] if file_id in cache: raise BzrError("duplicated file_id in cache: {%s}" % file_id) cache[file_id] = (f[0], f[1], a2b_qp(f[2])) + tuple([long(x) for x in f[3:]]) return cache def _files_from_inventory(inv): for path, ie in inv.iter_entries(): if ie.kind != 'file': continue yield ie.file_id, path def build_cache(branch): inv = branch.read_working_inventory() cache = {} _update_cache_from_list(branch, cache, _files_from_inventory(inv)) def update_cache(branch, inv): # TODO: It's supposed to be faster to stat the files in order by inum. # We don't directly know the inum of the files of course but we do # know where they were last sighted, so we can sort by that. cache = load_cache(branch) return _update_cache_from_list(branch, cache, _files_from_inventory(inv)) def _update_cache_from_list(branch, cache, to_update): """Update the cache to have info on the named files. to_update is a sequence of (file_id, path) pairs. """ hardcheck = dirty = 0 for file_id, path in to_update: fap = branch.abspath(path) fp = fingerprint(fap, path) cacheentry = cache.get(file_id) if fp == None: # not here if cacheentry: del cache[file_id] dirty += 1 continue if cacheentry and (cacheentry[3:] == fp): continue # all stat fields unchanged hardcheck += 1 dig = sha.new(file(fap, 'rb').read()).hexdigest() if cacheentry == None or dig != cacheentry[1]: # if there was no previous entry for this file, or if the # SHA has changed, then update the cache cacheentry = (file_id, dig, path) + fp cache[file_id] = cacheentry dirty += 1 mutter('work cache: read %d files, %d changed' % (hardcheck, dirty)) if dirty: write_cache(branch, cache.itervalues()) return cache commit refs/heads/master mark :429 committer Martin Pool 1115705236 +1000 data 102 - New command update-stat-cache for testing - work-cache always stored with unix newlines and in ascii from :428 R bzrlib/cache.py bzrlib/statcache.py M 644 inline bzrlib/atomicfile.py data 1896 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA class AtomicFile: """A file that does an atomic-rename to move into place. This also causes hardlinks to break when it's written out. Open this as for a regular file, then use commit() to move into place or abort() to cancel. You may wish to wrap this in a codecs.EncodedFile to do unicode encoding. """ def __init__(self, filename, mode='wb'): if mode != 'wb' and mode != 'wt': raise ValueError("invalid AtomicFile mode %r" % mode) import os, socket self.tmpfilename = '%s.tmp.%d.%s' % (filename, os.getpid(), socket.gethostname()) self.realfilename = filename self.f = open(self.tmpfilename, mode) self.write = self.f.write self.closed = property(self.f.closed) def commit(self): import sys, os self.f.close() if sys.platform == 'win32': os.remove(self.realfilename) os.rename(self.tmpfilename, self.realfilename) def abort(self): import os self.f.close() os.remove(self.tmpfilename) M 644 inline bzrlib/commands.py data 31777 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_args = ['file*'] takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False, file_list=None): b = Branch('.', lock_mode='r') b.show_status(show_all=all, file_list=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from branch import find_branch b = find_branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') # FIXME: probably doesn't handle non-ascii patterns if os.path.exists(ifn): f = b.controlfile(ifn, 'rt') igns = f.read() f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' f = AtomicFile(ifn, 'wt') f.write(igns) f.commit() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False): ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() statcache.update_cache(b, inv) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/statcache.py data 5335 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import stat, os, sha, time from binascii import b2a_qp, a2b_qp from trace import mutter from errors import BzrError """File stat cache to speed up tree comparisons. This module basically gives a quick way to find the SHA-1 and related information of a file in the working directory, without actually reading and hashing the whole file. This is done by maintaining a cache indexed by a file fingerprint of (path, size, mtime, ctime, ino, dev) pointing to the SHA-1. If the fingerprint has changed, we assume the file content has not changed either and the SHA-1 is therefore the same. If any of the fingerprint fields have changed then the file content *may* have changed, or it may not have. We need to reread the file contents to make sure, but this is not visible to the user or higher-level code (except as a delay of course). The mtime and ctime are stored with nanosecond fields, but not all filesystems give this level of precision. There is therefore a possible race: the file might be modified twice within a second without changing the size or mtime, and a SHA-1 cached from the first version would be wrong. We handle this by not recording a cached hash for any files which were modified in the current second and that therefore have the chance to change again before the second is up. The only known hole in this design is if the system clock jumps backwards crossing invocations of bzr. Please don't do that; use ntp to gradually adjust your clock or don't use bzr over the step. At the moment this is stored in a simple textfile; it might be nice to use a tdb instead. """ def fingerprint(path, abspath): try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) def write_cache(branch, entry_iter): from atomicfile import AtomicFile outf = AtomicFile(branch.controlfilename('work-cache.tmp'), 'wb') try: for entry in entry_iter: outf.write(entry[0] + ' ' + entry[1] + ' ') outf.write(b2a_qp(entry[2], True)) outf.write(' %d %d %d %d %d\n' % entry[3:]) outf.commit() finally: if not outf.closed: outf.abort() def load_cache(branch): cache = {} try: cachefile = branch.controlfile('work-cache', 'rb') except IOError: return cache for l in cachefile: f = l.split(' ') file_id = f[0] if file_id in cache: raise BzrError("duplicated file_id in cache: {%s}" % file_id) cache[file_id] = (f[0], f[1], a2b_qp(f[2])) + tuple([long(x) for x in f[3:]]) return cache def _files_from_inventory(inv): for path, ie in inv.iter_entries(): if ie.kind != 'file': continue yield ie.file_id, path def build_cache(branch): inv = branch.read_working_inventory() cache = {} _update_cache_from_list(branch, cache, _files_from_inventory(inv)) def update_cache(branch, inv): # TODO: It's supposed to be faster to stat the files in order by inum. # We don't directly know the inum of the files of course but we do # know where they were last sighted, so we can sort by that. cache = load_cache(branch) return _update_cache_from_list(branch, cache, _files_from_inventory(inv)) def _update_cache_from_list(branch, cache, to_update): """Update the cache to have info on the named files. to_update is a sequence of (file_id, path) pairs. """ hardcheck = dirty = 0 for file_id, path in to_update: fap = branch.abspath(path) fp = fingerprint(fap, path) cacheentry = cache.get(file_id) if fp == None: # not here if cacheentry: del cache[file_id] dirty += 1 continue if cacheentry and (cacheentry[3:] == fp): continue # all stat fields unchanged hardcheck += 1 dig = sha.new(file(fap, 'rb').read()).hexdigest() if cacheentry == None or dig != cacheentry[1]: # if there was no previous entry for this file, or if the # SHA has changed, then update the cache cacheentry = (file_id, dig, path) + fp cache[file_id] = cacheentry dirty += 1 mutter('work cache: read %d files, %d changed' % (hardcheck, dirty)) if dirty: write_cache(branch, cache.itervalues()) return cache commit refs/heads/master mark :430 committer Martin Pool 1115705398 +1000 data 3 doc from :429 M 644 inline bzrlib/branch.py data 36222 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree, WorkingTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f.startswith('http://') or f.startswith('https://'): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. base Base directory of the branch. """ _lockmode = None def __init__(self, base, init=False, find_root=True, lock_mode='w'): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.lock(lock_mode) self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def lock(self, mode='w'): """Lock the on-disk branch, excluding other processes.""" try: import fcntl, errno if mode == 'w': lm = fcntl.LOCK_EX om = os.O_WRONLY | os.O_CREAT elif mode == 'r': lm = fcntl.LOCK_SH om = os.O_RDONLY else: raise BzrError("invalid locking mode %r" % mode) try: lockfile = os.open(self.controlfilename('branch-lock'), om) except OSError, e: if e.errno == errno.ENOENT: # might not exist on branches from <0.0.4 self.controlfile('branch-lock', 'w').close() lockfile = os.open(self.controlfilename('branch-lock'), om) else: raise e fcntl.lockf(lockfile, lm) def unlock(): fcntl.lockf(lockfile, fcntl.LOCK_UN) os.close(lockfile) self._lockmode = None self.unlock = unlock self._lockmode = mode except ImportError: warning("please write a locking method for platform %r" % sys.platform) def unlock(): self._lockmode = None self.unlock = unlock self._lockmode = mode def _need_readlock(self): if self._lockmode not in ['r', 'w']: raise BzrError('need read lock on branch, only have %r' % self._lockmode) def _need_writelock(self): if self._lockmode not in ['w']: raise BzrError('need write lock on branch, only have %r' % self._lockmode) def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" self._need_readlock() before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self._need_writelock() ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ self._need_writelock() # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" self._need_readlock() tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability self._need_writelock() if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. """ self._need_writelock() ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) self.append_revision(rev_id) if verbose: note("commited r%d" % self.revno()) def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" self._need_readlock() r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" self._need_readlock() i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" self._need_readlock() if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self._need_readlock() return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise BzrError('invalid history direction %r' % direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] else: return None def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" self._need_readlock() if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self._need_writelock() tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self._need_writelock() ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(self, show_all=False, file_list=None): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo """ self._need_readlock() # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = self.basis_tree() new = self.working_tree() items = diff_trees(old, new) # We want to filter out only if any file was provided in the file_list. if isinstance(file_list, list) and len(file_list): items = [item for item in items if item[3] in file_list] for fs, fid, oldname, newname, kind in items: if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("weird file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :431 committer Martin Pool 1115705629 +1000 data 68 - stat cache is written in utf-8 to accomodate non-ascii filenames from :430 M 644 inline bzrlib/atomicfile.py data 2010 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA class AtomicFile: """A file that does an atomic-rename to move into place. This also causes hardlinks to break when it's written out. Open this as for a regular file, then use commit() to move into place or abort() to cancel. An encoding can be specified; otherwise the default is ascii. """ def __init__(self, filename, mode='wb', encoding=None): if mode != 'wb' and mode != 'wt': raise ValueError("invalid AtomicFile mode %r" % mode) import os, socket self.tmpfilename = '%s.tmp.%d.%s' % (filename, os.getpid(), socket.gethostname()) self.realfilename = filename self.f = open(self.tmpfilename, mode) if encoding: import codecs self.f = codecs.EncodedFile(self.f, encoding) self.write = self.f.write self.closed = property(self.f.closed) def commit(self): import sys, os self.f.close() if sys.platform == 'win32': os.remove(self.realfilename) os.rename(self.tmpfilename, self.realfilename) def abort(self): import os self.f.close() os.remove(self.tmpfilename) M 644 inline bzrlib/statcache.py data 5338 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import stat, os, sha, time from binascii import b2a_qp, a2b_qp from trace import mutter from errors import BzrError """File stat cache to speed up tree comparisons. This module basically gives a quick way to find the SHA-1 and related information of a file in the working directory, without actually reading and hashing the whole file. This is done by maintaining a cache indexed by a file fingerprint of (path, size, mtime, ctime, ino, dev) pointing to the SHA-1. If the fingerprint has changed, we assume the file content has not changed either and the SHA-1 is therefore the same. If any of the fingerprint fields have changed then the file content *may* have changed, or it may not have. We need to reread the file contents to make sure, but this is not visible to the user or higher-level code (except as a delay of course). The mtime and ctime are stored with nanosecond fields, but not all filesystems give this level of precision. There is therefore a possible race: the file might be modified twice within a second without changing the size or mtime, and a SHA-1 cached from the first version would be wrong. We handle this by not recording a cached hash for any files which were modified in the current second and that therefore have the chance to change again before the second is up. The only known hole in this design is if the system clock jumps backwards crossing invocations of bzr. Please don't do that; use ntp to gradually adjust your clock or don't use bzr over the step. At the moment this is stored in a simple textfile; it might be nice to use a tdb instead. """ def fingerprint(path, abspath): try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) def write_cache(branch, entry_iter): from atomicfile import AtomicFile outf = AtomicFile(branch.controlfilename('work-cache'), 'w', 'utf-8') try: for entry in entry_iter: outf.write(entry[0] + ' ' + entry[1] + ' ') outf.write(b2a_qp(entry[2], True)) outf.write(' %d %d %d %d %d\n' % entry[3:]) outf.commit() finally: if not outf.closed: outf.abort() def load_cache(branch): cache = {} try: cachefile = branch.controlfile('work-cache', 'r') except IOError: return cache for l in cachefile: f = l.split(' ') file_id = f[0] if file_id in cache: raise BzrError("duplicated file_id in cache: {%s}" % file_id) cache[file_id] = (f[0], f[1], a2b_qp(f[2])) + tuple([long(x) for x in f[3:]]) return cache def _files_from_inventory(inv): for path, ie in inv.iter_entries(): if ie.kind != 'file': continue yield ie.file_id, path def build_cache(branch): inv = branch.read_working_inventory() cache = {} _update_cache_from_list(branch, cache, _files_from_inventory(inv)) def update_cache(branch, inv): # TODO: It's supposed to be faster to stat the files in order by inum. # We don't directly know the inum of the files of course but we do # know where they were last sighted, so we can sort by that. cache = load_cache(branch) return _update_cache_from_list(branch, cache, _files_from_inventory(inv)) def _update_cache_from_list(branch, cache, to_update): """Update the cache to have info on the named files. to_update is a sequence of (file_id, path) pairs. """ hardcheck = dirty = 0 for file_id, path in to_update: fap = branch.abspath(path) fp = fingerprint(fap, path) cacheentry = cache.get(file_id) if fp == None: # not here if cacheentry: del cache[file_id] dirty += 1 continue if cacheentry and (cacheentry[3:] == fp): continue # all stat fields unchanged hardcheck += 1 dig = sha.new(file(fap, 'rb').read()).hexdigest() if cacheentry == None or dig != cacheentry[1]: # if there was no previous entry for this file, or if the # SHA has changed, then update the cache cacheentry = (file_id, dig, path) + fp cache[file_id] = cacheentry dirty += 1 mutter('work cache: read %d files, %d changed' % (hardcheck, dirty)) if dirty: write_cache(branch, cache.itervalues()) return cache commit refs/heads/master mark :432 committer Martin Pool 1115705696 +1000 data 28 - fix AtomicFile constructor from :431 M 644 inline bzrlib/statcache.py data 5339 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import stat, os, sha, time from binascii import b2a_qp, a2b_qp from trace import mutter from errors import BzrError """File stat cache to speed up tree comparisons. This module basically gives a quick way to find the SHA-1 and related information of a file in the working directory, without actually reading and hashing the whole file. This is done by maintaining a cache indexed by a file fingerprint of (path, size, mtime, ctime, ino, dev) pointing to the SHA-1. If the fingerprint has changed, we assume the file content has not changed either and the SHA-1 is therefore the same. If any of the fingerprint fields have changed then the file content *may* have changed, or it may not have. We need to reread the file contents to make sure, but this is not visible to the user or higher-level code (except as a delay of course). The mtime and ctime are stored with nanosecond fields, but not all filesystems give this level of precision. There is therefore a possible race: the file might be modified twice within a second without changing the size or mtime, and a SHA-1 cached from the first version would be wrong. We handle this by not recording a cached hash for any files which were modified in the current second and that therefore have the chance to change again before the second is up. The only known hole in this design is if the system clock jumps backwards crossing invocations of bzr. Please don't do that; use ntp to gradually adjust your clock or don't use bzr over the step. At the moment this is stored in a simple textfile; it might be nice to use a tdb instead. """ def fingerprint(path, abspath): try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) def write_cache(branch, entry_iter): from atomicfile import AtomicFile outf = AtomicFile(branch.controlfilename('work-cache'), 'wb', 'utf-8') try: for entry in entry_iter: outf.write(entry[0] + ' ' + entry[1] + ' ') outf.write(b2a_qp(entry[2], True)) outf.write(' %d %d %d %d %d\n' % entry[3:]) outf.commit() finally: if not outf.closed: outf.abort() def load_cache(branch): cache = {} try: cachefile = branch.controlfile('work-cache', 'r') except IOError: return cache for l in cachefile: f = l.split(' ') file_id = f[0] if file_id in cache: raise BzrError("duplicated file_id in cache: {%s}" % file_id) cache[file_id] = (f[0], f[1], a2b_qp(f[2])) + tuple([long(x) for x in f[3:]]) return cache def _files_from_inventory(inv): for path, ie in inv.iter_entries(): if ie.kind != 'file': continue yield ie.file_id, path def build_cache(branch): inv = branch.read_working_inventory() cache = {} _update_cache_from_list(branch, cache, _files_from_inventory(inv)) def update_cache(branch, inv): # TODO: It's supposed to be faster to stat the files in order by inum. # We don't directly know the inum of the files of course but we do # know where they were last sighted, so we can sort by that. cache = load_cache(branch) return _update_cache_from_list(branch, cache, _files_from_inventory(inv)) def _update_cache_from_list(branch, cache, to_update): """Update the cache to have info on the named files. to_update is a sequence of (file_id, path) pairs. """ hardcheck = dirty = 0 for file_id, path in to_update: fap = branch.abspath(path) fp = fingerprint(fap, path) cacheentry = cache.get(file_id) if fp == None: # not here if cacheentry: del cache[file_id] dirty += 1 continue if cacheentry and (cacheentry[3:] == fp): continue # all stat fields unchanged hardcheck += 1 dig = sha.new(file(fap, 'rb').read()).hexdigest() if cacheentry == None or dig != cacheentry[1]: # if there was no previous entry for this file, or if the # SHA has changed, then update the cache cacheentry = (file_id, dig, path) + fp cache[file_id] = cacheentry dirty += 1 mutter('work cache: read %d files, %d changed' % (hardcheck, dirty)) if dirty: write_cache(branch, cache.itervalues()) return cache commit refs/heads/master mark :433 committer Martin Pool 1115705866 +1000 data 27 - more trace from statcache from :432 M 644 inline bzrlib/statcache.py data 5375 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import stat, os, sha, time from binascii import b2a_qp, a2b_qp from trace import mutter from errors import BzrError """File stat cache to speed up tree comparisons. This module basically gives a quick way to find the SHA-1 and related information of a file in the working directory, without actually reading and hashing the whole file. This is done by maintaining a cache indexed by a file fingerprint of (path, size, mtime, ctime, ino, dev) pointing to the SHA-1. If the fingerprint has changed, we assume the file content has not changed either and the SHA-1 is therefore the same. If any of the fingerprint fields have changed then the file content *may* have changed, or it may not have. We need to reread the file contents to make sure, but this is not visible to the user or higher-level code (except as a delay of course). The mtime and ctime are stored with nanosecond fields, but not all filesystems give this level of precision. There is therefore a possible race: the file might be modified twice within a second without changing the size or mtime, and a SHA-1 cached from the first version would be wrong. We handle this by not recording a cached hash for any files which were modified in the current second and that therefore have the chance to change again before the second is up. The only known hole in this design is if the system clock jumps backwards crossing invocations of bzr. Please don't do that; use ntp to gradually adjust your clock or don't use bzr over the step. At the moment this is stored in a simple textfile; it might be nice to use a tdb instead. """ def fingerprint(path, abspath): try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) def write_cache(branch, entry_iter): from atomicfile import AtomicFile outf = AtomicFile(branch.controlfilename('work-cache'), 'wb', 'utf-8') try: for entry in entry_iter: outf.write(entry[0] + ' ' + entry[1] + ' ') outf.write(b2a_qp(entry[2], True)) outf.write(' %d %d %d %d %d\n' % entry[3:]) outf.commit() finally: if not outf.closed: outf.abort() def load_cache(branch): cache = {} try: cachefile = branch.controlfile('work-cache', 'r') except IOError: return cache for l in cachefile: f = l.split(' ') file_id = f[0] if file_id in cache: raise BzrError("duplicated file_id in cache: {%s}" % file_id) cache[file_id] = (f[0], f[1], a2b_qp(f[2])) + tuple([long(x) for x in f[3:]]) return cache def _files_from_inventory(inv): for path, ie in inv.iter_entries(): if ie.kind != 'file': continue yield ie.file_id, path def build_cache(branch): inv = branch.read_working_inventory() cache = {} _update_cache_from_list(branch, cache, _files_from_inventory(inv)) def update_cache(branch, inv): # TODO: It's supposed to be faster to stat the files in order by inum. # We don't directly know the inum of the files of course but we do # know where they were last sighted, so we can sort by that. cache = load_cache(branch) return _update_cache_from_list(branch, cache, _files_from_inventory(inv)) def _update_cache_from_list(branch, cache, to_update): """Update the cache to have info on the named files. to_update is a sequence of (file_id, path) pairs. """ hardcheck = dirty = 0 for file_id, path in to_update: fap = branch.abspath(path) fp = fingerprint(fap, path) cacheentry = cache.get(file_id) if fp == None: # not here if cacheentry: del cache[file_id] dirty += 1 continue if cacheentry and (cacheentry[3:] == fp): continue # all stat fields unchanged hardcheck += 1 dig = sha.new(file(fap, 'rb').read()).hexdigest() if cacheentry == None or dig != cacheentry[1]: # if there was no previous entry for this file, or if the # SHA has changed, then update the cache cacheentry = (file_id, dig, path) + fp cache[file_id] = cacheentry dirty += 1 mutter('work cache: read %d files, %d changed, %d in cache' % (hardcheck, dirty, len(cache))) if dirty: write_cache(branch, cache.itervalues()) return cache commit refs/heads/master mark :434 committer Martin Pool 1115706067 +1000 data 3 doc from :433 M 644 inline bzrlib/statcache.py data 5492 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import stat, os, sha, time from binascii import b2a_qp, a2b_qp from trace import mutter from errors import BzrError """File stat cache to speed up tree comparisons. This module basically gives a quick way to find the SHA-1 and related information of a file in the working directory, without actually reading and hashing the whole file. This is done by maintaining a cache indexed by a file fingerprint of (path, size, mtime, ctime, ino, dev) pointing to the SHA-1. If the fingerprint has changed, we assume the file content has not changed either and the SHA-1 is therefore the same. If any of the fingerprint fields have changed then the file content *may* have changed, or it may not have. We need to reread the file contents to make sure, but this is not visible to the user or higher-level code (except as a delay of course). The mtime and ctime are stored with nanosecond fields, but not all filesystems give this level of precision. There is therefore a possible race: the file might be modified twice within a second without changing the size or mtime, and a SHA-1 cached from the first version would be wrong. We handle this by not recording a cached hash for any files which were modified in the current second and that therefore have the chance to change again before the second is up. The only known hole in this design is if the system clock jumps backwards crossing invocations of bzr. Please don't do that; use ntp to gradually adjust your clock or don't use bzr over the step. At the moment this is stored in a simple textfile; it might be nice to use a tdb instead. The cache is represented as a map from file_id to a tuple of (file_id, sha1, path, size, mtime, ctime, ino, dev). """ def fingerprint(path, abspath): try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) def _write_cache(branch, entry_iter): from atomicfile import AtomicFile outf = AtomicFile(branch.controlfilename('work-cache'), 'wb', 'utf-8') try: for entry in entry_iter: outf.write(entry[0] + ' ' + entry[1] + ' ') outf.write(b2a_qp(entry[2], True)) outf.write(' %d %d %d %d %d\n' % entry[3:]) outf.commit() finally: if not outf.closed: outf.abort() def load_cache(branch): cache = {} try: cachefile = branch.controlfile('work-cache', 'r') except IOError: return cache for l in cachefile: f = l.split(' ') file_id = f[0] if file_id in cache: raise BzrError("duplicated file_id in cache: {%s}" % file_id) cache[file_id] = (f[0], f[1], a2b_qp(f[2])) + tuple([long(x) for x in f[3:]]) return cache def _files_from_inventory(inv): for path, ie in inv.iter_entries(): if ie.kind != 'file': continue yield ie.file_id, path def build_cache(branch): inv = branch.read_working_inventory() cache = {} _update_cache_from_list(branch, cache, _files_from_inventory(inv)) def update_cache(branch, inv): # TODO: It's supposed to be faster to stat the files in order by inum. # We don't directly know the inum of the files of course but we do # know where they were last sighted, so we can sort by that. cache = load_cache(branch) return _update_cache_from_list(branch, cache, _files_from_inventory(inv)) def _update_cache_from_list(branch, cache, to_update): """Update the cache to have info on the named files. to_update is a sequence of (file_id, path) pairs. """ hardcheck = dirty = 0 for file_id, path in to_update: fap = branch.abspath(path) fp = fingerprint(fap, path) cacheentry = cache.get(file_id) if fp == None: # not here if cacheentry: del cache[file_id] dirty += 1 continue if cacheentry and (cacheentry[3:] == fp): continue # all stat fields unchanged hardcheck += 1 dig = sha.new(file(fap, 'rb').read()).hexdigest() if cacheentry == None or dig != cacheentry[1]: # if there was no previous entry for this file, or if the # SHA has changed, then update the cache cacheentry = (file_id, dig, path) + fp cache[file_id] = cacheentry dirty += 1 mutter('work cache: read %d files, %d changed, %d in cache' % (hardcheck, dirty, len(cache))) if dirty: _write_cache(branch, cache.itervalues()) return cache commit refs/heads/master mark :435 committer Martin Pool 1115706112 +1000 data 46 - Always call it 'statcache' not 'work cache'. from :434 M 644 inline bzrlib/statcache.py data 5491 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import stat, os, sha, time from binascii import b2a_qp, a2b_qp from trace import mutter from errors import BzrError """File stat cache to speed up tree comparisons. This module basically gives a quick way to find the SHA-1 and related information of a file in the working directory, without actually reading and hashing the whole file. This is done by maintaining a cache indexed by a file fingerprint of (path, size, mtime, ctime, ino, dev) pointing to the SHA-1. If the fingerprint has changed, we assume the file content has not changed either and the SHA-1 is therefore the same. If any of the fingerprint fields have changed then the file content *may* have changed, or it may not have. We need to reread the file contents to make sure, but this is not visible to the user or higher-level code (except as a delay of course). The mtime and ctime are stored with nanosecond fields, but not all filesystems give this level of precision. There is therefore a possible race: the file might be modified twice within a second without changing the size or mtime, and a SHA-1 cached from the first version would be wrong. We handle this by not recording a cached hash for any files which were modified in the current second and that therefore have the chance to change again before the second is up. The only known hole in this design is if the system clock jumps backwards crossing invocations of bzr. Please don't do that; use ntp to gradually adjust your clock or don't use bzr over the step. At the moment this is stored in a simple textfile; it might be nice to use a tdb instead. The cache is represented as a map from file_id to a tuple of (file_id, sha1, path, size, mtime, ctime, ino, dev). """ def fingerprint(path, abspath): try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) def _write_cache(branch, entry_iter): from atomicfile import AtomicFile outf = AtomicFile(branch.controlfilename('stat-cache'), 'wb', 'utf-8') try: for entry in entry_iter: outf.write(entry[0] + ' ' + entry[1] + ' ') outf.write(b2a_qp(entry[2], True)) outf.write(' %d %d %d %d %d\n' % entry[3:]) outf.commit() finally: if not outf.closed: outf.abort() def load_cache(branch): cache = {} try: cachefile = branch.controlfile('stat-cache', 'r') except IOError: return cache for l in cachefile: f = l.split(' ') file_id = f[0] if file_id in cache: raise BzrError("duplicated file_id in cache: {%s}" % file_id) cache[file_id] = (f[0], f[1], a2b_qp(f[2])) + tuple([long(x) for x in f[3:]]) return cache def _files_from_inventory(inv): for path, ie in inv.iter_entries(): if ie.kind != 'file': continue yield ie.file_id, path def build_cache(branch): inv = branch.read_working_inventory() cache = {} _update_cache_from_list(branch, cache, _files_from_inventory(inv)) def update_cache(branch, inv): # TODO: It's supposed to be faster to stat the files in order by inum. # We don't directly know the inum of the files of course but we do # know where they were last sighted, so we can sort by that. cache = load_cache(branch) return _update_cache_from_list(branch, cache, _files_from_inventory(inv)) def _update_cache_from_list(branch, cache, to_update): """Update the cache to have info on the named files. to_update is a sequence of (file_id, path) pairs. """ hardcheck = dirty = 0 for file_id, path in to_update: fap = branch.abspath(path) fp = fingerprint(fap, path) cacheentry = cache.get(file_id) if fp == None: # not here if cacheentry: del cache[file_id] dirty += 1 continue if cacheentry and (cacheentry[3:] == fp): continue # all stat fields unchanged hardcheck += 1 dig = sha.new(file(fap, 'rb').read()).hexdigest() if cacheentry == None or dig != cacheentry[1]: # if there was no previous entry for this file, or if the # SHA has changed, then update the cache cacheentry = (file_id, dig, path) + fp cache[file_id] = cacheentry dirty += 1 mutter('statcache: read %d files, %d changed, %d in cache' % (hardcheck, dirty, len(cache))) if dirty: _write_cache(branch, cache.itervalues()) return cache commit refs/heads/master mark :436 committer Martin Pool 1115706881 +1000 data 130 - Avoid dangerous files when writing out stat cache - remove build_cache in favour of just update_cache with parameter to flush from :435 M 644 inline bzrlib/commands.py data 31731 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_args = ['file*'] takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False, file_list=None): b = Branch('.', lock_mode='r') b.show_status(show_all=all, file_list=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from branch import find_branch b = find_branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') # FIXME: probably doesn't handle non-ascii patterns if os.path.exists(ifn): f = b.controlfile(ifn, 'rt') igns = f.read() f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' f = AtomicFile(ifn, 'wt') f.write(igns) f.commit() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False): ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/statcache.py data 6267 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import stat, os, sha, time from binascii import b2a_qp, a2b_qp from trace import mutter from errors import BzrError """File stat cache to speed up tree comparisons. This module basically gives a quick way to find the SHA-1 and related information of a file in the working directory, without actually reading and hashing the whole file. This is done by maintaining a cache indexed by a file fingerprint of (path, size, mtime, ctime, ino, dev) pointing to the SHA-1. If the fingerprint has changed, we assume the file content has not changed either and the SHA-1 is therefore the same. If any of the fingerprint fields have changed then the file content *may* have changed, or it may not have. We need to reread the file contents to make sure, but this is not visible to the user or higher-level code (except as a delay of course). The mtime and ctime are stored with nanosecond fields, but not all filesystems give this level of precision. There is therefore a possible race: the file might be modified twice within a second without changing the size or mtime, and a SHA-1 cached from the first version would be wrong. We handle this by not recording a cached hash for any files which were modified in the current second and that therefore have the chance to change again before the second is up. The only known hole in this design is if the system clock jumps backwards crossing invocations of bzr. Please don't do that; use ntp to gradually adjust your clock or don't use bzr over the step. At the moment this is stored in a simple textfile; it might be nice to use a tdb instead. The cache is represented as a map from file_id to a tuple of (file_id, sha1, path, size, mtime, ctime, ino, dev). """ FP_SIZE = 0 FP_MTIME = 1 FP_CTIME = 2 FP_INO = 3 FP_DEV = 4 def fingerprint(path, abspath): try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) def _write_cache(branch, entry_iter, dangerfiles): from atomicfile import AtomicFile outf = AtomicFile(branch.controlfilename('stat-cache'), 'wb', 'utf-8') try: for entry in entry_iter: if entry[0] in dangerfiles: continue outf.write(entry[0] + ' ' + entry[1] + ' ') outf.write(b2a_qp(entry[2], True)) outf.write(' %d %d %d %d %d\n' % entry[3:]) outf.commit() finally: if not outf.closed: outf.abort() def load_cache(branch): cache = {} try: cachefile = branch.controlfile('stat-cache', 'r') except IOError: return cache for l in cachefile: f = l.split(' ') file_id = f[0] if file_id in cache: raise BzrError("duplicated file_id in cache: {%s}" % file_id) cache[file_id] = (f[0], f[1], a2b_qp(f[2])) + tuple([long(x) for x in f[3:]]) return cache def _files_from_inventory(inv): for path, ie in inv.iter_entries(): if ie.kind != 'file': continue yield ie.file_id, path def update_cache(branch, flush=False): """Update and return the cache for the branch. The returned cache may contain entries that have not been written to disk for files recently touched. flush -- discard any previous cache and recalculate from scratch. """ # TODO: It's supposed to be faster to stat the files in order by inum. # We don't directly know the inum of the files of course but we do # know where they were last sighted, so we can sort by that. if flush: cache = {} else: cache = load_cache(branch) inv = branch.read_working_inventory() return _update_cache_from_list(branch, cache, _files_from_inventory(inv)) def _update_cache_from_list(branch, cache, to_update): """Update and return the cache for given files. cache -- Previously cached values to be validated. to_update -- Sequence of (file_id, path) pairs to check. """ from sets import Set hardcheck = dirty = 0 # files that have been recently touched and can't be # committed to a persistent cache yet. dangerfiles = Set() now = int(time.time()) for file_id, path in to_update: fap = branch.abspath(path) fp = fingerprint(fap, path) cacheentry = cache.get(file_id) if fp == None: # not here if cacheentry: del cache[file_id] dirty += 1 continue if (fp[FP_MTIME] >= now) or (fp[FP_CTIME] >= now): dangerfiles.add(file_id) if cacheentry and (cacheentry[3:] == fp): continue # all stat fields unchanged hardcheck += 1 dig = sha.new(file(fap, 'rb').read()).hexdigest() if cacheentry == None or dig != cacheentry[1]: # if there was no previous entry for this file, or if the # SHA has changed, then update the cache cacheentry = (file_id, dig, path) + fp cache[file_id] = cacheentry dirty += 1 mutter('statcache: read %d files, %d changed, %d dangerous, ' '%d in cache' % (hardcheck, dirty, len(dangerfiles), len(cache))) if dirty: mutter('updating on-disk statcache') _write_cache(branch, cache.itervalues(), dangerfiles) return cache commit refs/heads/master mark :437 committer Martin Pool 1115707714 +1000 data 54 - new command 'bzr modified' to exercise the statcache from :436 M 644 inline bzrlib/commands.py data 32362 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_args = ['file*'] takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False, file_list=None): b = Branch('.', lock_mode='r') b.show_status(show_all=all, file_list=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') sc = statcache.update_cache(b) basis = b.basis_tree() basis_inv = basis.inventory for path, ie in basis_inv.iter_entries(): if ie.kind != 'file': continue cacheentry = sc.get(ie.file_id) if not cacheentry: # deleted continue if cacheentry[statcache.SC_SHA1] != ie.text_sha1: print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from branch import find_branch b = find_branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') # FIXME: probably doesn't handle non-ascii patterns if os.path.exists(ifn): f = b.controlfile(ifn, 'rt') igns = f.read() f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' f = AtomicFile(ifn, 'wt') f.write(igns) f.commit() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False): ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/statcache.py data 6300 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import stat, os, sha, time from binascii import b2a_qp, a2b_qp from trace import mutter from errors import BzrError """File stat cache to speed up tree comparisons. This module basically gives a quick way to find the SHA-1 and related information of a file in the working directory, without actually reading and hashing the whole file. This is done by maintaining a cache indexed by a file fingerprint of (path, size, mtime, ctime, ino, dev) pointing to the SHA-1. If the fingerprint has changed, we assume the file content has not changed either and the SHA-1 is therefore the same. If any of the fingerprint fields have changed then the file content *may* have changed, or it may not have. We need to reread the file contents to make sure, but this is not visible to the user or higher-level code (except as a delay of course). The mtime and ctime are stored with nanosecond fields, but not all filesystems give this level of precision. There is therefore a possible race: the file might be modified twice within a second without changing the size or mtime, and a SHA-1 cached from the first version would be wrong. We handle this by not recording a cached hash for any files which were modified in the current second and that therefore have the chance to change again before the second is up. The only known hole in this design is if the system clock jumps backwards crossing invocations of bzr. Please don't do that; use ntp to gradually adjust your clock or don't use bzr over the step. At the moment this is stored in a simple textfile; it might be nice to use a tdb instead. The cache is represented as a map from file_id to a tuple of (file_id, sha1, path, size, mtime, ctime, ino, dev). """ FP_SIZE = 0 FP_MTIME = 1 FP_CTIME = 2 FP_INO = 3 FP_DEV = 4 SC_FILE_ID = 0 SC_SHA1 = 1 def fingerprint(path, abspath): try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) def _write_cache(branch, entry_iter, dangerfiles): from atomicfile import AtomicFile outf = AtomicFile(branch.controlfilename('stat-cache'), 'wb', 'utf-8') try: for entry in entry_iter: if entry[0] in dangerfiles: continue outf.write(entry[0] + ' ' + entry[1] + ' ') outf.write(b2a_qp(entry[2], True)) outf.write(' %d %d %d %d %d\n' % entry[3:]) outf.commit() finally: if not outf.closed: outf.abort() def load_cache(branch): cache = {} try: cachefile = branch.controlfile('stat-cache', 'r') except IOError: return cache for l in cachefile: f = l.split(' ') file_id = f[0] if file_id in cache: raise BzrError("duplicated file_id in cache: {%s}" % file_id) cache[file_id] = (f[0], f[1], a2b_qp(f[2])) + tuple([long(x) for x in f[3:]]) return cache def _files_from_inventory(inv): for path, ie in inv.iter_entries(): if ie.kind != 'file': continue yield ie.file_id, path def update_cache(branch, flush=False): """Update and return the cache for the branch. The returned cache may contain entries that have not been written to disk for files recently touched. flush -- discard any previous cache and recalculate from scratch. """ # TODO: It's supposed to be faster to stat the files in order by inum. # We don't directly know the inum of the files of course but we do # know where they were last sighted, so we can sort by that. if flush: cache = {} else: cache = load_cache(branch) inv = branch.read_working_inventory() return _update_cache_from_list(branch, cache, _files_from_inventory(inv)) def _update_cache_from_list(branch, cache, to_update): """Update and return the cache for given files. cache -- Previously cached values to be validated. to_update -- Sequence of (file_id, path) pairs to check. """ from sets import Set hardcheck = dirty = 0 # files that have been recently touched and can't be # committed to a persistent cache yet. dangerfiles = Set() now = int(time.time()) for file_id, path in to_update: fap = branch.abspath(path) fp = fingerprint(fap, path) cacheentry = cache.get(file_id) if fp == None: # not here if cacheentry: del cache[file_id] dirty += 1 continue if (fp[FP_MTIME] >= now) or (fp[FP_CTIME] >= now): dangerfiles.add(file_id) if cacheentry and (cacheentry[3:] == fp): continue # all stat fields unchanged hardcheck += 1 dig = sha.new(file(fap, 'rb').read()).hexdigest() if cacheentry == None or dig != cacheentry[1]: # if there was no previous entry for this file, or if the # SHA has changed, then update the cache cacheentry = (file_id, dig, path) + fp cache[file_id] = cacheentry dirty += 1 mutter('statcache: read %d files, %d changed, %d dangerous, ' '%d in cache' % (hardcheck, dirty, len(dangerfiles), len(cache))) if dirty: mutter('updating on-disk statcache') _write_cache(branch, cache.itervalues(), dangerfiles) return cache commit refs/heads/master mark :438 committer Martin Pool 1115708052 +1000 data 194 - Avoid calling Inventory.iter_entries() when finding modified files. Just calculate path for files known to be changed. - update_cache optionally takes inventory to avoid reading it twice. from :437 M 644 inline bzrlib/commands.py data 32664 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_args = ['file*'] takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False, file_list=None): b = Branch('.', lock_mode='r') b.show_status(show_all=all, file_list=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from branch import find_branch b = find_branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') # FIXME: probably doesn't handle non-ascii patterns if os.path.exists(ifn): f = b.controlfile(ifn, 'rt') igns = f.read() f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' f = AtomicFile(ifn, 'wt') f.write(igns) f.commit() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False): ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/statcache.py data 6369 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import stat, os, sha, time from binascii import b2a_qp, a2b_qp from trace import mutter from errors import BzrError """File stat cache to speed up tree comparisons. This module basically gives a quick way to find the SHA-1 and related information of a file in the working directory, without actually reading and hashing the whole file. This is done by maintaining a cache indexed by a file fingerprint of (path, size, mtime, ctime, ino, dev) pointing to the SHA-1. If the fingerprint has changed, we assume the file content has not changed either and the SHA-1 is therefore the same. If any of the fingerprint fields have changed then the file content *may* have changed, or it may not have. We need to reread the file contents to make sure, but this is not visible to the user or higher-level code (except as a delay of course). The mtime and ctime are stored with nanosecond fields, but not all filesystems give this level of precision. There is therefore a possible race: the file might be modified twice within a second without changing the size or mtime, and a SHA-1 cached from the first version would be wrong. We handle this by not recording a cached hash for any files which were modified in the current second and that therefore have the chance to change again before the second is up. The only known hole in this design is if the system clock jumps backwards crossing invocations of bzr. Please don't do that; use ntp to gradually adjust your clock or don't use bzr over the step. At the moment this is stored in a simple textfile; it might be nice to use a tdb instead. The cache is represented as a map from file_id to a tuple of (file_id, sha1, path, size, mtime, ctime, ino, dev). """ FP_SIZE = 0 FP_MTIME = 1 FP_CTIME = 2 FP_INO = 3 FP_DEV = 4 SC_FILE_ID = 0 SC_SHA1 = 1 def fingerprint(path, abspath): try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) def _write_cache(branch, entry_iter, dangerfiles): from atomicfile import AtomicFile outf = AtomicFile(branch.controlfilename('stat-cache'), 'wb', 'utf-8') try: for entry in entry_iter: if entry[0] in dangerfiles: continue outf.write(entry[0] + ' ' + entry[1] + ' ') outf.write(b2a_qp(entry[2], True)) outf.write(' %d %d %d %d %d\n' % entry[3:]) outf.commit() finally: if not outf.closed: outf.abort() def load_cache(branch): cache = {} try: cachefile = branch.controlfile('stat-cache', 'r') except IOError: return cache for l in cachefile: f = l.split(' ') file_id = f[0] if file_id in cache: raise BzrError("duplicated file_id in cache: {%s}" % file_id) cache[file_id] = (f[0], f[1], a2b_qp(f[2])) + tuple([long(x) for x in f[3:]]) return cache def _files_from_inventory(inv): for path, ie in inv.iter_entries(): if ie.kind != 'file': continue yield ie.file_id, path def update_cache(branch, inv=None, flush=False): """Update and return the cache for the branch. The returned cache may contain entries that have not been written to disk for files recently touched. flush -- discard any previous cache and recalculate from scratch. """ # TODO: It's supposed to be faster to stat the files in order by inum. # We don't directly know the inum of the files of course but we do # know where they were last sighted, so we can sort by that. assert isinstance(flush, bool) if flush: cache = {} else: cache = load_cache(branch) if inv == None: inv = branch.read_working_inventory() return _update_cache_from_list(branch, cache, _files_from_inventory(inv)) def _update_cache_from_list(branch, cache, to_update): """Update and return the cache for given files. cache -- Previously cached values to be validated. to_update -- Sequence of (file_id, path) pairs to check. """ from sets import Set hardcheck = dirty = 0 # files that have been recently touched and can't be # committed to a persistent cache yet. dangerfiles = Set() now = int(time.time()) for file_id, path in to_update: fap = branch.abspath(path) fp = fingerprint(fap, path) cacheentry = cache.get(file_id) if fp == None: # not here if cacheentry: del cache[file_id] dirty += 1 continue if (fp[FP_MTIME] >= now) or (fp[FP_CTIME] >= now): dangerfiles.add(file_id) if cacheentry and (cacheentry[3:] == fp): continue # all stat fields unchanged hardcheck += 1 dig = sha.new(file(fap, 'rb').read()).hexdigest() if cacheentry == None or dig != cacheentry[1]: # if there was no previous entry for this file, or if the # SHA has changed, then update the cache cacheentry = (file_id, dig, path) + fp cache[file_id] = cacheentry dirty += 1 mutter('statcache: read %d files, %d changed, %d dangerous, ' '%d in cache' % (hardcheck, dirty, len(dangerfiles), len(cache))) if dirty: mutter('updating on-disk statcache') _write_cache(branch, cache.itervalues(), dangerfiles) return cache commit refs/heads/master mark :439 committer Martin Pool 1115708765 +1000 data 25 - new command 'bzr added' from :438 M 644 inline bzrlib/commands.py data 33134 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_args = ['file*'] takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False, file_list=None): b = Branch('.', lock_mode='r') b.show_status(show_all=all, file_list=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from branch import find_branch b = find_branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') # FIXME: probably doesn't handle non-ascii patterns if os.path.exists(ifn): f = b.controlfile(ifn, 'rt') igns = f.read() f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' f = AtomicFile(ifn, 'wt') f.write(igns) f.commit() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False): ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: log_error('usage: bzr COMMAND') log_error(' try "bzr help"') return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :440 committer Martin Pool 1115709263 +1000 data 3 doc from :439 M 644 inline NEWS data 6317 bzr-0.0.5 NOT RELEASED YET ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New ``bzr ignore PATTERN`` command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. * ``bzr log`` and ``bzr root`` can be given an http URL instead of a filename. * Commands can now be defined by external programs or scripts in a directory on $BZRPATH. * New "stat cache" avoids reading the contents of files if they haven't changed since the previous time. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. * New developer commands ``added``, ``modified``. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. commit refs/heads/master mark :441 committer Martin Pool 1115709348 +1000 data 71 - Fix from Lalo for unidiff output of newly added and changed files. from :440 M 644 inline NEWS data 6456 bzr-0.0.5 NOT RELEASED YET ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New ``bzr ignore PATTERN`` command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. * ``bzr log`` and ``bzr root`` can be given an http URL instead of a filename. * Commands can now be defined by external programs or scripts in a directory on $BZRPATH. * New "stat cache" avoids reading the contents of files if they haven't changed since the previous time. BUG FIXES: * Fixed diff format so that added and removed files will be handled properly by patch. Fix from Lalo Martins. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. * New developer commands ``added``, ``modified``. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/diff.py data 11268 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set from trace import mutter from errors import BzrError def diff_trees(old_tree, new_tree): """Compute diff between two trees. They may be in different branches and may be working or historical trees. Yields a sequence of (state, id, old_name, new_name, kind). Each filename and each id is listed only once. """ ## TODO: Compare files before diffing; only mention those that have changed ## TODO: Set nice names in the headers, maybe include diffstat ## TODO: Perhaps make this a generator rather than using ## a callback object? ## TODO: Allow specifying a list of files to compare, rather than ## doing the whole tree? (Not urgent.) ## TODO: Allow diffing any two inventories, not just the ## current one against one. We mgiht need to specify two ## stores to look for the files if diffing two branches. That ## might imply this shouldn't be primarily a Branch method. ## XXX: This doesn't report on unknown files; that can be done ## from a separate method. old_it = old_tree.list_files() new_it = new_tree.list_files() def next(it): try: return it.next() except StopIteration: return None old_item = next(old_it) new_item = next(new_it) # We step through the two sorted iterators in parallel, trying to # keep them lined up. while (old_item != None) or (new_item != None): # OK, we still have some remaining on both, but they may be # out of step. if old_item != None: old_name, old_class, old_kind, old_id = old_item else: old_name = None if new_item != None: new_name, new_class, new_kind, new_id = new_item else: new_name = None mutter(" diff pairwise %r" % (old_item,)) mutter(" %r" % (new_item,)) if old_item: # can't handle the old tree being a WorkingTree assert old_class == 'V' if new_item and (new_class != 'V'): yield new_class, None, None, new_name, new_kind new_item = next(new_it) elif (not new_item) or (old_item and (old_name < new_name)): mutter(" extra entry in old-tree sequence") if new_tree.has_id(old_id): # will be mentioned as renamed under new name pass else: yield 'D', old_id, old_name, None, old_kind old_item = next(old_it) elif (not old_item) or (new_item and (new_name < old_name)): mutter(" extra entry in new-tree sequence") if old_tree.has_id(new_id): yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind else: yield 'A', new_id, None, new_name, new_kind new_item = next(new_it) elif old_id != new_id: assert old_name == new_name # both trees have a file of this name, but it is not the # same file. in other words, the old filename has been # overwritten by either a newly-added or a renamed file. # (should we return something about the overwritten file?) if old_tree.has_id(new_id): # renaming, overlying a deleted file yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind else: yield 'A', new_id, None, new_name, new_kind new_item = next(new_it) old_item = next(old_it) else: assert old_id == new_id assert old_id != None assert old_name == new_name assert old_kind == new_kind if old_kind == 'directory': yield '.', new_id, old_name, new_name, new_kind elif old_tree.get_file_size(old_id) != new_tree.get_file_size(old_id): mutter(" file size has changed, must be different") yield 'M', new_id, old_name, new_name, new_kind elif old_tree.get_file_sha1(old_id) == new_tree.get_file_sha1(old_id): mutter(" SHA1 indicates they're identical") ## assert compare_files(old_tree.get_file(i), new_tree.get_file(i)) yield '.', new_id, old_name, new_name, new_kind else: mutter(" quick compare shows different") yield 'M', new_id, old_name, new_name, new_kind new_item = next(new_it) old_item = next(old_it) def show_diff(b, revision, file_list): import difflib, sys, types if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. if file_list: file_list = [b.relpath(f) for f in file_list] # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in diff_trees(old_tree, new_tree): if file_list and (new_name not in file_list): continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') sys.stdout.writelines(ud) if nonl: print "\\ No newline at end of file" sys.stdout.write('\n') if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: raise BzrError("can't represent state %s {%s}" % (file_state, fid)) class TreeDelta: """Describes changes from one tree to another. Contains four lists: added (path, id) removed (path, id) renamed (oldpath, newpath, id) modified (path, id) A path may occur in more than one list if it was e.g. deleted under an old id and renamed into place in a new id. Files are listed in either modified or renamed, not both. In other words, renamed files may also be modified. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] def compare_inventories(old_inv, new_inv): """Return a TreeDelta object describing changes between inventories. This only describes changes in the shape of the tree, not the actual texts. This is an alternative to diff_trees() and should probably eventually replace it. """ old_ids = old_inv.id_set() new_ids = new_inv.id_set() delta = TreeDelta() delta.removed = [(old_inv.id2path(fid), fid) for fid in (old_ids - new_ids)] delta.removed.sort() delta.added = [(new_inv.id2path(fid), fid) for fid in (new_ids - old_ids)] delta.added.sort() for fid in old_ids & new_ids: old_ie = old_inv[fid] new_ie = new_inv[fid] old_path = old_inv.id2path(fid) new_path = new_inv.id2path(fid) if old_path != new_path: delta.renamed.append((old_path, new_path, fid)) elif old_ie.text_sha1 != new_ie.text_sha1: delta.modified.append((new_path, fid)) delta.modified.sort() delta.renamed.sort() return delta commit refs/heads/master mark :442 committer Martin Pool 1115709427 +1000 data 4 todo from :441 M 644 inline TODO data 9589 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * testbzr should by default test the bzr binary in the same directory as the testbzr script, or take a path to it as a first parameter. Should show the version from bzr and the path name. * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. Medium things ------------- * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still expose it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :443 committer Martin Pool 1115712958 +1000 data 192 - Patch from Fredrik Lundh to check Python version and try to find a better one if it's too old. Patched to try to prevent infinite loops in wierd configurations, and to log to stderr. from :442 M 644 inline NEWS data 6588 bzr-0.0.5 NOT RELEASED YET ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New ``bzr ignore PATTERN`` command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. * ``bzr log`` and ``bzr root`` can be given an http URL instead of a filename. * Commands can now be defined by external programs or scripts in a directory on $BZRPATH. * New "stat cache" avoids reading the contents of files if they haven't changed since the previous time. * If the Python interpreter is too old, try to find a better one or give an error. Based on a patch from Fredrik Lundh. BUG FIXES: * Fixed diff format so that added and removed files will be handled properly by patch. Fix from Lalo Martins. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. * New developer commands ``added``, ``modified``. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzr data 1550 #! /usr/bin/env python # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, sys try: version_info = sys.version_info except AttributeError: version_info = 1, 5 # 1.5 or older REINVOKE = "__BZR_REINVOKE" NEED_VERS = (2, 3) if version_info < NEED_VERS: if not os.environ.has_key(REINVOKE): # mutating os.environ doesn't work in old Pythons os.putenv(REINVOKE, "1") for python in 'python2.4', 'python2.3': try: os.execvp(python, [python] + sys.argv) except OSError: pass print >>sys.stderr, "bzr: error: cannot find a suitable python interpreter" print >>sys.stderr, " (need %d.%d or later)" % NEED_VERS sys.exit(1) os.unsetenv(REINVOKE) import bzrlib, bzrlib.commands if __name__ == '__main__': sys.exit(bzrlib.commands.main(sys.argv)) commit refs/heads/master mark :444 committer Martin Pool 1115713338 +1000 data 71 - cope on platforms with no urandom feature including win32 python2.3 from :443 M 644 inline NEWS data 6710 bzr-0.0.5 NOT RELEASED YET ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New ``bzr ignore PATTERN`` command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. * ``bzr log`` and ``bzr root`` can be given an http URL instead of a filename. * Commands can now be defined by external programs or scripts in a directory on $BZRPATH. * New "stat cache" avoids reading the contents of files if they haven't changed since the previous time. * If the Python interpreter is too old, try to find a better one or give an error. Based on a patch from Fredrik Lundh. BUG FIXES: * Fixed diff format so that added and removed files will be handled properly by patch. Fix from Lalo Martins. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. * New developer commands ``added``, ``modified``. PORTABILITY: * Cope on Windows on python2.3 by using the weaker random seed. 2.4 is now only recommended. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/osutils.py data 8776 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, types, re, time, errno, sys from stat import S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE from errors import bailout, BzrError from trace import mutter import bzrlib def make_readonly(filename): """Make a filename read-only.""" # TODO: probably needs to be fixed for windows mod = os.stat(filename).st_mode mod = mod & 0777555 os.chmod(filename, mod) def make_writable(filename): mod = os.stat(filename).st_mode mod = mod | 0200 os.chmod(filename, mod) _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])') def quotefn(f): """Return shell-quoted filename""" ## We could be a bit more terse by using double-quotes etc f = _QUOTE_RE.sub(r'\\\1', f) if f[0] == '~': f[0:1] = r'\~' return f def file_kind(f): mode = os.lstat(f)[ST_MODE] if S_ISREG(mode): return 'file' elif S_ISDIR(mode): return 'directory' elif S_ISLNK(mode): return 'symlink' else: raise BzrError("can't handle file kind with mode %o of %r" % (mode, f)) def isdir(f): """True if f is an accessible directory.""" try: return S_ISDIR(os.lstat(f)[ST_MODE]) except OSError: return False def isfile(f): """True if f is a regular file.""" try: return S_ISREG(os.lstat(f)[ST_MODE]) except OSError: return False def pumpfile(fromfile, tofile): """Copy contents of one file to another.""" tofile.write(fromfile.read()) def uuid(): """Return a new UUID""" try: return file('/proc/sys/kernel/random/uuid').readline().rstrip('\n') except IOError: return chomp(os.popen('uuidgen').readline()) def sha_file(f): import sha if hasattr(f, 'tell'): assert f.tell() == 0 s = sha.new() BUFSIZE = 128<<10 while True: b = f.read(BUFSIZE) if not b: break s.update(b) return s.hexdigest() def sha_string(f): import sha s = sha.new() s.update(f) return s.hexdigest() def fingerprint_file(f): import sha s = sha.new() b = f.read() s.update(b) size = len(b) return {'size': size, 'sha1': s.hexdigest()} def config_dir(): """Return per-user configuration directory. By default this is ~/.bzr.conf/ TODO: Global option --config-dir to override this. """ return os.path.expanduser("~/.bzr.conf") def _auto_user_id(): """Calculate automatic user identification. Returns (realname, email). Only used when none is set in the environment or the id file. This previously used the FQDN as the default domain, but that can be very slow on machines where DNS is broken. So now we simply use the hostname. """ import socket # XXX: Any good way to get real user name on win32? try: import pwd uid = os.getuid() w = pwd.getpwuid(uid) gecos = w.pw_gecos.decode(bzrlib.user_encoding) username = w.pw_name.decode(bzrlib.user_encoding) comma = gecos.find(',') if comma == -1: realname = gecos else: realname = gecos[:comma] if not realname: realname = username except ImportError: import getpass realname = username = getpass.getuser().decode(bzrlib.user_encoding) return realname, (username + '@' + socket.gethostname()) def _get_user_id(): """Return the full user id from a file or environment variable. TODO: Allow taking this from a file in the branch directory too for per-branch ids.""" v = os.environ.get('BZREMAIL') if v: return v.decode(bzrlib.user_encoding) try: return (open(os.path.join(config_dir(), "email")) .read() .decode(bzrlib.user_encoding) .rstrip("\r\n")) except IOError, e: if e.errno != errno.ENOENT: raise e v = os.environ.get('EMAIL') if v: return v.decode(bzrlib.user_encoding) else: return None def username(): """Return email-style username. Something similar to 'Martin Pool ' TODO: Check it's reasonably well-formed. """ v = _get_user_id() if v: return v name, email = _auto_user_id() if name: return '%s <%s>' % (name, email) else: return email _EMAIL_RE = re.compile(r'[\w+.-]+@[\w+.-]+') def user_email(): """Return just the email component of a username.""" e = _get_user_id() if e: m = _EMAIL_RE.search(e) if not m: bailout("%r doesn't seem to contain a reasonable email address" % e) return m.group(0) return _auto_user_id()[1] def compare_files(a, b): """Returns true if equal in contents""" BUFSIZE = 4096 while True: ai = a.read(BUFSIZE) bi = b.read(BUFSIZE) if ai != bi: return False if ai == '': return True def local_time_offset(t=None): """Return offset of local zone from GMT, either at present or at time t.""" # python2.3 localtime() can't take None if t == None: t = time.time() if time.localtime(t).tm_isdst and time.daylight: return -time.altzone else: return -time.timezone def format_date(t, offset=0, timezone='original'): ## TODO: Perhaps a global option to use either universal or local time? ## Or perhaps just let people set $TZ? assert isinstance(t, float) if timezone == 'utc': tt = time.gmtime(t) offset = 0 elif timezone == 'original': if offset == None: offset = 0 tt = time.gmtime(t + offset) elif timezone == 'local': tt = time.localtime(t) offset = local_time_offset(t) else: bailout("unsupported timezone format %r", ['options are "utc", "original", "local"']) return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt) + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)) def compact_date(when): return time.strftime('%Y%m%d%H%M%S', time.gmtime(when)) def filesize(f): """Return size of given open file.""" return os.fstat(f.fileno())[ST_SIZE] if hasattr(os, 'urandom'): # python 2.4 and later rand_bytes = os.urandom elif sys.platform == 'linux2': rand_bytes = file('/dev/urandom', 'rb').read else: # not well seeded, but better than nothing def rand_bytes(n): import random s = '' while n: s += chr(random.randint(0, 255)) n -= 1 return s ## TODO: We could later have path objects that remember their list ## decomposition (might be too tricksy though.) def splitpath(p): """Turn string into list of parts. >>> splitpath('a') ['a'] >>> splitpath('a/b') ['a', 'b'] >>> splitpath('a/./b') ['a', 'b'] >>> splitpath('a/.b') ['a', '.b'] >>> splitpath('a/../b') Traceback (most recent call last): ... BzrError: ("sorry, '..' not allowed in path", []) """ assert isinstance(p, types.StringTypes) # split on either delimiter because people might use either on # Windows ps = re.split(r'[\\/]', p) rps = [] for f in ps: if f == '..': bailout("sorry, %r not allowed in path" % f) elif (f == '.') or (f == ''): pass else: rps.append(f) return rps def joinpath(p): assert isinstance(p, list) for f in p: if (f == '..') or (f == None) or (f == ''): bailout("sorry, %r not allowed in path" % f) return os.path.join(*p) def appendpath(p1, p2): if p1 == '': return p2 else: return os.path.join(p1, p2) def extern_command(cmd, ignore_errors = False): mutter('external command: %s' % `cmd`) if os.system(cmd): if not ignore_errors: bailout('command failed') commit refs/heads/master mark :445 committer Martin Pool 1115713389 +1000 data 32 - now only python2.3 is required from :444 M 644 inline README data 1271 ********************************* Release notes for Bazaar-NG 0.0.5 ********************************* mbp@sourcefrog.net, May 2005, Canberra Caveats ------- * There is little locking or transaction control here; if you interrupt it the tree may be arbitrarily broken. This will be fixed. * Don't use this for critical data; at the very least keep separate regular snapshots of your tree. Dependencies ------------ This is mostly developed on Linux (Ubuntu); it should work on Unix, Windows, or OS X with relatively little trouble. The only dependency is Python, at least 2.3 and preferably 2.4. You may optionally install cElementTree to speed up some operations. Installation ------------ The best way to install bzr is to symlink the ``bzr`` command onto a directory on your path. For example:: ln -s ~/work/bzr/bzr ~/bin/bzr If you use a symlink for this, Python will be able to automatically find the bzr libraries. Otherwise you must ensure they are listed on your $PYTHONPATH. After installing, please run the test suite to identify any problems on your platform:: ./testbzr If you use the setup.py script then bzr will be installed into the specified path. In this case you must install ElementTree or cElementTree separately. commit refs/heads/master mark :446 committer Martin Pool 1115713916 +1000 data 3 doc from :445 M 644 inline README data 1135 ********************************* Release notes for Bazaar-NG 0.0.5 ********************************* mbp@sourcefrog.net, May 2005, Canberra Caveats ------- * Don't use this for critical data; at the very least keep separate regular snapshots of your tree. Dependencies ------------ This is mostly developed on Linux (Ubuntu); it should work on Unix, Windows, or OS X with relatively little trouble. The only dependency is Python, at least 2.3 and preferably 2.4. You may optionally install cElementTree to speed up some operations. Installation ------------ The best way to install bzr is to symlink the ``bzr`` command onto a directory on your path. For example:: ln -s ~/work/bzr/bzr ~/bin/bzr If you use a symlink for this, Python will be able to automatically find the bzr libraries. Otherwise you must ensure they are listed on your $PYTHONPATH. After installing, please run the test suite to identify any problems on your platform:: ./testbzr If you use the setup.py script then bzr will be installed into the specified path. In this case you must install ElementTree or cElementTree separately. commit refs/heads/master mark :447 committer Martin Pool 1115715388 +1000 data 80 - atomicfile temporaries should end in .tmp to make it easier to clean them up from :446 M 644 inline bzrlib/atomicfile.py data 2010 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA class AtomicFile: """A file that does an atomic-rename to move into place. This also causes hardlinks to break when it's written out. Open this as for a regular file, then use commit() to move into place or abort() to cancel. An encoding can be specified; otherwise the default is ascii. """ def __init__(self, filename, mode='wb', encoding=None): if mode != 'wb' and mode != 'wt': raise ValueError("invalid AtomicFile mode %r" % mode) import os, socket self.tmpfilename = '%s.%d.%s.tmp' % (filename, os.getpid(), socket.gethostname()) self.realfilename = filename self.f = open(self.tmpfilename, mode) if encoding: import codecs self.f = codecs.EncodedFile(self.f, encoding) self.write = self.f.write self.closed = property(self.f.closed) def commit(self): import sys, os self.f.close() if sys.platform == 'win32': os.remove(self.realfilename) os.rename(self.tmpfilename, self.realfilename) def abort(self): import os self.f.close() os.remove(self.tmpfilename) commit refs/heads/master mark :448 committer Martin Pool 1115773009 +1000 data 55 - bzr with no command now shows help, not just an error from :447 M 644 inline NEWS data 6838 bzr-0.0.5 NOT RELEASED YET CHANGES: * ``bzr`` with no command now shows help rather than giving an error. Suggested by Michael Ellerman. ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New ``bzr ignore PATTERN`` command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. * ``bzr log`` and ``bzr root`` can be given an http URL instead of a filename. * Commands can now be defined by external programs or scripts in a directory on $BZRPATH. * New "stat cache" avoids reading the contents of files if they haven't changed since the previous time. * If the Python interpreter is too old, try to find a better one or give an error. Based on a patch from Fredrik Lundh. BUG FIXES: * Fixed diff format so that added and removed files will be handled properly by patch. Fix from Lalo Martins. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. * New developer commands ``added``, ``modified``. PORTABILITY: * Cope on Windows on python2.3 by using the weaker random seed. 2.4 is now only recommended. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 33107 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, WorkingTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_args = ['file*'] takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False, file_list=None): b = Branch('.', lock_mode='r') b.show_status(show_all=all, file_list=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from branch import find_branch b = find_branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') # FIXME: probably doesn't handle non-ascii patterns if os.path.exists(ifn): f = b.controlfile(ifn, 'rt') igns = f.read() f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' f = AtomicFile(ifn, 'wt') f.write(igns) f.commit() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False): ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :449 committer Martin Pool 1115773402 +1000 data 31 - Notes on library dependencies from :448 M 644 inline README data 1428 ********************************* Release notes for Bazaar-NG 0.0.5 ********************************* mbp@sourcefrog.net, May 2005, Canberra Caveats ------- * Don't use this for critical data; at the very least keep separate regular snapshots of your tree. Dependencies ------------ This is mostly developed on Linux (Ubuntu); it should work on Unix, Windows, or OS X with relatively little trouble. The only dependency is Python, at least 2.3 and preferably 2.4. You may optionally install cElementTree to speed up some operations. This is strongly recommended if you are dealing with large trees. This package includes copies of two Python libraries, elementtree and urlgrabber. If you are building .deb or .rpm packages for bzr, please don't include these libraries but instead package them separately and depend on that. Installation ------------ The best way to install bzr is to symlink the ``bzr`` command onto a directory on your path. For example:: ln -s ~/work/bzr/bzr ~/bin/bzr If you use a symlink for this, Python will be able to automatically find the bzr libraries. Otherwise you must ensure they are listed on your $PYTHONPATH. After installing, please run the test suite to identify any problems on your platform:: ./testbzr If you use the setup.py script then bzr will be installed into the specified path. In this case you must install ElementTree and urlgrabber separately. commit refs/heads/master mark :450 committer Martin Pool 1115773567 +1000 data 27 - Use urlgrabber by default from :449 M 644 inline bzrlib/remotebranch.py data 6315 #! /usr/bin/env python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Proxy object for access to remote branches. At the moment remote branches are only for HTTP and only for read access. """ import gzip from sets import Set from cStringIO import StringIO import urllib2 from errors import BzrError, BzrCheckError from branch import Branch, BZR_BRANCH_FORMAT from trace import mutter # velocitynet.com.au transparently proxies connections and thereby # breaks keep-alive -- sucks! ENABLE_URLGRABBER = True if ENABLE_URLGRABBER: import urlgrabber import urlgrabber.keepalive urlgrabber.keepalive.DEBUG = 0 def get_url(path, compressed=False): try: url = path if compressed: url += '.gz' mutter("grab url %s" % url) url_f = urlgrabber.urlopen(url, keepalive=1, close_connection=0) if not compressed: return url_f else: return gzip.GzipFile(fileobj=StringIO(url_f.read())) except urllib2.URLError, e: raise BzrError("remote fetch failed: %r: %s" % (url, e)) else: def get_url(url, compressed=False): import urllib2 if compressed: url += '.gz' mutter("get_url %s" % url) url_f = urllib2.urlopen(url) if compressed: return gzip.GzipFile(fileobj=StringIO(url_f.read())) else: return url_f def _find_remote_root(url): """Return the prefix URL that corresponds to the branch root.""" orig_url = url while True: try: ff = get_url(url + '/.bzr/branch-format') fmt = ff.read() ff.close() fmt = fmt.rstrip('\r\n') if fmt != BZR_BRANCH_FORMAT.rstrip('\r\n'): raise BzrError("sorry, branch format %r not supported at url %s" % (fmt, url)) return url except urllib2.URLError: pass try: idx = url.rindex('/') except ValueError: raise BzrError('no branch root found for URL %s' % orig_url) url = url[:idx] class RemoteBranch(Branch): def __init__(self, baseurl, find_root=True, lock_mode='r'): """Create new proxy for a remote branch.""" if lock_mode not in ('', 'r'): raise BzrError('lock mode %r is not supported for remote branches' % lock_mode) if find_root: self.baseurl = _find_remote_root(baseurl) else: self.baseurl = baseurl self._check_format() def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.baseurl) __repr__ = __str__ def controlfile(self, filename, mode): if mode not in ('rb', 'rt', 'r'): raise BzrError("file mode %r not supported for remote branches" % mode) return get_url(self.baseurl + '/.bzr/' + filename, False) def _need_readlock(self): # remote branch always safe for read pass def _need_writelock(self): raise BzrError("cannot get write lock on HTTP remote branch") def relpath(self, path): if not path.startswith(self.baseurl): raise BzrError('path %r is not under base URL %r' % (path, self.baseurl)) pl = len(self.baseurl) return path[pl:].lstrip('/') def get_revision(self, revision_id): from revision import Revision revf = get_url(self.baseurl + '/.bzr/revision-store/' + revision_id, True) r = Revision.read_xml(revf) if r.revision_id != revision_id: raise BzrCheckError('revision stored as {%s} actually contains {%s}' % (revision_id, r.revision_id)) return r def simple_walk(): from revision import Revision from branch import Branch from inventory import Inventory got_invs = Set() got_texts = Set() print 'read history' history = get_url('/.bzr/revision-history').readlines() num_revs = len(history) for i, rev_id in enumerate(history): rev_id = rev_id.rstrip() print 'read revision %d/%d' % (i, num_revs) # python gzip needs a seekable file (!!) but the HTTP response # isn't, so we need to buffer it rev_f = get_url('/.bzr/revision-store/%s' % rev_id, compressed=True) rev = Revision.read_xml(rev_f) print rev.message inv_id = rev.inventory_id if inv_id not in got_invs: print 'get inventory %s' % inv_id inv_f = get_url('/.bzr/inventory-store/%s' % inv_id, compressed=True) inv = Inventory.read_xml(inv_f) print '%4d inventory entries' % len(inv) for path, ie in inv.iter_entries(): text_id = ie.text_id if text_id == None: continue if text_id in got_texts: continue print ' fetch %s text {%s}' % (path, text_id) text_f = get_url('/.bzr/text-store/%s' % text_id, compressed=True) got_texts.add(text_id) got_invs.add(inv_id) print '----' def try_me(): BASE_URL = 'http://bazaar-ng.org/bzr/bzr.dev/' b = RemoteBranch(BASE_URL) ## print '\n'.join(b.revision_history()) from log import show_log show_log(b) if __name__ == '__main__': try_me() commit refs/heads/master mark :451 committer Martin Pool 1115773781 +1000 data 4 todo from :450 M 644 inline TODO data 9896 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * testbzr should by default test the bzr binary in the same directory as the testbzr script, or take a path to it as a first parameter. Should show the version from bzr and the path name. * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. Medium things ------------- * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still expose it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :452 committer Martin Pool 1115774752 +1000 data 28 - show command usage in help from :451 M 644 inline bzrlib/help.py data 4243 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA global_help = \ """Bazaar-NG -- a free distributed version-control tool http://bazaar-ng.org/ **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** * Metadata format is not stable yet -- you may need to discard history in the future. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. To make a branch, use 'bzr init' in an existing directory, then 'bzr add' to make files versioned. 'bzr add .' will recursively add all non-ignored files. 'bzr status' describes files that are unknown, ignored, or modified. 'bzr diff' shows the text changes to the tree or named files. 'bzr commit -m ' commits all changes in that branch. 'bzr move' and 'bzr rename' allow you to rename files or directories. 'bzr remove' makes a file unversioned but keeps the working copy; to delete that too simply delete the file. 'bzr log' shows a history of changes, and 'bzr info' gives summary statistical information. 'bzr check' validates all files are stored safely. Files can be ignored by giving a path or a glob in .bzrignore at the top of the tree. Use 'bzr ignored' to see what files are ignored and why, and 'bzr unknowns' to see files that are neither versioned or ignored. For more help on any command, type 'bzr help COMMAND', or 'bzr help commands' for a list. """ def help(topic=None): if topic == None: print global_help elif topic == 'commands': help_commands() else: help_on_command(topic) def command_usage(cmdname, cmdclass): """Return single-line grammar for command. Only describes arguments, not options. """ s = cmdname + ' ' for aname in cmdclass.takes_args: aname = aname.upper() if aname[-1] in ['$', '+']: aname = aname[:-1] + '...' elif aname[-1] == '?': aname = '[' + aname[:-1] + ']' elif aname[-1] == '*': aname = '[' + aname[:-1] + '...]' s += aname + ' ' assert s[-1] == ' ' s = s[:-1] return s def help_on_command(cmdname): cmdname = str(cmdname) from inspect import getdoc import commands topic, cmdclass = commands.get_cmd_class(cmdname) doc = getdoc(cmdclass) if doc == None: raise NotImplementedError("sorry, no detailed help yet for %r" % cmdname) if '\n' in doc: short, rest = doc.split('\n', 1) else: short = doc rest = '' print 'usage:', command_usage(topic, cmdclass) if cmdclass.aliases: print 'aliases: ' + ', '.join(cmdclass.aliases) if rest: print rest help_on_option(cmdclass.takes_options) def help_on_option(options): import commands if not options: return print print 'options:' for on in options: l = ' --' + on for shortname, longname in commands.SHORT_OPTIONS.items(): if longname == on: l += ', -' + shortname break print l def help_commands(): """List all commands""" import inspect import commands accu = [] for cmdname, cmdclass in commands.get_all_cmds(): accu.append((cmdname, cmdclass)) accu.sort() for cmdname, cmdclass in accu: if cmdclass.hidden: continue print command_usage(cmdname, cmdclass) help = inspect.getdoc(cmdclass) if help: print " " + help.split('\n', 1)[0] commit refs/heads/master mark :453 committer Martin Pool 1115778146 +1000 data 201 - Split WorkingTree into its own file - pychecker fixes - statcache works in terms of just directories, not branches - use statcache from workingtree when getting file SHA-1 so that diffs are faster from :452 M 644 inline bzrlib/workingtree.py data 8179 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os import bzrlib.tree from errors import BzrCheckError from trace import mutter class WorkingTree(bzrlib.tree.Tree): """Working copy tree. The inventory is held in the `Branch` working-inventory, and the files are in a directory on disk. It is possible for a `WorkingTree` to have a filename which is not listed in the Inventory and vice versa. """ _statcache = None def __init__(self, basedir, inv): self._inventory = inv self.basedir = basedir self.path2id = inv.path2id def __repr__(self): return "<%s of %s>" % (self.__class__.__name__, self.basedir) def abspath(self, filename): return os.path.join(self.basedir, filename) def has_filename(self, filename): return os.path.exists(self.abspath(filename)) def get_file(self, file_id): return self.get_file_byname(self.id2path(file_id)) def get_file_byname(self, filename): return file(self.abspath(filename), 'rb') def _get_store_filename(self, file_id): ## XXX: badly named; this isn't in the store at all return self.abspath(self.id2path(file_id)) def has_id(self, file_id): # files that have been deleted are excluded if not self.inventory.has_id(file_id): return False import os return os.access(self.abspath(self.inventory.id2path(file_id)), os.F_OK) def get_file_size(self, file_id): import os, stat return os.stat(self._get_store_filename(file_id))[stat.ST_SIZE] def get_file_sha1(self, file_id): import statcache if not self._statcache: self._statcache = statcache.update_cache(self.basedir, self.inventory) return self._statcache[file_id][statcache.SC_SHA1] def file_class(self, filename): if self.path2id(filename): return 'V' elif self.is_ignored(filename): return 'I' else: return '?' def list_files(self): """Recursively list all files as (path, class, kind, id). Lists, but does not descend into unversioned directories. This does not include files that have been deleted in this tree. Skips the control directory. """ from osutils import appendpath, file_kind import os inv = self.inventory def descend(from_dir_relpath, from_dir_id, dp): ls = os.listdir(dp) ls.sort() for f in ls: ## TODO: If we find a subdirectory with its own .bzr ## directory, then that is a separate tree and we ## should exclude it. if bzrlib.BZRDIR == f: continue # path within tree fp = appendpath(from_dir_relpath, f) # absolute path fap = appendpath(dp, f) f_ie = inv.get_child(from_dir_id, f) if f_ie: c = 'V' elif self.is_ignored(fp): c = 'I' else: c = '?' fk = file_kind(fap) if f_ie: if f_ie.kind != fk: raise BzrCheckError("file %r entered as kind %r id %r, " "now of kind %r" % (fap, f_ie.kind, f_ie.file_id, fk)) yield fp, c, fk, (f_ie and f_ie.file_id) if fk != 'directory': continue if c != 'V': # don't descend unversioned directories continue for ff in descend(fp, f_ie.file_id, fap): yield ff for f in descend('', inv.root.file_id, self.basedir): yield f def unknowns(self): for subp in self.extras(): if not self.is_ignored(subp): yield subp def extras(self): """Yield all unknown files in this WorkingTree. If there are any unknown directories then only the directory is returned, not all its children. But if there are unknown files under a versioned subdirectory, they are returned. Currently returned depth-first, sorted by name within directories. """ ## TODO: Work from given directory downwards from osutils import isdir, appendpath for path, dir_entry in self.inventory.directories(): mutter("search for unknowns in %r" % path) dirabs = self.abspath(path) if not isdir(dirabs): # e.g. directory deleted continue fl = [] for subf in os.listdir(dirabs): if (subf != '.bzr' and (subf not in dir_entry.children)): fl.append(subf) fl.sort() for subf in fl: subp = appendpath(path, subf) yield subp def ignored_files(self): """Yield list of PATH, IGNORE_PATTERN""" for subp in self.extras(): pat = self.is_ignored(subp) if pat != None: yield subp, pat def get_ignore_list(self): """Return list of ignore patterns. Cached in the Tree object after the first call. """ if hasattr(self, '_ignorelist'): return self._ignorelist l = bzrlib.DEFAULT_IGNORE[:] if self.has_filename(bzrlib.IGNORE_FILENAME): f = self.get_file_byname(bzrlib.IGNORE_FILENAME) l.extend([line.rstrip("\n\r") for line in f.readlines()]) self._ignorelist = l return l def is_ignored(self, filename): r"""Check whether the filename matches an ignore pattern. Patterns containing '/' or '\' need to match the whole path; others match against only the last component. If the file is ignored, returns the pattern which caused it to be ignored, otherwise None. So this can simply be used as a boolean if desired.""" # TODO: Use '**' to match directories, and other extended # globbing stuff from cvs/rsync. # XXX: fnmatch is actually not quite what we want: it's only # approximately the same as real Unix fnmatch, and doesn't # treat dotfiles correctly and allows * to match /. # Eventually it should be replaced with something more # accurate. import fnmatch from osutils import splitpath for pat in self.get_ignore_list(): if '/' in pat or '\\' in pat: # as a special case, you can put ./ at the start of a # pattern; this is good to match in the top-level # only; if (pat[:2] == './') or (pat[:2] == '.\\'): newpat = pat[2:] else: newpat = pat if fnmatch.fnmatchcase(filename, newpat): return pat else: if fnmatch.fnmatchcase(splitpath(filename)[-1], pat): return pat else: return None M 644 inline TODO data 9952 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * testbzr should by default test the bzr binary in the same directory as the testbzr script, or take a path to it as a first parameter. Should show the version from bzr and the path name. * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. Medium things ------------- * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Should be a signature at the top of the cache file. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` M 644 inline bzrlib/branch.py data 36253 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f.startswith('http://') or f.startswith('https://'): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. base Base directory of the branch. """ _lockmode = None def __init__(self, base, init=False, find_root=True, lock_mode='w'): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.lock(lock_mode) self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def lock(self, mode='w'): """Lock the on-disk branch, excluding other processes.""" try: import fcntl, errno if mode == 'w': lm = fcntl.LOCK_EX om = os.O_WRONLY | os.O_CREAT elif mode == 'r': lm = fcntl.LOCK_SH om = os.O_RDONLY else: raise BzrError("invalid locking mode %r" % mode) try: lockfile = os.open(self.controlfilename('branch-lock'), om) except OSError, e: if e.errno == errno.ENOENT: # might not exist on branches from <0.0.4 self.controlfile('branch-lock', 'w').close() lockfile = os.open(self.controlfilename('branch-lock'), om) else: raise e fcntl.lockf(lockfile, lm) def unlock(): fcntl.lockf(lockfile, fcntl.LOCK_UN) os.close(lockfile) self._lockmode = None self.unlock = unlock self._lockmode = mode except ImportError: warning("please write a locking method for platform %r" % sys.platform) def unlock(): self._lockmode = None self.unlock = unlock self._lockmode = mode def _need_readlock(self): if self._lockmode not in ['r', 'w']: raise BzrError('need read lock on branch, only have %r' % self._lockmode) def _need_writelock(self): if self._lockmode not in ['w']: raise BzrError('need write lock on branch, only have %r' % self._lockmode) def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" self._need_readlock() before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self._need_writelock() ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ self._need_writelock() # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" self._need_readlock() tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability self._need_writelock() if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. """ self._need_writelock() ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) self.append_revision(rev_id) if verbose: note("commited r%d" % self.revno()) def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" self._need_readlock() r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" self._need_readlock() i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" self._need_readlock() if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self._need_readlock() return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise BzrError('invalid history direction %r' % direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] else: return None def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" self._need_readlock() if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self._need_writelock() tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self._need_writelock() ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(self, show_all=False, file_list=None): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo """ self._need_readlock() # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = self.basis_tree() new = self.working_tree() items = diff_trees(old, new) # We want to filter out only if any file was provided in the file_list. if isinstance(file_list, list) and len(file_list): items = [item for item in items if item[3] in file_list] for fs, fid, oldname, newname, kind in items: if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("weird file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/commands.py data 33094 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_args = ['file*'] takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False, file_list=None): b = Branch('.', lock_mode='r') b.show_status(show_all=all, file_list=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from branch import find_branch b = find_branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') # FIXME: probably doesn't handle non-ascii patterns if os.path.exists(ifn): f = b.controlfile(ifn, 'rt') igns = f.read() f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' f = AtomicFile(ifn, 'wt') f.write(igns) f.commit() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False): ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/statcache.py data 6419 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import stat, os, sha, time from binascii import b2a_qp, a2b_qp from trace import mutter from errors import BzrError """File stat cache to speed up tree comparisons. This module basically gives a quick way to find the SHA-1 and related information of a file in the working directory, without actually reading and hashing the whole file. This is done by maintaining a cache indexed by a file fingerprint of (path, size, mtime, ctime, ino, dev) pointing to the SHA-1. If the fingerprint has changed, we assume the file content has not changed either and the SHA-1 is therefore the same. If any of the fingerprint fields have changed then the file content *may* have changed, or it may not have. We need to reread the file contents to make sure, but this is not visible to the user or higher-level code (except as a delay of course). The mtime and ctime are stored with nanosecond fields, but not all filesystems give this level of precision. There is therefore a possible race: the file might be modified twice within a second without changing the size or mtime, and a SHA-1 cached from the first version would be wrong. We handle this by not recording a cached hash for any files which were modified in the current second and that therefore have the chance to change again before the second is up. The only known hole in this design is if the system clock jumps backwards crossing invocations of bzr. Please don't do that; use ntp to gradually adjust your clock or don't use bzr over the step. At the moment this is stored in a simple textfile; it might be nice to use a tdb instead. The cache is represented as a map from file_id to a tuple of (file_id, sha1, path, size, mtime, ctime, ino, dev). """ FP_SIZE = 0 FP_MTIME = 1 FP_CTIME = 2 FP_INO = 3 FP_DEV = 4 SC_FILE_ID = 0 SC_SHA1 = 1 def fingerprint(path, abspath): try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) def _write_cache(basedir, entry_iter, dangerfiles): from atomicfile import AtomicFile cachefn = os.path.join(basedir, '.bzr', 'stat-cache') outf = AtomicFile(cachefn, 'wb', 'utf-8') try: for entry in entry_iter: if entry[0] in dangerfiles: continue outf.write(entry[0] + ' ' + entry[1] + ' ') outf.write(b2a_qp(entry[2], True)) outf.write(' %d %d %d %d %d\n' % entry[3:]) outf.commit() finally: if not outf.closed: outf.abort() def load_cache(basedir): import codecs cache = {} try: cachefn = os.path.join(basedir, '.bzr', 'stat-cache') cachefile = codecs.open(cachefn, 'r', 'utf-8') except IOError: return cache for l in cachefile: f = l.split(' ') file_id = f[0] if file_id in cache: raise BzrError("duplicated file_id in cache: {%s}" % file_id) cache[file_id] = (f[0], f[1], a2b_qp(f[2])) + tuple([long(x) for x in f[3:]]) return cache def _files_from_inventory(inv): for path, ie in inv.iter_entries(): if ie.kind != 'file': continue yield ie.file_id, path def update_cache(basedir, inv, flush=False): """Update and return the cache for the branch. The returned cache may contain entries that have not been written to disk for files recently touched. flush -- discard any previous cache and recalculate from scratch. """ # TODO: It's supposed to be faster to stat the files in order by inum. # We don't directly know the inum of the files of course but we do # know where they were last sighted, so we can sort by that. assert isinstance(flush, bool) if flush: cache = {} else: cache = load_cache(basedir) return _update_cache_from_list(basedir, cache, _files_from_inventory(inv)) def _update_cache_from_list(basedir, cache, to_update): """Update and return the cache for given files. cache -- Previously cached values to be validated. to_update -- Sequence of (file_id, path) pairs to check. """ from sets import Set hardcheck = dirty = 0 # files that have been recently touched and can't be # committed to a persistent cache yet. dangerfiles = Set() now = int(time.time()) for file_id, path in to_update: fap = os.path.join(basedir, path) fp = fingerprint(fap, path) cacheentry = cache.get(file_id) if fp == None: # not here if cacheentry: del cache[file_id] dirty += 1 continue if (fp[FP_MTIME] >= now) or (fp[FP_CTIME] >= now): dangerfiles.add(file_id) if cacheentry and (cacheentry[3:] == fp): continue # all stat fields unchanged hardcheck += 1 dig = sha.new(file(fap, 'rb').read()).hexdigest() if cacheentry == None or dig != cacheentry[1]: # if there was no previous entry for this file, or if the # SHA has changed, then update the cache cacheentry = (file_id, dig, path) + fp cache[file_id] = cacheentry dirty += 1 mutter('statcache: read %d files, %d changed, %d dangerous, ' '%d in cache' % (hardcheck, dirty, len(dangerfiles), len(cache))) if dirty: mutter('updating on-disk statcache') _write_cache(basedir, cache.itervalues(), dangerfiles) return cache M 644 inline bzrlib/tree.py data 7784 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tree classes, representing directory at point in time. """ from sets import Set import os.path, os, fnmatch from osutils import pumpfile, compare_files, filesize, quotefn, sha_file, \ joinpath, splitpath, appendpath, isdir, isfile, file_kind, fingerprint_file import errno from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE from inventory import Inventory from trace import mutter, note from errors import bailout import branch import bzrlib class Tree: """Abstract file tree. There are several subclasses: * `WorkingTree` exists as files on disk editable by the user. * `RevisionTree` is a tree as recorded at some point in the past. * `EmptyTree` Trees contain an `Inventory` object, and also know how to retrieve file texts mentioned in the inventory, either from a working directory or from a store. It is possible for trees to contain files that are not described in their inventory or vice versa; for this use `filenames()`. Trees can be compared, etc, regardless of whether they are working trees or versioned trees. """ def has_filename(self, filename): """True if the tree has given filename.""" raise NotImplementedError() def has_id(self, file_id): return self.inventory.has_id(file_id) def id_set(self): """Return set of all ids in this tree.""" return self.inventory.id_set() def id2path(self, file_id): return self.inventory.id2path(file_id) def _get_inventory(self): return self._inventory inventory = property(_get_inventory, doc="Inventory of this Tree") def _check_retrieved(self, ie, f): fp = fingerprint_file(f) f.seek(0) if ie.text_size != None: if ie.text_size != fp['size']: bailout("mismatched size for file %r in %r" % (ie.file_id, self._store), ["inventory expects %d bytes" % ie.text_size, "file is actually %d bytes" % fp['size'], "store is probably damaged/corrupt"]) if ie.text_sha1 != fp['sha1']: bailout("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store), ["inventory expects %s" % ie.text_sha1, "file is actually %s" % fp['sha1'], "store is probably damaged/corrupt"]) def print_file(self, fileid): """Print file with id `fileid` to stdout.""" import sys pumpfile(self.get_file(fileid), sys.stdout) def export(self, dest): """Export this tree to a new directory. `dest` should not exist, and will be created holding the contents of this tree. TODO: To handle subdirectories we need to create the directories first. :note: If the export fails, the destination directory will be left in a half-assed state. """ os.mkdir(dest) mutter('export version %r' % self) inv = self.inventory for dp, ie in inv.iter_entries(): kind = ie.kind fullpath = appendpath(dest, dp) if kind == 'directory': os.mkdir(fullpath) elif kind == 'file': pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb')) else: bailout("don't know how to export {%s} of kind %r" % (ie.file_id, kind)) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) class RevisionTree(Tree): """Tree viewing a previous revision. File text can be retrieved from the text store. TODO: Some kind of `__repr__` method, but a good one probably means knowing the branch and revision number, or at least passing a description to the constructor. """ def __init__(self, store, inv): self._store = store self._inventory = inv def get_file(self, file_id): ie = self._inventory[file_id] f = self._store[ie.text_id] mutter(" get fileid{%s} from %r" % (file_id, self)) self._check_retrieved(ie, f) return f def get_file_size(self, file_id): return self._inventory[file_id].text_size def get_file_sha1(self, file_id): ie = self._inventory[file_id] return ie.text_sha1 def has_filename(self, filename): return bool(self.inventory.path2id(filename)) def list_files(self): # The only files returned by this are those from the version for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id class EmptyTree(Tree): def __init__(self): self._inventory = Inventory() def has_filename(self, filename): return False def list_files(self): if False: # just to make it a generator yield None ###################################################################### # diff # TODO: Merge these two functions into a single one that can operate # on either a whole tree or a set of files. # TODO: Return the diff in order by filename, not by category or in # random order. Can probably be done by lock-stepping through the # filenames from both trees. def file_status(filename, old_tree, new_tree): """Return single-letter status, old and new names for a file. The complexity here is in deciding how to represent renames; many complex cases are possible. """ old_inv = old_tree.inventory new_inv = new_tree.inventory new_id = new_inv.path2id(filename) old_id = old_inv.path2id(filename) if not new_id and not old_id: # easy: doesn't exist in either; not versioned at all if new_tree.is_ignored(filename): return 'I', None, None else: return '?', None, None elif new_id: # There is now a file of this name, great. pass else: # There is no longer a file of this name, but we can describe # what happened to the file that used to have # this name. There are two possibilities: either it was # deleted entirely, or renamed. assert old_id if new_inv.has_id(old_id): return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id) else: return 'D', old_inv.id2path(old_id), None # if the file_id is new in this revision, it is added if new_id and not old_inv.has_id(new_id): return 'A' # if there used to be a file of this name, but that ID has now # disappeared, it is deleted if old_id and not new_inv.has_id(old_id): return 'D' return 'wtf?' def find_renames(old_inv, new_inv): for file_id in old_inv: if file_id not in new_inv: continue old_name = old_inv.id2path(file_id) new_name = new_inv.id2path(file_id) if old_name != new_name: yield (old_name, new_name) commit refs/heads/master mark :454 committer Martin Pool 1115778295 +1000 data 31 - fix update-stat-cache command from :453 M 644 inline bzrlib/commands.py data 33127 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_args = ['file*'] takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False, file_list=None): b = Branch('.', lock_mode='r') b.show_status(show_all=all, file_list=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from branch import find_branch b = find_branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') # FIXME: probably doesn't handle non-ascii patterns if os.path.exists(ifn): f = b.controlfile(ifn, 'rt') igns = f.read() f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' f = AtomicFile(ifn, 'wt') f.write(igns) f.commit() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False): ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :455 committer Martin Pool 1115778433 +1000 data 16 - fix 'bzr root' from :454 M 644 inline bzrlib/branch.py data 36261 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. base Base directory of the branch. """ _lockmode = None def __init__(self, base, init=False, find_root=True, lock_mode='w'): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.lock(lock_mode) self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def lock(self, mode='w'): """Lock the on-disk branch, excluding other processes.""" try: import fcntl, errno if mode == 'w': lm = fcntl.LOCK_EX om = os.O_WRONLY | os.O_CREAT elif mode == 'r': lm = fcntl.LOCK_SH om = os.O_RDONLY else: raise BzrError("invalid locking mode %r" % mode) try: lockfile = os.open(self.controlfilename('branch-lock'), om) except OSError, e: if e.errno == errno.ENOENT: # might not exist on branches from <0.0.4 self.controlfile('branch-lock', 'w').close() lockfile = os.open(self.controlfilename('branch-lock'), om) else: raise e fcntl.lockf(lockfile, lm) def unlock(): fcntl.lockf(lockfile, fcntl.LOCK_UN) os.close(lockfile) self._lockmode = None self.unlock = unlock self._lockmode = mode except ImportError: warning("please write a locking method for platform %r" % sys.platform) def unlock(): self._lockmode = None self.unlock = unlock self._lockmode = mode def _need_readlock(self): if self._lockmode not in ['r', 'w']: raise BzrError('need read lock on branch, only have %r' % self._lockmode) def _need_writelock(self): if self._lockmode not in ['w']: raise BzrError('need write lock on branch, only have %r' % self._lockmode) def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" self._need_readlock() before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self._need_writelock() ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ self._need_writelock() # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" self._need_readlock() tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability self._need_writelock() if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. """ self._need_writelock() ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) self.append_revision(rev_id) if verbose: note("commited r%d" % self.revno()) def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" self._need_readlock() r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" self._need_readlock() i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" self._need_readlock() if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self._need_readlock() return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise BzrError('invalid history direction %r' % direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] else: return None def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" self._need_readlock() if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self._need_writelock() tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self._need_writelock() ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) def show_status(self, show_all=False, file_list=None): """Display single-line status for non-ignored working files. The list is show sorted in order by file name. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> b.show_status() ? foo >>> b.add('foo') >>> b.show_status() A foo >>> b.commit("add foo") >>> b.show_status() >>> os.unlink(b.abspath('foo')) >>> b.show_status() D foo """ self._need_readlock() # We have to build everything into a list first so that it can # sorted by name, incorporating all the different sources. # FIXME: Rather than getting things in random order and then sorting, # just step through in order. # Interesting case: the old ID for a file has been removed, # but a new file has been created under that name. old = self.basis_tree() new = self.working_tree() items = diff_trees(old, new) # We want to filter out only if any file was provided in the file_list. if isinstance(file_list, list) and len(file_list): items = [item for item in items if item[3] in file_list] for fs, fid, oldname, newname, kind in items: if fs == 'R': show_status(fs, kind, oldname + ' => ' + newname) elif fs == 'A' or fs == 'M': show_status(fs, kind, newname) elif fs == 'D': show_status(fs, kind, oldname) elif fs == '.': if show_all: show_status(fs, kind, newname) elif fs == 'I': if show_all: show_status(fs, kind, newname) elif fs == '?': show_status(fs, kind, newname) else: bailout("weird file state %r" % ((fs, fid),)) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :456 committer Martin Pool 1115778601 +1000 data 20 - tests for bzr root from :455 M 644 inline testbzr data 8878 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): logfile.write('$ %s\n' % cmd) cmd = cmd.split() else: logfile.write('$ %r\n' % cmd) if cmd[0] == 'bzr': cmd[0] = BZRPATH return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open(LOGFILENAME, 'wt', buffering=1) try: mypath = os.path.abspath(sys.argv[0]) print '%-30s %s' % ('running tests from', mypath) global BZRPATH if len(sys.argv) > 1: BZRPATH = sys.argv[1] else: BZRPATH = os.path.join(os.path.split(mypath)[0], 'bzr') print '%-30s %s' % ('against bzr', BZRPATH) print '%-30s %s' % ('in directory', os.getcwd()) print print backtick([BZRPATH, 'version']) runcmd(['mkdir', TESTDIR]) cd(TESTDIR) test_root = os.getcwd() progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("internal tests") runcmd("bzr selftest") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) runcmd("bzr diff --message foo", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == '''? test.txt\n''' out = backtick("bzr status --all") assert out == "? test.txt\n" out = backtick("bzr status test.txt --all") assert out == "? test.txt\n" f = file('test2.txt', 'wt') f.write('goodbye cruel world...\n') f.close() out = backtick("bzr status test.txt") assert out == "? test.txt\n" out = backtick("bzr status") assert out == "? test.txt\n" \ + "? test2.txt\n" os.unlink('test2.txt') progress("command aliases") out = backtick("bzr st --all") assert out == "? test.txt\n" out = backtick("bzr stat") assert out == "? test.txt\n" progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == "A test.txt\n" progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == "sub2/hello.txt\n" assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == 'sub1/sub2/hello.txt\n' assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') assert backtick('bzr relpath sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') cd('..') cd('..') progress('ignore patterns') mkdir('ignorebranch') cd('ignorebranch') runcmd('bzr init') assert backtick('bzr unknowns') == '' file('foo.tmp', 'wt').write('tmp files are ignored') assert backtick('bzr unknowns') == '' file('foo.c', 'wt').write('int main() {}') assert backtick('bzr unknowns') == 'foo.c\n' runcmd('bzr add foo.c') assert backtick('bzr unknowns') == '' file('foo.blah', 'wt').write('blah') assert backtick('bzr unknowns') == 'foo.blah\n' runcmd('bzr ignore *.blah') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rt').read() == '*.blah\n' progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :457 committer Martin Pool 1115779027 +1000 data 39 - more trace and profiling in statcache from :456 M 644 inline bzrlib/statcache.py data 6615 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import stat, os, sha, time from binascii import b2a_qp, a2b_qp from trace import mutter from errors import BzrError """File stat cache to speed up tree comparisons. This module basically gives a quick way to find the SHA-1 and related information of a file in the working directory, without actually reading and hashing the whole file. This is done by maintaining a cache indexed by a file fingerprint of (path, size, mtime, ctime, ino, dev) pointing to the SHA-1. If the fingerprint has changed, we assume the file content has not changed either and the SHA-1 is therefore the same. If any of the fingerprint fields have changed then the file content *may* have changed, or it may not have. We need to reread the file contents to make sure, but this is not visible to the user or higher-level code (except as a delay of course). The mtime and ctime are stored with nanosecond fields, but not all filesystems give this level of precision. There is therefore a possible race: the file might be modified twice within a second without changing the size or mtime, and a SHA-1 cached from the first version would be wrong. We handle this by not recording a cached hash for any files which were modified in the current second and that therefore have the chance to change again before the second is up. The only known hole in this design is if the system clock jumps backwards crossing invocations of bzr. Please don't do that; use ntp to gradually adjust your clock or don't use bzr over the step. At the moment this is stored in a simple textfile; it might be nice to use a tdb instead. The cache is represented as a map from file_id to a tuple of (file_id, sha1, path, size, mtime, ctime, ino, dev). """ FP_SIZE = 0 FP_MTIME = 1 FP_CTIME = 2 FP_INO = 3 FP_DEV = 4 SC_FILE_ID = 0 SC_SHA1 = 1 def fingerprint(path, abspath): try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) def _write_cache(basedir, entry_iter, dangerfiles): from atomicfile import AtomicFile cachefn = os.path.join(basedir, '.bzr', 'stat-cache') outf = AtomicFile(cachefn, 'wb', 'utf-8') try: for entry in entry_iter: if entry[0] in dangerfiles: continue outf.write(entry[0] + ' ' + entry[1] + ' ') outf.write(b2a_qp(entry[2], True)) outf.write(' %d %d %d %d %d\n' % entry[3:]) outf.commit() finally: if not outf.closed: outf.abort() def load_cache(basedir): import codecs cache = {} try: cachefn = os.path.join(basedir, '.bzr', 'stat-cache') cachefile = codecs.open(cachefn, 'r', 'utf-8') except IOError: return cache for l in cachefile: f = l.split(' ') file_id = f[0] if file_id in cache: raise BzrError("duplicated file_id in cache: {%s}" % file_id) cache[file_id] = (f[0], f[1], a2b_qp(f[2])) + tuple([long(x) for x in f[3:]]) return cache def _files_from_inventory(inv): for path, ie in inv.iter_entries(): if ie.kind != 'file': continue yield ie.file_id, path def update_cache(basedir, inv, flush=False): """Update and return the cache for the branch. The returned cache may contain entries that have not been written to disk for files recently touched. flush -- discard any previous cache and recalculate from scratch. """ # TODO: It's supposed to be faster to stat the files in order by inum. # We don't directly know the inum of the files of course but we do # know where they were last sighted, so we can sort by that. assert isinstance(flush, bool) if flush: cache = {} else: cache = load_cache(basedir) return _update_cache_from_list(basedir, cache, _files_from_inventory(inv)) def _update_cache_from_list(basedir, cache, to_update): """Update and return the cache for given files. cache -- Previously cached values to be validated. to_update -- Sequence of (file_id, path) pairs to check. """ from sets import Set stat_cnt = missing_cnt = hardcheck = change_cnt = 0 # files that have been recently touched and can't be # committed to a persistent cache yet. dangerfiles = Set() now = int(time.time()) mutter('update statcache under %r' % basedir) for file_id, path in to_update: abspath = os.path.join(basedir, path) fp = fingerprint(abspath, path) stat_cnt += 1 cacheentry = cache.get(file_id) if fp == None: # not here if cacheentry: del cache[file_id] change_cnt += 1 missing_cnt += 1 continue if (fp[FP_MTIME] >= now) or (fp[FP_CTIME] >= now): dangerfiles.add(file_id) if cacheentry and (cacheentry[3:] == fp): continue # all stat fields unchanged hardcheck += 1 dig = sha.new(file(abspath, 'rb').read()).hexdigest() if cacheentry == None or dig != cacheentry[1]: # if there was no previous entry for this file, or if the # SHA has changed, then update the cache cacheentry = (file_id, dig, path) + fp cache[file_id] = cacheentry change_cnt += 1 mutter('statcache: statted %d files, read %d files, %d changed, %d dangerous, ' '%d in cache' % (stat_cnt, hardcheck, change_cnt, len(dangerfiles), len(cache))) if change_cnt: mutter('updating on-disk statcache') _write_cache(basedir, cache.itervalues(), dangerfiles) return cache commit refs/heads/master mark :458 committer Martin Pool 1115779083 +1000 data 42 - fix statcache update from subdirectories from :457 M 644 inline bzrlib/statcache.py data 6606 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import stat, os, sha, time from binascii import b2a_qp, a2b_qp from trace import mutter from errors import BzrError """File stat cache to speed up tree comparisons. This module basically gives a quick way to find the SHA-1 and related information of a file in the working directory, without actually reading and hashing the whole file. This is done by maintaining a cache indexed by a file fingerprint of (path, size, mtime, ctime, ino, dev) pointing to the SHA-1. If the fingerprint has changed, we assume the file content has not changed either and the SHA-1 is therefore the same. If any of the fingerprint fields have changed then the file content *may* have changed, or it may not have. We need to reread the file contents to make sure, but this is not visible to the user or higher-level code (except as a delay of course). The mtime and ctime are stored with nanosecond fields, but not all filesystems give this level of precision. There is therefore a possible race: the file might be modified twice within a second without changing the size or mtime, and a SHA-1 cached from the first version would be wrong. We handle this by not recording a cached hash for any files which were modified in the current second and that therefore have the chance to change again before the second is up. The only known hole in this design is if the system clock jumps backwards crossing invocations of bzr. Please don't do that; use ntp to gradually adjust your clock or don't use bzr over the step. At the moment this is stored in a simple textfile; it might be nice to use a tdb instead. The cache is represented as a map from file_id to a tuple of (file_id, sha1, path, size, mtime, ctime, ino, dev). """ FP_SIZE = 0 FP_MTIME = 1 FP_CTIME = 2 FP_INO = 3 FP_DEV = 4 SC_FILE_ID = 0 SC_SHA1 = 1 def fingerprint(abspath): try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) def _write_cache(basedir, entry_iter, dangerfiles): from atomicfile import AtomicFile cachefn = os.path.join(basedir, '.bzr', 'stat-cache') outf = AtomicFile(cachefn, 'wb', 'utf-8') try: for entry in entry_iter: if entry[0] in dangerfiles: continue outf.write(entry[0] + ' ' + entry[1] + ' ') outf.write(b2a_qp(entry[2], True)) outf.write(' %d %d %d %d %d\n' % entry[3:]) outf.commit() finally: if not outf.closed: outf.abort() def load_cache(basedir): import codecs cache = {} try: cachefn = os.path.join(basedir, '.bzr', 'stat-cache') cachefile = codecs.open(cachefn, 'r', 'utf-8') except IOError: return cache for l in cachefile: f = l.split(' ') file_id = f[0] if file_id in cache: raise BzrError("duplicated file_id in cache: {%s}" % file_id) cache[file_id] = (f[0], f[1], a2b_qp(f[2])) + tuple([long(x) for x in f[3:]]) return cache def _files_from_inventory(inv): for path, ie in inv.iter_entries(): if ie.kind != 'file': continue yield ie.file_id, path def update_cache(basedir, inv, flush=False): """Update and return the cache for the branch. The returned cache may contain entries that have not been written to disk for files recently touched. flush -- discard any previous cache and recalculate from scratch. """ # TODO: It's supposed to be faster to stat the files in order by inum. # We don't directly know the inum of the files of course but we do # know where they were last sighted, so we can sort by that. assert isinstance(flush, bool) if flush: cache = {} else: cache = load_cache(basedir) return _update_cache_from_list(basedir, cache, _files_from_inventory(inv)) def _update_cache_from_list(basedir, cache, to_update): """Update and return the cache for given files. cache -- Previously cached values to be validated. to_update -- Sequence of (file_id, path) pairs to check. """ from sets import Set stat_cnt = missing_cnt = hardcheck = change_cnt = 0 # files that have been recently touched and can't be # committed to a persistent cache yet. dangerfiles = Set() now = int(time.time()) ## mutter('update statcache under %r' % basedir) for file_id, path in to_update: abspath = os.path.join(basedir, path) fp = fingerprint(abspath) stat_cnt += 1 cacheentry = cache.get(file_id) if fp == None: # not here if cacheentry: del cache[file_id] change_cnt += 1 missing_cnt += 1 continue if (fp[FP_MTIME] >= now) or (fp[FP_CTIME] >= now): dangerfiles.add(file_id) if cacheentry and (cacheentry[3:] == fp): continue # all stat fields unchanged hardcheck += 1 dig = sha.new(file(abspath, 'rb').read()).hexdigest() if cacheentry == None or dig != cacheentry[1]: # if there was no previous entry for this file, or if the # SHA has changed, then update the cache cacheentry = (file_id, dig, path) + fp cache[file_id] = cacheentry change_cnt += 1 mutter('statcache: statted %d files, read %d files, %d changed, %d dangerous, ' '%d in cache' % (stat_cnt, hardcheck, change_cnt, len(dangerfiles), len(cache))) if change_cnt: mutter('updating on-disk statcache') _write_cache(basedir, cache.itervalues(), dangerfiles) return cache commit refs/heads/master mark :459 committer Martin Pool 1115779719 +1000 data 41 - diff now uses stat-cache -- much faster from :458 M 644 inline TODO data 10005 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * testbzr should by default test the bzr binary in the same directory as the testbzr script, or take a path to it as a first parameter. Should show the version from bzr and the path name. * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. Medium things ------------- * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Faster diff/status. Status should be handled differently because it needs to report on deleted and unknown files. diff only needs to deal with versioned files. * Merge Aaron's merge code. * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Should be a signature at the top of the cache file. * Paranoid mode where we never trust SHA-1 matches. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` M 644 inline bzrlib/diff.py data 10823 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set from trace import mutter from errors import BzrError def diff_trees(old_tree, new_tree): """Compute diff between two trees. They may be in different branches and may be working or historical trees. Yields a sequence of (state, id, old_name, new_name, kind). Each filename and each id is listed only once. """ ## TODO: Compare files before diffing; only mention those that have changed ## TODO: Set nice names in the headers, maybe include diffstat ## TODO: Perhaps make this a generator rather than using ## a callback object? ## TODO: Allow specifying a list of files to compare, rather than ## doing the whole tree? (Not urgent.) ## TODO: Allow diffing any two inventories, not just the ## current one against one. We mgiht need to specify two ## stores to look for the files if diffing two branches. That ## might imply this shouldn't be primarily a Branch method. ## XXX: This doesn't report on unknown files; that can be done ## from a separate method. sha_match_cnt = modified_cnt = 0 old_it = old_tree.list_files() new_it = new_tree.list_files() def next(it): try: return it.next() except StopIteration: return None old_item = next(old_it) new_item = next(new_it) # We step through the two sorted iterators in parallel, trying to # keep them lined up. while (old_item != None) or (new_item != None): # OK, we still have some remaining on both, but they may be # out of step. if old_item != None: old_name, old_class, old_kind, old_id = old_item else: old_name = None if new_item != None: new_name, new_class, new_kind, new_id = new_item else: new_name = None if old_item: # can't handle the old tree being a WorkingTree assert old_class == 'V' if new_item and (new_class != 'V'): yield new_class, None, None, new_name, new_kind new_item = next(new_it) elif (not new_item) or (old_item and (old_name < new_name)): if new_tree.has_id(old_id): # will be mentioned as renamed under new name pass else: yield 'D', old_id, old_name, None, old_kind old_item = next(old_it) elif (not old_item) or (new_item and (new_name < old_name)): if old_tree.has_id(new_id): yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind else: yield 'A', new_id, None, new_name, new_kind new_item = next(new_it) elif old_id != new_id: assert old_name == new_name # both trees have a file of this name, but it is not the # same file. in other words, the old filename has been # overwritten by either a newly-added or a renamed file. # (should we return something about the overwritten file?) if old_tree.has_id(new_id): # renaming, overlying a deleted file yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind else: yield 'A', new_id, None, new_name, new_kind new_item = next(new_it) old_item = next(old_it) else: assert old_id == new_id assert old_id != None assert old_name == new_name assert old_kind == new_kind if old_kind == 'directory': yield '.', new_id, old_name, new_name, new_kind elif old_tree.get_file_sha1(old_id) == new_tree.get_file_sha1(old_id): sha_match_cnt += 1 yield '.', new_id, old_name, new_name, new_kind else: modified_cnt += 1 yield 'M', new_id, old_name, new_name, new_kind new_item = next(new_it) old_item = next(old_it) mutter("diff finished: %d SHA matches, %d modified" % (sha_match_cnt, modified_cnt)) def show_diff(b, revision, file_list): import difflib, sys, types if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. if file_list: file_list = [b.relpath(f) for f in file_list] # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in diff_trees(old_tree, new_tree): if file_list and (new_name not in file_list): continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') sys.stdout.writelines(ud) if nonl: print "\\ No newline at end of file" sys.stdout.write('\n') if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: raise BzrError("can't represent state %s {%s}" % (file_state, fid)) class TreeDelta: """Describes changes from one tree to another. Contains four lists: added (path, id) removed (path, id) renamed (oldpath, newpath, id) modified (path, id) A path may occur in more than one list if it was e.g. deleted under an old id and renamed into place in a new id. Files are listed in either modified or renamed, not both. In other words, renamed files may also be modified. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] def compare_inventories(old_inv, new_inv): """Return a TreeDelta object describing changes between inventories. This only describes changes in the shape of the tree, not the actual texts. This is an alternative to diff_trees() and should probably eventually replace it. """ old_ids = old_inv.id_set() new_ids = new_inv.id_set() delta = TreeDelta() delta.removed = [(old_inv.id2path(fid), fid) for fid in (old_ids - new_ids)] delta.removed.sort() delta.added = [(new_inv.id2path(fid), fid) for fid in (new_ids - old_ids)] delta.added.sort() for fid in old_ids & new_ids: old_ie = old_inv[fid] new_ie = new_inv[fid] old_path = old_inv.id2path(fid) new_path = new_inv.id2path(fid) if old_path != new_path: delta.renamed.append((old_path, new_path, fid)) elif old_ie.text_sha1 != new_ie.text_sha1: delta.modified.append((new_path, fid)) delta.modified.sort() delta.renamed.sort() return delta commit refs/heads/master mark :460 committer Martin Pool 1115785131 +1000 data 353 - new testing command compare-trees - change operation of TreeDelta object a bit to specify which renamed files also have modified text - new TreeDelta.show() factored out - new compare_trees similar to compare_inventories but handling WorkingTrees that don't have a SHA-1 in the inventory but can get it from cache - new Inventory.get_file_kind from :459 M 644 inline bzrlib/commands.py data 33395 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_args = ['file*'] takes_options = ['all'] aliases = ['st', 'stat'] def run(self, all=False, file_list=None): b = Branch('.', lock_mode='r') b.show_status(show_all=all, file_list=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from branch import find_branch b = find_branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') # FIXME: probably doesn't handle non-ascii patterns if os.path.exists(ifn): f = b.controlfile(ifn, 'rt') igns = f.read() f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' f = AtomicFile(ifn, 'wt') f.write(igns) f.commit() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False): ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) class cmd_compare_trees(Command): """Show quick calculation of status.""" hidden = True def run(self): import diff b = Branch('.') delta = diff.compare_trees(b.basis_tree(), b.working_tree()) delta.show(sys.stdout, False) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/diff.py data 13256 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set from trace import mutter from errors import BzrError def diff_trees(old_tree, new_tree): """Compute diff between two trees. They may be in different branches and may be working or historical trees. This only compares the versioned files, paying no attention to files which are ignored or unknown. Those can only be present in working trees and can be reported on separately. Yields a sequence of (state, id, old_name, new_name, kind). Each filename and each id is listed only once. """ ## TODO: Allow specifying a list of files to compare, rather than ## doing the whole tree? (Not urgent.) ## TODO: Allow diffing any two inventories, not just the ## current one against one. We mgiht need to specify two ## stores to look for the files if diffing two branches. That ## might imply this shouldn't be primarily a Branch method. sha_match_cnt = modified_cnt = 0 old_it = old_tree.list_files() new_it = new_tree.list_files() def next(it): try: return it.next() except StopIteration: return None old_item = next(old_it) new_item = next(new_it) # We step through the two sorted iterators in parallel, trying to # keep them lined up. while (old_item != None) or (new_item != None): # OK, we still have some remaining on both, but they may be # out of step. if old_item != None: old_name, old_class, old_kind, old_id = old_item else: old_name = None if new_item != None: new_name, new_class, new_kind, new_id = new_item else: new_name = None if old_item: # can't handle the old tree being a WorkingTree assert old_class == 'V' if new_item and (new_class != 'V'): yield new_class, None, None, new_name, new_kind new_item = next(new_it) elif (not new_item) or (old_item and (old_name < new_name)): if new_tree.has_id(old_id): # will be mentioned as renamed under new name pass else: yield 'D', old_id, old_name, None, old_kind old_item = next(old_it) elif (not old_item) or (new_item and (new_name < old_name)): if old_tree.has_id(new_id): yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind else: yield 'A', new_id, None, new_name, new_kind new_item = next(new_it) elif old_id != new_id: assert old_name == new_name # both trees have a file of this name, but it is not the # same file. in other words, the old filename has been # overwritten by either a newly-added or a renamed file. # (should we return something about the overwritten file?) if old_tree.has_id(new_id): # renaming, overlying a deleted file yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind else: yield 'A', new_id, None, new_name, new_kind new_item = next(new_it) old_item = next(old_it) else: assert old_id == new_id assert old_id != None assert old_name == new_name assert old_kind == new_kind if old_kind == 'directory': yield '.', new_id, old_name, new_name, new_kind elif old_tree.get_file_sha1(old_id) == new_tree.get_file_sha1(old_id): sha_match_cnt += 1 yield '.', new_id, old_name, new_name, new_kind else: modified_cnt += 1 yield 'M', new_id, old_name, new_name, new_kind new_item = next(new_it) old_item = next(old_it) mutter("diff finished: %d SHA matches, %d modified" % (sha_match_cnt, modified_cnt)) def show_diff(b, revision, file_list): import difflib, sys, types if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. if file_list: file_list = [b.relpath(f) for f in file_list] # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in diff_trees(old_tree, new_tree): if file_list and (new_name not in file_list): continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') sys.stdout.writelines(ud) if nonl: print "\\ No newline at end of file" sys.stdout.write('\n') if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: raise BzrError("can't represent state %s {%s}" % (file_state, fid)) class TreeDelta: """Describes changes from one tree to another. Contains four lists: added (path, id) removed (path, id) renamed (oldpath, newpath, id, text_modified) modified (path, id) Each id is listed only once. Files that are both modified and renamed are listed only in renamed, with the text_modified flag true. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] def show(self, to_file, show_ids): if self.removed: print >>to_file, 'removed files:' for path, fid in self.removed: if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if self.added: print >>to_file, 'added files:' for path, fid in self.added: if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ' + path if self.renamed: print >>to_file, 'renamed files:' for oldpath, newpath, fid, text_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if self.modified: print >>to_file, 'modified files:' for path, fid in self.modified: if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ' + path def compare_inventories(old_inv, new_inv): """Return a TreeDelta object describing changes between inventories. This only describes changes in the shape of the tree, not the actual texts. This is an alternative to diff_trees() and should probably eventually replace it. """ old_ids = old_inv.id_set() new_ids = new_inv.id_set() delta = TreeDelta() delta.removed = [(old_inv.id2path(fid), fid) for fid in (old_ids - new_ids)] delta.removed.sort() delta.added = [(new_inv.id2path(fid), fid) for fid in (new_ids - old_ids)] delta.added.sort() for fid in old_ids & new_ids: old_ie = old_inv[fid] new_ie = new_inv[fid] old_path = old_inv.id2path(fid) new_path = new_inv.id2path(fid) text_modified = (old_ie.text_sha1 != new_ie.text_sha1) if old_path != new_path: delta.renamed.append((old_path, new_path, fid, text_modified)) elif text_modified: delta.modified.append((new_path, fid)) delta.modified.sort() delta.renamed.sort() return delta def compare_trees(old_tree, new_tree): old_inv = old_tree.inventory new_inv = new_tree.inventory delta = TreeDelta() for file_id in old_inv: if file_id in new_inv: old_path = old_inv.id2path(file_id) new_path = new_inv.id2path(file_id) kind = old_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ 'invalid file kind %r' % kind if kind == 'file': old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False if old_path != new_path: delta.renamed.append((old_path, new_path, file_id, text_modified)) elif text_modified: delta.modified.append((new_path, file_id)) else: delta.removed.append((old_inv.id2path(file_id), file_id)) for file_id in new_inv: if file_id in old_inv: continue delta.added.append((new_inv.id2path(file_id), file_id)) delta.removed.sort() delta.added.sort() delta.renamed.sort() delta.modified.sort() return delta M 644 inline bzrlib/inventory.py data 18692 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # This should really be an id randomly assigned when the tree is # created, but it's not for now. ROOT_ID = "TREE_ROOT" import sys, os.path, types, re from sets import Set try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from xml import XMLMixin from errors import bailout, BzrError, BzrCheckError import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') 'TREE_ROOT' >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID)) >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123')) >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' TODO: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ # TODO: split InventoryEntry into subclasses for files, # directories, etc etc. text_sha1 = None text_size = None def __init__(self, file_id, name, kind, parent_id, text_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID) >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID) Traceback (most recent call last): BzrCheckError: InventoryEntry name 'src/hello.c' is invalid """ if '/' in name or '\\' in name: raise BzrCheckError('InventoryEntry name %r is invalid' % name) self.file_id = file_id self.name = name self.kind = kind self.text_id = text_id self.parent_id = parent_id if kind == 'directory': self.children = {} elif kind == 'file': pass else: raise BzrError("unhandled entry kind %r" % kind) def sorted_children(self): l = self.children.items() l.sort() return l def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.parent_id, text_id=self.text_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size != None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1']: v = getattr(self, f) if v != None: e.set(f, v) # to be conservative, we don't externalize the root pointers # for now, leaving them as null in the xml form. in a future # version it will be implied by nested elements. if self.parent_id != ROOT_ID: assert isinstance(self.parent_id, basestring) e.set('parent_id', self.parent_id) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' ## original format inventories don't have a parent_id for ## nodes in the root directory, but it's cleaner to use one ## internally. parent_id = elt.get('parent_id') if parent_id == None: parent_id = ROOT_ID self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __cmp__(self, other): if self is other: return 0 if not isinstance(other, InventoryEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.name, other.name) \ or cmp(self.text_sha1, other.text_sha1) \ or cmp(self.text_size, other.text_size) \ or cmp(self.text_id, other.text_id) \ or cmp(self.parent_id, other.parent_id) \ or cmp(self.kind, other.kind) class RootEntry(InventoryEntry): def __init__(self, file_id): self.file_id = file_id self.children = {} self.kind = 'root_directory' self.parent_id = None self.name = '' def __cmp__(self, other): if self is other: return 0 if not isinstance(other, RootEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.children, other.children) class Inventory(XMLMixin): """Inventory of versioned files in a tree. This describes which file_id is present at each point in the tree, and possibly the SHA-1 or other information about the file. Entries can be looked up either by path or by file_id. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted, other than through the Inventory API. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID)) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. The inventory is created with a default root directory, with an id of None. """ self.root = RootEntry(ROOT_ID) self._byid = {self.root.file_id: self.root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, from_dir=None): """Return (path, entry) pairs, in order by name.""" if from_dir == None: assert self.root from_dir = self.root elif isinstance(from_dir, basestring): from_dir = self._byid[from_dir] kids = from_dir.children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(from_dir=ie.file_id): yield os.path.join(name, cn), cie def directories(self): """Return (path, entry) pairs for all directories. """ def descend(parent_ie): parent_name = parent_ie.name yield parent_name, parent_ie # directory children in sorted order dn = [] for ie in parent_ie.children.itervalues(): if ie.kind == 'directory': dn.append((ie.name, ie)) dn.sort() for name, child_ie in dn: for sub_name, sub_ie in descend(child_ie): yield appendpath(parent_name, sub_name), sub_ie for name, ie in descend(self.root): yield name, ie def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID)) >>> inv['123123'].name 'hello.c' """ try: return self._byid[file_id] except KeyError: if file_id == None: raise BzrError("can't look up file_id None") else: raise BzrError("file_id {%s} not in inventory" % file_id) def get_file_kind(self, file_id): return self._byid[file_id].kind def get_child(self, parent_id, filename): return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self._byid: bailout("inventory already contains entry with id {%s}" % entry.file_id) try: parent = self._byid[entry.parent_id] except KeyError: bailout("parent_id {%s} not in inventory" % entry.parent_id) if parent.children.has_key(entry.name): bailout("%s is already versioned" % appendpath(self.id2path(parent.file_id), entry.name)) self._byid[entry.file_id] = entry parent.children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: bailout("cannot re-add root of inventory") if file_id == None: file_id = bzrlib.branch.gen_file_id(relpath) parent_id = self.path2id(parts[:-1]) assert parent_id != None ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def id_set(self): return Set(self._byid) def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID)) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __cmp__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 True """ if self is other: return 0 if not isinstance(other, Inventory): return NotImplemented if self.id_set() ^ other.id_set(): return 1 for file_id in self._byid: c = cmp(self[file_id], other[file_id]) if c: return c return 0 def get_idpath(self, file_id): """Return a list of file_ids for the path to an entry. The list contains one element for each directory followed by the id of the file itself. So the length of the returned list is equal to the depth of the file in the tree, counting the root directory as depth 1. """ p = [] while file_id != None: try: ie = self._byid[file_id] except KeyError: bailout("file_id {%s} not found in inventory" % file_id) p.insert(0, ie.file_id) file_id = ie.parent_id return p def id2path(self, file_id): """Return as a list the path to file_id.""" # get all names, skipping root p = [self[fid].name for fid in self.get_idpath(file_id)[1:]] return os.sep.join(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. Returns None iff the path is not found. """ if isinstance(name, types.StringTypes): name = splitpath(name) mutter("lookup path %r" % name) parent = self.root for f in name: try: cie = parent.children[f] assert cie.name == f assert cie.parent_id == parent.file_id parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): return self._byid.has_key(file_id) def rename(self, file_id, new_parent_id, new_name): """Move a file within the inventory. This can change either the name, or the parent, or both. This does not move the working file.""" if not is_valid_name(new_name): bailout("not an acceptable filename: %r" % new_name) new_parent = self._byid[new_parent_id] if new_name in new_parent.children: bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id))) new_parent_idpath = self.get_idpath(new_parent_id) if file_id in new_parent_idpath: bailout("cannot move directory %r into a subdirectory of itself, %r" % (self.id2path(file_id), self.id2path(new_parent_id))) file_ie = self._byid[file_id] old_parent = self._byid[file_ie.parent_id] # TODO: Don't leave things messed up if this fails del old_parent.children[file_ie.name] new_parent.children[new_name] = file_ie file_ie.name = new_name file_ie.parent_id = new_parent_id _NAME_RE = re.compile(r'^[^/\\]+$') def is_valid_name(name): return bool(_NAME_RE.match(name)) M 644 inline bzrlib/log.py data 5083 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def find_touching_revisions(branch, file_id): """Yield a description of revisions which affect the file_id. Each returned element is (revno, revision_id, description) This is the list of revisions where the file is either added, modified, renamed or deleted. Revisions are returned in chronological order. TODO: Perhaps some way to limit this to only particular revisions, or to traverse a non-branch set of revisions? TODO: If a directory is given, then by default look for all changes under that directory. """ last_ie = None last_path = None revno = 1 for revision_id in branch.revision_history(): this_inv = branch.get_revision_inventory(revision_id) if file_id in this_inv: this_ie = this_inv[file_id] this_path = this_inv.id2path(file_id) else: this_ie = this_path = None # now we know how it was last time, and how it is in this revision. # are those two states effectively the same or not? if not this_ie and not last_ie: # not present in either pass elif this_ie and not last_ie: yield revno, revision_id, "added " + this_path elif not this_ie and last_ie: # deleted here yield revno, revision_id, "deleted " + last_path elif this_path != last_path: yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path)) elif (this_ie.text_size != last_ie.text_size or this_ie.text_sha1 != last_ie.text_sha1): yield revno, revision_id, "modified " + this_path last_ie = this_ie last_path = this_path revno += 1 def show_log(branch, filename=None, show_timezone='original', verbose=False, show_ids=False, to_file=None): """Write out human-readable log of commits to this branch. filename If true, list only the commits affecting the specified file, rather than all commits. show_timezone 'original' (committer's timezone), 'utc' (universal time), or 'local' (local user's timezone) verbose If true show added/changed/deleted/renamed files. show_ids If true, show revision and file ids. to_file File to send log to; by default stdout. """ from osutils import format_date from errors import BzrCheckError from diff import compare_inventories from textui import show_status from inventory import Inventory if to_file == None: import sys to_file = sys.stdout if filename: file_id = branch.read_working_inventory().path2id(filename) def which_revs(): for revno, revid, why in find_touching_revisions(branch, file_id): yield revno, revid else: def which_revs(): for i, revid in enumerate(branch.revision_history()): yield i+1, revid branch._need_readlock() precursor = None if verbose: prev_inv = Inventory() for revno, revision_id in which_revs(): print >>to_file, '-' * 60 print >>to_file, 'revno:', revno rev = branch.get_revision(revision_id) if show_ids: print >>to_file, 'revision-id:', revision_id print >>to_file, 'committer:', rev.committer print >>to_file, 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) if revision_id != rev.revision_id: raise BzrCheckError("retrieved wrong revision: %r" % (revision_id, rev.revision_id)) print >>to_file, 'message:' if not rev.message: print >>to_file, ' (no message)' else: for l in rev.message.split('\n'): print >>to_file, ' ' + l # Don't show a list of changed files if we were asked about # one specific file. if verbose and not filename: this_inv = branch.get_inventory(rev.inventory_id) delta = compare_inventories(prev_inv, this_inv) delta.show(to_file, show_ids) prev_inv = this_inv precursor = revision_id commit refs/heads/master mark :461 committer Martin Pool 1115785535 +1000 data 88 - remove compare_inventories() in favor of compare_trees() - add basic tests for bzr log from :460 M 644 inline bzrlib/diff.py data 12147 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set from trace import mutter from errors import BzrError def diff_trees(old_tree, new_tree): """Compute diff between two trees. They may be in different branches and may be working or historical trees. This only compares the versioned files, paying no attention to files which are ignored or unknown. Those can only be present in working trees and can be reported on separately. Yields a sequence of (state, id, old_name, new_name, kind). Each filename and each id is listed only once. """ ## TODO: Allow specifying a list of files to compare, rather than ## doing the whole tree? (Not urgent.) ## TODO: Allow diffing any two inventories, not just the ## current one against one. We mgiht need to specify two ## stores to look for the files if diffing two branches. That ## might imply this shouldn't be primarily a Branch method. sha_match_cnt = modified_cnt = 0 old_it = old_tree.list_files() new_it = new_tree.list_files() def next(it): try: return it.next() except StopIteration: return None old_item = next(old_it) new_item = next(new_it) # We step through the two sorted iterators in parallel, trying to # keep them lined up. while (old_item != None) or (new_item != None): # OK, we still have some remaining on both, but they may be # out of step. if old_item != None: old_name, old_class, old_kind, old_id = old_item else: old_name = None if new_item != None: new_name, new_class, new_kind, new_id = new_item else: new_name = None if old_item: # can't handle the old tree being a WorkingTree assert old_class == 'V' if new_item and (new_class != 'V'): yield new_class, None, None, new_name, new_kind new_item = next(new_it) elif (not new_item) or (old_item and (old_name < new_name)): if new_tree.has_id(old_id): # will be mentioned as renamed under new name pass else: yield 'D', old_id, old_name, None, old_kind old_item = next(old_it) elif (not old_item) or (new_item and (new_name < old_name)): if old_tree.has_id(new_id): yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind else: yield 'A', new_id, None, new_name, new_kind new_item = next(new_it) elif old_id != new_id: assert old_name == new_name # both trees have a file of this name, but it is not the # same file. in other words, the old filename has been # overwritten by either a newly-added or a renamed file. # (should we return something about the overwritten file?) if old_tree.has_id(new_id): # renaming, overlying a deleted file yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind else: yield 'A', new_id, None, new_name, new_kind new_item = next(new_it) old_item = next(old_it) else: assert old_id == new_id assert old_id != None assert old_name == new_name assert old_kind == new_kind if old_kind == 'directory': yield '.', new_id, old_name, new_name, new_kind elif old_tree.get_file_sha1(old_id) == new_tree.get_file_sha1(old_id): sha_match_cnt += 1 yield '.', new_id, old_name, new_name, new_kind else: modified_cnt += 1 yield 'M', new_id, old_name, new_name, new_kind new_item = next(new_it) old_item = next(old_it) mutter("diff finished: %d SHA matches, %d modified" % (sha_match_cnt, modified_cnt)) def show_diff(b, revision, file_list): import difflib, sys, types if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. if file_list: file_list = [b.relpath(f) for f in file_list] # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in diff_trees(old_tree, new_tree): if file_list and (new_name not in file_list): continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') sys.stdout.writelines(ud) if nonl: print "\\ No newline at end of file" sys.stdout.write('\n') if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: raise BzrError("can't represent state %s {%s}" % (file_state, fid)) class TreeDelta: """Describes changes from one tree to another. Contains four lists: added (path, id) removed (path, id) renamed (oldpath, newpath, id, text_modified) modified (path, id) Each id is listed only once. Files that are both modified and renamed are listed only in renamed, with the text_modified flag true. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] def show(self, to_file, show_ids): if self.removed: print >>to_file, 'removed files:' for path, fid in self.removed: if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if self.added: print >>to_file, 'added files:' for path, fid in self.added: if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ' + path if self.renamed: print >>to_file, 'renamed files:' for oldpath, newpath, fid, text_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if self.modified: print >>to_file, 'modified files:' for path, fid in self.modified: if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ' + path def compare_trees(old_tree, new_tree): old_inv = old_tree.inventory new_inv = new_tree.inventory delta = TreeDelta() for file_id in old_inv: if file_id in new_inv: old_path = old_inv.id2path(file_id) new_path = new_inv.id2path(file_id) kind = old_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ 'invalid file kind %r' % kind if kind == 'file': old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False if old_path != new_path: delta.renamed.append((old_path, new_path, file_id, text_modified)) elif text_modified: delta.modified.append((new_path, file_id)) else: delta.removed.append((old_inv.id2path(file_id), file_id)) for file_id in new_inv: if file_id in old_inv: continue delta.added.append((new_inv.id2path(file_id), file_id)) delta.removed.sort() delta.added.sort() delta.renamed.sort() delta.modified.sort() return delta M 644 inline bzrlib/log.py data 5054 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def find_touching_revisions(branch, file_id): """Yield a description of revisions which affect the file_id. Each returned element is (revno, revision_id, description) This is the list of revisions where the file is either added, modified, renamed or deleted. Revisions are returned in chronological order. TODO: Perhaps some way to limit this to only particular revisions, or to traverse a non-branch set of revisions? TODO: If a directory is given, then by default look for all changes under that directory. """ last_ie = None last_path = None revno = 1 for revision_id in branch.revision_history(): this_inv = branch.get_revision_inventory(revision_id) if file_id in this_inv: this_ie = this_inv[file_id] this_path = this_inv.id2path(file_id) else: this_ie = this_path = None # now we know how it was last time, and how it is in this revision. # are those two states effectively the same or not? if not this_ie and not last_ie: # not present in either pass elif this_ie and not last_ie: yield revno, revision_id, "added " + this_path elif not this_ie and last_ie: # deleted here yield revno, revision_id, "deleted " + last_path elif this_path != last_path: yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path)) elif (this_ie.text_size != last_ie.text_size or this_ie.text_sha1 != last_ie.text_sha1): yield revno, revision_id, "modified " + this_path last_ie = this_ie last_path = this_path revno += 1 def show_log(branch, filename=None, show_timezone='original', verbose=False, show_ids=False, to_file=None): """Write out human-readable log of commits to this branch. filename If true, list only the commits affecting the specified file, rather than all commits. show_timezone 'original' (committer's timezone), 'utc' (universal time), or 'local' (local user's timezone) verbose If true show added/changed/deleted/renamed files. show_ids If true, show revision and file ids. to_file File to send log to; by default stdout. """ from osutils import format_date from errors import BzrCheckError from diff import compare_trees from textui import show_status if to_file == None: import sys to_file = sys.stdout if filename: file_id = branch.read_working_inventory().path2id(filename) def which_revs(): for revno, revid, why in find_touching_revisions(branch, file_id): yield revno, revid else: def which_revs(): for i, revid in enumerate(branch.revision_history()): yield i+1, revid branch._need_readlock() precursor = None if verbose: from tree import EmptyTree prev_tree = EmptyTree() for revno, revision_id in which_revs(): print >>to_file, '-' * 60 print >>to_file, 'revno:', revno rev = branch.get_revision(revision_id) if show_ids: print >>to_file, 'revision-id:', revision_id print >>to_file, 'committer:', rev.committer print >>to_file, 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) if revision_id != rev.revision_id: raise BzrCheckError("retrieved wrong revision: %r" % (revision_id, rev.revision_id)) print >>to_file, 'message:' if not rev.message: print >>to_file, ' (no message)' else: for l in rev.message.split('\n'): print >>to_file, ' ' + l # Don't show a list of changed files if we were asked about # one specific file. if verbose: this_tree = branch.revision_tree(revision_id) delta = compare_trees(prev_tree, this_tree) delta.show(to_file, show_ids) prev_tree = this_tree precursor = revision_id M 644 inline testbzr data 8926 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): logfile.write('$ %s\n' % cmd) cmd = cmd.split() else: logfile.write('$ %r\n' % cmd) if cmd[0] == 'bzr': cmd[0] = BZRPATH return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open(LOGFILENAME, 'wt', buffering=1) try: mypath = os.path.abspath(sys.argv[0]) print '%-30s %s' % ('running tests from', mypath) global BZRPATH if len(sys.argv) > 1: BZRPATH = sys.argv[1] else: BZRPATH = os.path.join(os.path.split(mypath)[0], 'bzr') print '%-30s %s' % ('against bzr', BZRPATH) print '%-30s %s' % ('in directory', os.getcwd()) print print backtick([BZRPATH, 'version']) runcmd(['mkdir', TESTDIR]) cd(TESTDIR) test_root = os.getcwd() progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("internal tests") runcmd("bzr selftest") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) runcmd("bzr diff --message foo", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == '''? test.txt\n''' out = backtick("bzr status --all") assert out == "? test.txt\n" out = backtick("bzr status test.txt --all") assert out == "? test.txt\n" f = file('test2.txt', 'wt') f.write('goodbye cruel world...\n') f.close() out = backtick("bzr status test.txt") assert out == "? test.txt\n" out = backtick("bzr status") assert out == "? test.txt\n" \ + "? test2.txt\n" os.unlink('test2.txt') progress("command aliases") out = backtick("bzr st --all") assert out == "? test.txt\n" out = backtick("bzr stat") assert out == "? test.txt\n" progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == "A test.txt\n" progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == "sub2/hello.txt\n" assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == 'sub1/sub2/hello.txt\n' assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') assert backtick('bzr relpath sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') runcmd('bzr log') runcmd('bzr log -v') cd('..') cd('..') progress('ignore patterns') mkdir('ignorebranch') cd('ignorebranch') runcmd('bzr init') assert backtick('bzr unknowns') == '' file('foo.tmp', 'wt').write('tmp files are ignored') assert backtick('bzr unknowns') == '' file('foo.c', 'wt').write('int main() {}') assert backtick('bzr unknowns') == 'foo.c\n' runcmd('bzr add foo.c') assert backtick('bzr unknowns') == '' file('foo.blah', 'wt').write('blah') assert backtick('bzr unknowns') == 'foo.blah\n' runcmd('bzr ignore *.blah') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rt').read() == '*.blah\n' progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :462 committer Martin Pool 1115787433 +1000 data 185 - New form 'file_id in tree' to check if the file is present - Rewrite show_info to work on compare_trees (much faster) - New form 'for file_id in tree' to iterate through files there. from :461 M 644 inline bzrlib/diff.py data 12149 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set from trace import mutter from errors import BzrError def diff_trees(old_tree, new_tree): """Compute diff between two trees. They may be in different branches and may be working or historical trees. This only compares the versioned files, paying no attention to files which are ignored or unknown. Those can only be present in working trees and can be reported on separately. Yields a sequence of (state, id, old_name, new_name, kind). Each filename and each id is listed only once. """ ## TODO: Allow specifying a list of files to compare, rather than ## doing the whole tree? (Not urgent.) ## TODO: Allow diffing any two inventories, not just the ## current one against one. We mgiht need to specify two ## stores to look for the files if diffing two branches. That ## might imply this shouldn't be primarily a Branch method. sha_match_cnt = modified_cnt = 0 old_it = old_tree.list_files() new_it = new_tree.list_files() def next(it): try: return it.next() except StopIteration: return None old_item = next(old_it) new_item = next(new_it) # We step through the two sorted iterators in parallel, trying to # keep them lined up. while (old_item != None) or (new_item != None): # OK, we still have some remaining on both, but they may be # out of step. if old_item != None: old_name, old_class, old_kind, old_id = old_item else: old_name = None if new_item != None: new_name, new_class, new_kind, new_id = new_item else: new_name = None if old_item: # can't handle the old tree being a WorkingTree assert old_class == 'V' if new_item and (new_class != 'V'): yield new_class, None, None, new_name, new_kind new_item = next(new_it) elif (not new_item) or (old_item and (old_name < new_name)): if new_tree.has_id(old_id): # will be mentioned as renamed under new name pass else: yield 'D', old_id, old_name, None, old_kind old_item = next(old_it) elif (not old_item) or (new_item and (new_name < old_name)): if old_tree.has_id(new_id): yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind else: yield 'A', new_id, None, new_name, new_kind new_item = next(new_it) elif old_id != new_id: assert old_name == new_name # both trees have a file of this name, but it is not the # same file. in other words, the old filename has been # overwritten by either a newly-added or a renamed file. # (should we return something about the overwritten file?) if old_tree.has_id(new_id): # renaming, overlying a deleted file yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind else: yield 'A', new_id, None, new_name, new_kind new_item = next(new_it) old_item = next(old_it) else: assert old_id == new_id assert old_id != None assert old_name == new_name assert old_kind == new_kind if old_kind == 'directory': yield '.', new_id, old_name, new_name, new_kind elif old_tree.get_file_sha1(old_id) == new_tree.get_file_sha1(old_id): sha_match_cnt += 1 yield '.', new_id, old_name, new_name, new_kind else: modified_cnt += 1 yield 'M', new_id, old_name, new_name, new_kind new_item = next(new_it) old_item = next(old_it) mutter("diff finished: %d SHA matches, %d modified" % (sha_match_cnt, modified_cnt)) def show_diff(b, revision, file_list): import difflib, sys, types if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. if file_list: file_list = [b.relpath(f) for f in file_list] # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in diff_trees(old_tree, new_tree): if file_list and (new_name not in file_list): continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') sys.stdout.writelines(ud) if nonl: print "\\ No newline at end of file" sys.stdout.write('\n') if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: raise BzrError("can't represent state %s {%s}" % (file_state, fid)) class TreeDelta: """Describes changes from one tree to another. Contains four lists: added (path, id) removed (path, id) renamed (oldpath, newpath, id, text_modified) modified (path, id) Each id is listed only once. Files that are both modified and renamed are listed only in renamed, with the text_modified flag true. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] def show(self, to_file, show_ids): if self.removed: print >>to_file, 'removed files:' for path, fid in self.removed: if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if self.added: print >>to_file, 'added files:' for path, fid in self.added: if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ' + path if self.renamed: print >>to_file, 'renamed files:' for oldpath, newpath, fid, text_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if self.modified: print >>to_file, 'modified files:' for path, fid in self.modified: if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ' + path def compare_trees(old_tree, new_tree): old_inv = old_tree.inventory new_inv = new_tree.inventory delta = TreeDelta() for file_id in old_tree: if file_id in new_tree: old_path = old_inv.id2path(file_id) new_path = new_inv.id2path(file_id) kind = old_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ 'invalid file kind %r' % kind if kind == 'file': old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False if old_path != new_path: delta.renamed.append((old_path, new_path, file_id, text_modified)) elif text_modified: delta.modified.append((new_path, file_id)) else: delta.removed.append((old_inv.id2path(file_id), file_id)) for file_id in new_inv: if file_id in old_inv: continue delta.added.append((new_inv.id2path(file_id), file_id)) delta.removed.sort() delta.added.sort() delta.renamed.sort() delta.modified.sort() return delta M 644 inline bzrlib/info.py data 3482 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import time from osutils import format_date def _countiter(it): # surely there's a builtin for this? i = 0 for j in it: i += 1 return i def show_info(b): import diff print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl != None: return pl else: return 's' count_version_dirs = 0 basis = b.basis_tree() working = b.working_tree() work_inv = working.inventory delta = diff.compare_trees(basis, working) print print 'in the working tree:' print ' %8s unchanged' % '?' print ' %8d modified' % len(delta.modified) print ' %8d added' % len(delta.added) print ' %8d removed' % len(delta.removed) print ' %8d renamed' % len(delta.renamed) ignore_cnt = unknown_cnt = 0 for path in working.extras(): if working.is_ignored(path): ignore_cnt += 1 else: unknown_cnt += 1 print ' %8d unknown' % unknown_cnt print ' %8d ignored' % ignore_cnt dir_cnt = 0 for file_id in work_inv: if work_inv.get_file_kind(file_id) == 'directory': dir_cnt += 1 print ' %8d versioned %s' \ % (dir_cnt, plural(dir_cnt, 'subdirectory', 'subdirectories')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %8d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %8d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %8d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) print print 'text store:' c, t = b.text_store.total_size() print ' %8d file texts' % c print ' %8d kB' % (t/1024) print print 'revision store:' c, t = b.revision_store.total_size() print ' %8d revisions' % c print ' %8d kB' % (t/1024) print print 'inventory store:' c, t = b.inventory_store.total_size() print ' %8d inventories' % c print ' %8d kB' % (t/1024) M 644 inline bzrlib/tree.py data 7872 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tree classes, representing directory at point in time. """ from sets import Set import os.path, os, fnmatch from osutils import pumpfile, compare_files, filesize, quotefn, sha_file, \ joinpath, splitpath, appendpath, isdir, isfile, file_kind, fingerprint_file import errno from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE from inventory import Inventory from trace import mutter, note from errors import bailout import branch import bzrlib class Tree: """Abstract file tree. There are several subclasses: * `WorkingTree` exists as files on disk editable by the user. * `RevisionTree` is a tree as recorded at some point in the past. * `EmptyTree` Trees contain an `Inventory` object, and also know how to retrieve file texts mentioned in the inventory, either from a working directory or from a store. It is possible for trees to contain files that are not described in their inventory or vice versa; for this use `filenames()`. Trees can be compared, etc, regardless of whether they are working trees or versioned trees. """ def has_filename(self, filename): """True if the tree has given filename.""" raise NotImplementedError() def has_id(self, file_id): return self.inventory.has_id(file_id) __contains__ = has_id def id_set(self): """Return set of all ids in this tree.""" return self.inventory.id_set() def __iter__(self): return iter(self.inventory) def id2path(self, file_id): return self.inventory.id2path(file_id) def _get_inventory(self): return self._inventory inventory = property(_get_inventory, doc="Inventory of this Tree") def _check_retrieved(self, ie, f): fp = fingerprint_file(f) f.seek(0) if ie.text_size != None: if ie.text_size != fp['size']: bailout("mismatched size for file %r in %r" % (ie.file_id, self._store), ["inventory expects %d bytes" % ie.text_size, "file is actually %d bytes" % fp['size'], "store is probably damaged/corrupt"]) if ie.text_sha1 != fp['sha1']: bailout("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store), ["inventory expects %s" % ie.text_sha1, "file is actually %s" % fp['sha1'], "store is probably damaged/corrupt"]) def print_file(self, fileid): """Print file with id `fileid` to stdout.""" import sys pumpfile(self.get_file(fileid), sys.stdout) def export(self, dest): """Export this tree to a new directory. `dest` should not exist, and will be created holding the contents of this tree. TODO: To handle subdirectories we need to create the directories first. :note: If the export fails, the destination directory will be left in a half-assed state. """ os.mkdir(dest) mutter('export version %r' % self) inv = self.inventory for dp, ie in inv.iter_entries(): kind = ie.kind fullpath = appendpath(dest, dp) if kind == 'directory': os.mkdir(fullpath) elif kind == 'file': pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb')) else: bailout("don't know how to export {%s} of kind %r" % (ie.file_id, kind)) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) class RevisionTree(Tree): """Tree viewing a previous revision. File text can be retrieved from the text store. TODO: Some kind of `__repr__` method, but a good one probably means knowing the branch and revision number, or at least passing a description to the constructor. """ def __init__(self, store, inv): self._store = store self._inventory = inv def get_file(self, file_id): ie = self._inventory[file_id] f = self._store[ie.text_id] mutter(" get fileid{%s} from %r" % (file_id, self)) self._check_retrieved(ie, f) return f def get_file_size(self, file_id): return self._inventory[file_id].text_size def get_file_sha1(self, file_id): ie = self._inventory[file_id] return ie.text_sha1 def has_filename(self, filename): return bool(self.inventory.path2id(filename)) def list_files(self): # The only files returned by this are those from the version for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id class EmptyTree(Tree): def __init__(self): self._inventory = Inventory() def has_filename(self, filename): return False def list_files(self): if False: # just to make it a generator yield None ###################################################################### # diff # TODO: Merge these two functions into a single one that can operate # on either a whole tree or a set of files. # TODO: Return the diff in order by filename, not by category or in # random order. Can probably be done by lock-stepping through the # filenames from both trees. def file_status(filename, old_tree, new_tree): """Return single-letter status, old and new names for a file. The complexity here is in deciding how to represent renames; many complex cases are possible. """ old_inv = old_tree.inventory new_inv = new_tree.inventory new_id = new_inv.path2id(filename) old_id = old_inv.path2id(filename) if not new_id and not old_id: # easy: doesn't exist in either; not versioned at all if new_tree.is_ignored(filename): return 'I', None, None else: return '?', None, None elif new_id: # There is now a file of this name, great. pass else: # There is no longer a file of this name, but we can describe # what happened to the file that used to have # this name. There are two possibilities: either it was # deleted entirely, or renamed. assert old_id if new_inv.has_id(old_id): return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id) else: return 'D', old_inv.id2path(old_id), None # if the file_id is new in this revision, it is added if new_id and not old_inv.has_id(new_id): return 'A' # if there used to be a file of this name, but that ID has now # disappeared, it is deleted if old_id and not new_inv.has_id(old_id): return 'D' return 'wtf?' def find_renames(old_inv, new_inv): for file_id in old_inv: if file_id not in new_inv: continue old_name = old_inv.id2path(file_id) new_name = new_inv.id2path(file_id) if old_name != new_name: yield (old_name, new_name) M 644 inline bzrlib/workingtree.py data 9118 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os import bzrlib.tree from errors import BzrCheckError from trace import mutter class WorkingTree(bzrlib.tree.Tree): """Working copy tree. The inventory is held in the `Branch` working-inventory, and the files are in a directory on disk. It is possible for a `WorkingTree` to have a filename which is not listed in the Inventory and vice versa. """ _statcache = None def __init__(self, basedir, inv): self._inventory = inv self.basedir = basedir self.path2id = inv.path2id def __iter__(self): """Iterate through file_ids for this tree. file_ids are in a WorkingTree if they are in the working inventory and the working file exists. """ self._update_statcache() inv = self._inventory for file_id in self._inventory: # TODO: This is slightly redundant; we should be able to just # check the statcache but it only includes regular files. # only include files which still exist on disk ie = inv[file_id] if ie.kind == 'file': if ((file_id in self._statcache) or (os.path.exists(self.abspath(inv.id2path(file_id))))): yield file_id def __repr__(self): return "<%s of %s>" % (self.__class__.__name__, self.basedir) def abspath(self, filename): return os.path.join(self.basedir, filename) def has_filename(self, filename): return os.path.exists(self.abspath(filename)) def get_file(self, file_id): return self.get_file_byname(self.id2path(file_id)) def get_file_byname(self, filename): return file(self.abspath(filename), 'rb') def _get_store_filename(self, file_id): ## XXX: badly named; this isn't in the store at all return self.abspath(self.id2path(file_id)) def has_id(self, file_id): # files that have been deleted are excluded if not self.inventory.has_id(file_id): return False self._update_statcache() if file_id in self._statcache: return True return os.path.exists(self.abspath(self.id2path(file_id))) __contains__ = has_id def _update_statcache(self): import statcache if not self._statcache: self._statcache = statcache.update_cache(self.basedir, self.inventory) def get_file_size(self, file_id): import os, stat return os.stat(self._get_store_filename(file_id))[stat.ST_SIZE] def get_file_sha1(self, file_id): import statcache self._update_statcache() return self._statcache[file_id][statcache.SC_SHA1] def file_class(self, filename): if self.path2id(filename): return 'V' elif self.is_ignored(filename): return 'I' else: return '?' def list_files(self): """Recursively list all files as (path, class, kind, id). Lists, but does not descend into unversioned directories. This does not include files that have been deleted in this tree. Skips the control directory. """ from osutils import appendpath, file_kind import os inv = self.inventory def descend(from_dir_relpath, from_dir_id, dp): ls = os.listdir(dp) ls.sort() for f in ls: ## TODO: If we find a subdirectory with its own .bzr ## directory, then that is a separate tree and we ## should exclude it. if bzrlib.BZRDIR == f: continue # path within tree fp = appendpath(from_dir_relpath, f) # absolute path fap = appendpath(dp, f) f_ie = inv.get_child(from_dir_id, f) if f_ie: c = 'V' elif self.is_ignored(fp): c = 'I' else: c = '?' fk = file_kind(fap) if f_ie: if f_ie.kind != fk: raise BzrCheckError("file %r entered as kind %r id %r, " "now of kind %r" % (fap, f_ie.kind, f_ie.file_id, fk)) yield fp, c, fk, (f_ie and f_ie.file_id) if fk != 'directory': continue if c != 'V': # don't descend unversioned directories continue for ff in descend(fp, f_ie.file_id, fap): yield ff for f in descend('', inv.root.file_id, self.basedir): yield f def unknowns(self): for subp in self.extras(): if not self.is_ignored(subp): yield subp def extras(self): """Yield all unknown files in this WorkingTree. If there are any unknown directories then only the directory is returned, not all its children. But if there are unknown files under a versioned subdirectory, they are returned. Currently returned depth-first, sorted by name within directories. """ ## TODO: Work from given directory downwards from osutils import isdir, appendpath for path, dir_entry in self.inventory.directories(): mutter("search for unknowns in %r" % path) dirabs = self.abspath(path) if not isdir(dirabs): # e.g. directory deleted continue fl = [] for subf in os.listdir(dirabs): if (subf != '.bzr' and (subf not in dir_entry.children)): fl.append(subf) fl.sort() for subf in fl: subp = appendpath(path, subf) yield subp def ignored_files(self): """Yield list of PATH, IGNORE_PATTERN""" for subp in self.extras(): pat = self.is_ignored(subp) if pat != None: yield subp, pat def get_ignore_list(self): """Return list of ignore patterns. Cached in the Tree object after the first call. """ if hasattr(self, '_ignorelist'): return self._ignorelist l = bzrlib.DEFAULT_IGNORE[:] if self.has_filename(bzrlib.IGNORE_FILENAME): f = self.get_file_byname(bzrlib.IGNORE_FILENAME) l.extend([line.rstrip("\n\r") for line in f.readlines()]) self._ignorelist = l return l def is_ignored(self, filename): r"""Check whether the filename matches an ignore pattern. Patterns containing '/' or '\' need to match the whole path; others match against only the last component. If the file is ignored, returns the pattern which caused it to be ignored, otherwise None. So this can simply be used as a boolean if desired.""" # TODO: Use '**' to match directories, and other extended # globbing stuff from cvs/rsync. # XXX: fnmatch is actually not quite what we want: it's only # approximately the same as real Unix fnmatch, and doesn't # treat dotfiles correctly and allows * to match /. # Eventually it should be replaced with something more # accurate. import fnmatch from osutils import splitpath for pat in self.get_ignore_list(): if '/' in pat or '\\' in pat: # as a special case, you can put ./ at the start of a # pattern; this is good to match in the top-level # only; if (pat[:2] == './') or (pat[:2] == '.\\'): newpat = pat[2:] else: newpat = pat if fnmatch.fnmatchcase(filename, newpat): return pat else: if fnmatch.fnmatchcase(splitpath(filename)[-1], pat): return pat else: return None commit refs/heads/master mark :463 committer Martin Pool 1115787755 +1000 data 46 - compare_trees() also reports unchanged files from :462 M 644 inline bzrlib/diff.py data 12350 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set from trace import mutter from errors import BzrError def diff_trees(old_tree, new_tree): """Compute diff between two trees. They may be in different branches and may be working or historical trees. This only compares the versioned files, paying no attention to files which are ignored or unknown. Those can only be present in working trees and can be reported on separately. Yields a sequence of (state, id, old_name, new_name, kind). Each filename and each id is listed only once. """ ## TODO: Allow specifying a list of files to compare, rather than ## doing the whole tree? (Not urgent.) ## TODO: Allow diffing any two inventories, not just the ## current one against one. We mgiht need to specify two ## stores to look for the files if diffing two branches. That ## might imply this shouldn't be primarily a Branch method. sha_match_cnt = modified_cnt = 0 old_it = old_tree.list_files() new_it = new_tree.list_files() def next(it): try: return it.next() except StopIteration: return None old_item = next(old_it) new_item = next(new_it) # We step through the two sorted iterators in parallel, trying to # keep them lined up. while (old_item != None) or (new_item != None): # OK, we still have some remaining on both, but they may be # out of step. if old_item != None: old_name, old_class, old_kind, old_id = old_item else: old_name = None if new_item != None: new_name, new_class, new_kind, new_id = new_item else: new_name = None if old_item: # can't handle the old tree being a WorkingTree assert old_class == 'V' if new_item and (new_class != 'V'): yield new_class, None, None, new_name, new_kind new_item = next(new_it) elif (not new_item) or (old_item and (old_name < new_name)): if new_tree.has_id(old_id): # will be mentioned as renamed under new name pass else: yield 'D', old_id, old_name, None, old_kind old_item = next(old_it) elif (not old_item) or (new_item and (new_name < old_name)): if old_tree.has_id(new_id): yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind else: yield 'A', new_id, None, new_name, new_kind new_item = next(new_it) elif old_id != new_id: assert old_name == new_name # both trees have a file of this name, but it is not the # same file. in other words, the old filename has been # overwritten by either a newly-added or a renamed file. # (should we return something about the overwritten file?) if old_tree.has_id(new_id): # renaming, overlying a deleted file yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind else: yield 'A', new_id, None, new_name, new_kind new_item = next(new_it) old_item = next(old_it) else: assert old_id == new_id assert old_id != None assert old_name == new_name assert old_kind == new_kind if old_kind == 'directory': yield '.', new_id, old_name, new_name, new_kind elif old_tree.get_file_sha1(old_id) == new_tree.get_file_sha1(old_id): sha_match_cnt += 1 yield '.', new_id, old_name, new_name, new_kind else: modified_cnt += 1 yield 'M', new_id, old_name, new_name, new_kind new_item = next(new_it) old_item = next(old_it) mutter("diff finished: %d SHA matches, %d modified" % (sha_match_cnt, modified_cnt)) def show_diff(b, revision, file_list): import difflib, sys, types if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. if file_list: file_list = [b.relpath(f) for f in file_list] # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in diff_trees(old_tree, new_tree): if file_list and (new_name not in file_list): continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') sys.stdout.writelines(ud) if nonl: print "\\ No newline at end of file" sys.stdout.write('\n') if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: raise BzrError("can't represent state %s {%s}" % (file_state, fid)) class TreeDelta: """Describes changes from one tree to another. Contains four lists: added (path, id) removed (path, id) renamed (oldpath, newpath, id, text_modified) modified (path, id) unchanged (path, id) Each id is listed only once. Files that are both modified and renamed are listed only in renamed, with the text_modified flag true. The lists are normally sorted when the delta is created. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] self.unchanged = [] def show(self, to_file, show_ids): if self.removed: print >>to_file, 'removed files:' for path, fid in self.removed: if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if self.added: print >>to_file, 'added files:' for path, fid in self.added: if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ' + path if self.renamed: print >>to_file, 'renamed files:' for oldpath, newpath, fid, text_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if self.modified: print >>to_file, 'modified files:' for path, fid in self.modified: if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ' + path def compare_trees(old_tree, new_tree): old_inv = old_tree.inventory new_inv = new_tree.inventory delta = TreeDelta() for file_id in old_tree: if file_id in new_tree: old_path = old_inv.id2path(file_id) new_path = new_inv.id2path(file_id) kind = old_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ 'invalid file kind %r' % kind if kind == 'file': old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False if old_path != new_path: delta.renamed.append((old_path, new_path, file_id, text_modified)) elif text_modified: delta.modified.append((new_path, file_id)) else: delta.unchanged.append((new_path, file_id)) else: delta.removed.append((old_inv.id2path(file_id), file_id)) for file_id in new_inv: if file_id in old_inv: continue delta.added.append((new_inv.id2path(file_id), file_id)) delta.removed.sort() delta.added.sort() delta.renamed.sort() delta.modified.sort() return delta M 644 inline bzrlib/info.py data 3499 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import time from osutils import format_date def _countiter(it): # surely there's a builtin for this? i = 0 for j in it: i += 1 return i def show_info(b): import diff print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl != None: return pl else: return 's' count_version_dirs = 0 basis = b.basis_tree() working = b.working_tree() work_inv = working.inventory delta = diff.compare_trees(basis, working) print print 'in the working tree:' print ' %8s unchanged' % len(delta.unchanged) print ' %8d modified' % len(delta.modified) print ' %8d added' % len(delta.added) print ' %8d removed' % len(delta.removed) print ' %8d renamed' % len(delta.renamed) ignore_cnt = unknown_cnt = 0 for path in working.extras(): if working.is_ignored(path): ignore_cnt += 1 else: unknown_cnt += 1 print ' %8d unknown' % unknown_cnt print ' %8d ignored' % ignore_cnt dir_cnt = 0 for file_id in work_inv: if work_inv.get_file_kind(file_id) == 'directory': dir_cnt += 1 print ' %8d versioned %s' \ % (dir_cnt, plural(dir_cnt, 'subdirectory', 'subdirectories')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %8d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %8d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %8d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) print print 'text store:' c, t = b.text_store.total_size() print ' %8d file texts' % c print ' %8d kB' % (t/1024) print print 'revision store:' c, t = b.revision_store.total_size() print ' %8d revisions' % c print ' %8d kB' % (t/1024) print print 'inventory store:' c, t = b.inventory_store.total_size() print ' %8d inventories' % c print ' %8d kB' % (t/1024) commit refs/heads/master mark :464 committer Martin Pool 1115788513 +1000 data 18 - remove dead code from :463 M 644 inline bzrlib/status.py data 726 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA commit refs/heads/master mark :465 committer Martin Pool 1115790927 +1000 data 244 - Move show_status() out of Branch into a new function in bzrlib.status - New option --show-ids for status command - Refactor TreeDelta.show() and add optional support for showing unmodified files - Changed output format for status command from :464 M 644 inline NEWS data 7120 bzr-0.0.5 NOT RELEASED YET CHANGES: * ``bzr`` with no command now shows help rather than giving an error. Suggested by Michael Ellerman. * ``bzr status`` output format changed, because svn-style output doesn't really match the model of bzr. Now files are grouped by status and can be shown with their IDs. ``bzr status --all`` shows all versioned files and unknown files but not ignored files. ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New ``bzr ignore PATTERN`` command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. * ``bzr log`` and ``bzr root`` can be given an http URL instead of a filename. * Commands can now be defined by external programs or scripts in a directory on $BZRPATH. * New "stat cache" avoids reading the contents of files if they haven't changed since the previous time. * If the Python interpreter is too old, try to find a better one or give an error. Based on a patch from Fredrik Lundh. BUG FIXES: * Fixed diff format so that added and removed files will be handled properly by patch. Fix from Lalo Martins. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. * New developer commands ``added``, ``modified``. PORTABILITY: * Cope on Windows on python2.3 by using the weaker random seed. 2.4 is now only recommended. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/branch.py data 34288 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status from diff import diff_trees BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. base Base directory of the branch. """ _lockmode = None def __init__(self, base, init=False, find_root=True, lock_mode='w'): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.lock(lock_mode) self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def lock(self, mode='w'): """Lock the on-disk branch, excluding other processes.""" try: import fcntl, errno if mode == 'w': lm = fcntl.LOCK_EX om = os.O_WRONLY | os.O_CREAT elif mode == 'r': lm = fcntl.LOCK_SH om = os.O_RDONLY else: raise BzrError("invalid locking mode %r" % mode) try: lockfile = os.open(self.controlfilename('branch-lock'), om) except OSError, e: if e.errno == errno.ENOENT: # might not exist on branches from <0.0.4 self.controlfile('branch-lock', 'w').close() lockfile = os.open(self.controlfilename('branch-lock'), om) else: raise e fcntl.lockf(lockfile, lm) def unlock(): fcntl.lockf(lockfile, fcntl.LOCK_UN) os.close(lockfile) self._lockmode = None self.unlock = unlock self._lockmode = mode except ImportError: warning("please write a locking method for platform %r" % sys.platform) def unlock(): self._lockmode = None self.unlock = unlock self._lockmode = mode def _need_readlock(self): if self._lockmode not in ['r', 'w']: raise BzrError('need read lock on branch, only have %r' % self._lockmode) def _need_writelock(self): if self._lockmode not in ['w']: raise BzrError('need write lock on branch, only have %r' % self._lockmode) def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" self._need_readlock() before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self._need_writelock() ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ self._need_writelock() # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" self._need_readlock() tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability self._need_writelock() if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. """ self._need_writelock() ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) self.append_revision(rev_id) if verbose: note("commited r%d" % self.revno()) def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" self._need_readlock() r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" self._need_readlock() i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" self._need_readlock() if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self._need_readlock() return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise BzrError('invalid history direction %r' % direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] else: return None def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" self._need_readlock() if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self._need_writelock() tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self._need_writelock() ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/commands.py data 33505 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. For each file there is a single line giving its file state and name. The name is that in the current revision unless it is deleted or missing, in which case the old name is shown. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): b = Branch('.', lock_mode='r') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, file_list=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from branch import find_branch b = find_branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') # FIXME: probably doesn't handle non-ascii patterns if os.path.exists(ifn): f = b.controlfile(ifn, 'rt') igns = f.read() f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' f = AtomicFile(ifn, 'wt') f.write(igns) f.commit() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False): ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) class cmd_compare_trees(Command): """Show quick calculation of status.""" hidden = True def run(self): import diff b = Branch('.') delta = diff.compare_trees(b.basis_tree(), b.working_tree()) delta.show(sys.stdout, False) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/diff.py data 12292 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set from trace import mutter from errors import BzrError def diff_trees(old_tree, new_tree): """Compute diff between two trees. They may be in different branches and may be working or historical trees. This only compares the versioned files, paying no attention to files which are ignored or unknown. Those can only be present in working trees and can be reported on separately. Yields a sequence of (state, id, old_name, new_name, kind). Each filename and each id is listed only once. """ ## TODO: Allow specifying a list of files to compare, rather than ## doing the whole tree? (Not urgent.) ## TODO: Allow diffing any two inventories, not just the ## current one against one. We mgiht need to specify two ## stores to look for the files if diffing two branches. That ## might imply this shouldn't be primarily a Branch method. sha_match_cnt = modified_cnt = 0 old_it = old_tree.list_files() new_it = new_tree.list_files() def next(it): try: return it.next() except StopIteration: return None old_item = next(old_it) new_item = next(new_it) # We step through the two sorted iterators in parallel, trying to # keep them lined up. while (old_item != None) or (new_item != None): # OK, we still have some remaining on both, but they may be # out of step. if old_item != None: old_name, old_class, old_kind, old_id = old_item else: old_name = None if new_item != None: new_name, new_class, new_kind, new_id = new_item else: new_name = None if old_item: # can't handle the old tree being a WorkingTree assert old_class == 'V' if new_item and (new_class != 'V'): yield new_class, None, None, new_name, new_kind new_item = next(new_it) elif (not new_item) or (old_item and (old_name < new_name)): if new_tree.has_id(old_id): # will be mentioned as renamed under new name pass else: yield 'D', old_id, old_name, None, old_kind old_item = next(old_it) elif (not old_item) or (new_item and (new_name < old_name)): if old_tree.has_id(new_id): yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind else: yield 'A', new_id, None, new_name, new_kind new_item = next(new_it) elif old_id != new_id: assert old_name == new_name # both trees have a file of this name, but it is not the # same file. in other words, the old filename has been # overwritten by either a newly-added or a renamed file. # (should we return something about the overwritten file?) if old_tree.has_id(new_id): # renaming, overlying a deleted file yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind else: yield 'A', new_id, None, new_name, new_kind new_item = next(new_it) old_item = next(old_it) else: assert old_id == new_id assert old_id != None assert old_name == new_name assert old_kind == new_kind if old_kind == 'directory': yield '.', new_id, old_name, new_name, new_kind elif old_tree.get_file_sha1(old_id) == new_tree.get_file_sha1(old_id): sha_match_cnt += 1 yield '.', new_id, old_name, new_name, new_kind else: modified_cnt += 1 yield 'M', new_id, old_name, new_name, new_kind new_item = next(new_it) old_item = next(old_it) mutter("diff finished: %d SHA matches, %d modified" % (sha_match_cnt, modified_cnt)) def show_diff(b, revision, file_list): import difflib, sys, types if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. if file_list: file_list = [b.relpath(f) for f in file_list] # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in diff_trees(old_tree, new_tree): if file_list and (new_name not in file_list): continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') sys.stdout.writelines(ud) if nonl: print "\\ No newline at end of file" sys.stdout.write('\n') if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: raise BzrError("can't represent state %s {%s}" % (file_state, fid)) class TreeDelta: """Describes changes from one tree to another. Contains four lists: added (path, id) removed (path, id) renamed (oldpath, newpath, id, text_modified) modified (path, id) unchanged (path, id) Each id is listed only once. Files that are both modified and renamed are listed only in renamed, with the text_modified flag true. The lists are normally sorted when the delta is created. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] self.unchanged = [] def show(self, to_file, show_ids=False, show_unchanged=False): def show_list(files): for path, fid in files: if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if self.removed: print >>to_file, 'removed files:' show_list(self.removed) if self.added: print >>to_file, 'added files:' show_list(self.added) if self.renamed: print >>to_file, 'renamed files:' for oldpath, newpath, fid, text_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if self.modified: print >>to_file, 'modified files:' show_list(self.modified) if show_unchanged and self.unchanged: print >>to_file, 'unchanged files:' show_list(self.unchanged) def compare_trees(old_tree, new_tree): old_inv = old_tree.inventory new_inv = new_tree.inventory delta = TreeDelta() for file_id in old_tree: if file_id in new_tree: old_path = old_inv.id2path(file_id) new_path = new_inv.id2path(file_id) kind = old_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ 'invalid file kind %r' % kind if kind == 'file': old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False if old_path != new_path: delta.renamed.append((old_path, new_path, file_id, text_modified)) elif text_modified: delta.modified.append((new_path, file_id)) else: delta.unchanged.append((new_path, file_id)) else: delta.removed.append((old_inv.id2path(file_id), file_id)) for file_id in new_inv: if file_id in old_inv: continue delta.added.append((new_inv.id2path(file_id), file_id)) delta.removed.sort() delta.added.sort() delta.renamed.sort() delta.modified.sort() return delta M 644 inline bzrlib/status.py data 1667 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def show_status(branch, show_unchanged=False, file_list=None, show_ids=False): """Display single-line status for non-ignored working files. show_all If true, show unmodified files too. file_list If set, only show the status of files in this list. """ import sys import diff branch._need_readlock() old = branch.basis_tree() new = branch.working_tree() if file_list: raise NotImplementedError("sorry, status on selected files is not implemented " "at the moment") delta = diff.compare_trees(old, new) delta.show(sys.stdout, show_ids=show_ids, show_unchanged=show_unchanged) unknowns = new.unknowns() done_header = False for path in unknowns: if not done_header: print 'unknown files:' done_header = True print ' ', path commit refs/heads/master mark :466 committer Martin Pool 1115791208 +1000 data 24 - doc for status command from :465 M 644 inline bzrlib/commands.py data 34156 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but not versioned or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): b = Branch('.', lock_mode='r') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, file_list=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from branch import find_branch b = find_branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') # FIXME: probably doesn't handle non-ascii patterns if os.path.exists(ifn): f = b.controlfile(ifn, 'rt') igns = f.read() f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' f = AtomicFile(ifn, 'wt') f.write(igns) f.commit() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False): ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) class cmd_compare_trees(Command): """Show quick calculation of status.""" hidden = True def run(self): import diff b = Branch('.') delta = diff.compare_trees(b.basis_tree(), b.working_tree()) delta.show(sys.stdout, False) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :467 committer Martin Pool 1115791257 +1000 data 24 - doc for status command from :466 M 644 inline bzrlib/commands.py data 34158 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): b = Branch('.', lock_mode='r') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, file_list=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from branch import find_branch b = find_branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') # FIXME: probably doesn't handle non-ascii patterns if os.path.exists(ifn): f = b.controlfile(ifn, 'rt') igns = f.read() f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' f = AtomicFile(ifn, 'wt') f.write(igns) f.commit() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False): ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) class cmd_compare_trees(Command): """Show quick calculation of status.""" hidden = True def run(self): import diff b = Branch('.') delta = diff.compare_trees(b.basis_tree(), b.working_tree()) delta.show(sys.stdout, False) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :468 committer Martin Pool 1115791528 +1000 data 104 - Interpret arguments to bzr status - Handle giving a non-default branch to bzr status - bzr status docs from :467 M 644 inline NEWS data 7185 bzr-0.0.5 NOT RELEASED YET CHANGES: * ``bzr`` with no command now shows help rather than giving an error. Suggested by Michael Ellerman. * ``bzr status`` output format changed, because svn-style output doesn't really match the model of bzr. Now files are grouped by status and can be shown with their IDs. ``bzr status --all`` shows all versioned files and unknown files but not ignored files. ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New ``bzr ignore PATTERN`` command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. File may be in a branch other than the working directory. * ``bzr log`` and ``bzr root`` can be given an http URL instead of a filename. * Commands can now be defined by external programs or scripts in a directory on $BZRPATH. * New "stat cache" avoids reading the contents of files if they haven't changed since the previous time. * If the Python interpreter is too old, try to find a better one or give an error. Based on a patch from Fredrik Lundh. BUG FIXES: * Fixed diff format so that added and removed files will be handled properly by patch. Fix from Lalo Martins. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. * New developer commands ``added``, ``modified``. PORTABILITY: * Cope on Windows on python2.3 by using the weaker random seed. 2.4 is now only recommended. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 34736 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0], lock_mode='r') file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.', lock_mode='r') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, file_list=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from branch import find_branch b = find_branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') # FIXME: probably doesn't handle non-ascii patterns if os.path.exists(ifn): f = b.controlfile(ifn, 'rt') igns = f.read() f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' f = AtomicFile(ifn, 'wt') f.write(igns) f.commit() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False): ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) class cmd_compare_trees(Command): """Show quick calculation of status.""" hidden = True def run(self): import diff b = Branch('.') delta = diff.compare_trees(b.basis_tree(), b.working_tree()) delta.show(sys.stdout, False) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :469 committer Martin Pool 1115791663 +1000 data 35 - remove diff_trees from public api from :468 M 644 inline bzrlib/__init__.py data 1734 # (C) 2005 Canonical Development Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr library""" from inventory import Inventory, InventoryEntry from branch import Branch, ScratchBranch from osutils import format_date from tree import Tree from diff import compare_trees from trace import mutter, warning, open_tracefile from log import show_log import add BZRDIR = ".bzr" DEFAULT_IGNORE = ['.bzr.log', '*~', '#*#', '*$', '.#*', '.*.swp', '.*.tmp', '*.tmp', '*.bak', '*.BAK', '*.orig', '*.o', '*.obj', '*.a', '*.py[oc]', '*.so', '*.exe', '*.elc', '{arch}', 'CVS', 'CVS.adm', '.svn', '_darcs', 'SCCS', 'RCS', '*,v', 'BitKeeper', 'TAGS', '.make.state', '.sconsign', '.tmp*', '.del-*'] IGNORE_FILENAME = ".bzrignore" import locale user_encoding = locale.getpreferredencoding() __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __version__ = '0.0.5pre' commit refs/heads/master mark :470 committer Martin Pool 1115791964 +1000 data 129 - remove dead code for cmd_compare_trees - compare_trees new parameter want_unchanged to avoid allocating strings if not needed from :469 M 644 inline bzrlib/commands.py data 34468 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0], lock_mode='r') file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.', lock_mode='r') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, file_list=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information for this branch""" def run(self): import info info.show_info(Branch('.')) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from branch import find_branch b = find_branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') # FIXME: probably doesn't handle non-ascii patterns if os.path.exists(ifn): f = b.controlfile(ifn, 'rt') igns = f.read() f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' f = AtomicFile(ifn, 'wt') f.write(igns) f.commit() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False): ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/diff.py data 12308 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set from trace import mutter from errors import BzrError def diff_trees(old_tree, new_tree): """Compute diff between two trees. They may be in different branches and may be working or historical trees. This only compares the versioned files, paying no attention to files which are ignored or unknown. Those can only be present in working trees and can be reported on separately. Yields a sequence of (state, id, old_name, new_name, kind). Each filename and each id is listed only once. """ ## TODO: Allow specifying a list of files to compare, rather than ## doing the whole tree? (Not urgent.) ## TODO: Allow diffing any two inventories, not just the ## current one against one. We mgiht need to specify two ## stores to look for the files if diffing two branches. That ## might imply this shouldn't be primarily a Branch method. sha_match_cnt = modified_cnt = 0 old_it = old_tree.list_files() new_it = new_tree.list_files() def next(it): try: return it.next() except StopIteration: return None old_item = next(old_it) new_item = next(new_it) # We step through the two sorted iterators in parallel, trying to # keep them lined up. while (old_item != None) or (new_item != None): # OK, we still have some remaining on both, but they may be # out of step. if old_item != None: old_name, old_class, old_kind, old_id = old_item else: old_name = None if new_item != None: new_name, new_class, new_kind, new_id = new_item else: new_name = None if old_item: # can't handle the old tree being a WorkingTree assert old_class == 'V' if new_item and (new_class != 'V'): yield new_class, None, None, new_name, new_kind new_item = next(new_it) elif (not new_item) or (old_item and (old_name < new_name)): if new_tree.has_id(old_id): # will be mentioned as renamed under new name pass else: yield 'D', old_id, old_name, None, old_kind old_item = next(old_it) elif (not old_item) or (new_item and (new_name < old_name)): if old_tree.has_id(new_id): yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind else: yield 'A', new_id, None, new_name, new_kind new_item = next(new_it) elif old_id != new_id: assert old_name == new_name # both trees have a file of this name, but it is not the # same file. in other words, the old filename has been # overwritten by either a newly-added or a renamed file. # (should we return something about the overwritten file?) if old_tree.has_id(new_id): # renaming, overlying a deleted file yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind else: yield 'A', new_id, None, new_name, new_kind new_item = next(new_it) old_item = next(old_it) else: assert old_id == new_id assert old_id != None assert old_name == new_name assert old_kind == new_kind if old_kind == 'directory': yield '.', new_id, old_name, new_name, new_kind elif old_tree.get_file_sha1(old_id) == new_tree.get_file_sha1(old_id): sha_match_cnt += 1 yield '.', new_id, old_name, new_name, new_kind else: modified_cnt += 1 yield 'M', new_id, old_name, new_name, new_kind new_item = next(new_it) old_item = next(old_it) mutter("diff finished: %d SHA matches, %d modified" % (sha_match_cnt, modified_cnt)) def show_diff(b, revision, file_list): import difflib, sys, types if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. if file_list: file_list = [b.relpath(f) for f in file_list] # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in diff_trees(old_tree, new_tree): if file_list and (new_name not in file_list): continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') sys.stdout.writelines(ud) if nonl: print "\\ No newline at end of file" sys.stdout.write('\n') if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: raise BzrError("can't represent state %s {%s}" % (file_state, fid)) class TreeDelta: """Describes changes from one tree to another. Contains four lists: added (path, id) removed (path, id) renamed (oldpath, newpath, id, text_modified) modified (path, id) unchanged (path, id) Each id is listed only once. Files that are both modified and renamed are listed only in renamed, with the text_modified flag true. The lists are normally sorted when the delta is created. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] self.unchanged = [] def show(self, to_file, show_ids=False, show_unchanged=False): def show_list(files): for path, fid in files: if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if self.removed: print >>to_file, 'removed files:' show_list(self.removed) if self.added: print >>to_file, 'added files:' show_list(self.added) if self.renamed: print >>to_file, 'renamed files:' for oldpath, newpath, fid, text_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if self.modified: print >>to_file, 'modified files:' show_list(self.modified) if show_unchanged and self.unchanged: print >>to_file, 'unchanged files:' show_list(self.unchanged) def compare_trees(old_tree, new_tree, want_unchanged): old_inv = old_tree.inventory new_inv = new_tree.inventory delta = TreeDelta() for file_id in old_tree: if file_id in new_tree: old_path = old_inv.id2path(file_id) new_path = new_inv.id2path(file_id) kind = old_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ 'invalid file kind %r' % kind if kind == 'file': old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False if old_path != new_path: delta.renamed.append((old_path, new_path, file_id, text_modified)) elif text_modified: delta.modified.append((new_path, file_id)) else: delta.unchanged.append((new_path, file_id)) else: delta.removed.append((old_inv.id2path(file_id), file_id)) for file_id in new_inv: if file_id in old_inv: continue delta.added.append((new_inv.id2path(file_id), file_id)) delta.removed.sort() delta.added.sort() delta.renamed.sort() delta.modified.sort() return delta M 644 inline bzrlib/info.py data 3520 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import time from osutils import format_date def _countiter(it): # surely there's a builtin for this? i = 0 for j in it: i += 1 return i def show_info(b): import diff print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl != None: return pl else: return 's' count_version_dirs = 0 basis = b.basis_tree() working = b.working_tree() work_inv = working.inventory delta = diff.compare_trees(basis, working, want_unchanged=True) print print 'in the working tree:' print ' %8s unchanged' % len(delta.unchanged) print ' %8d modified' % len(delta.modified) print ' %8d added' % len(delta.added) print ' %8d removed' % len(delta.removed) print ' %8d renamed' % len(delta.renamed) ignore_cnt = unknown_cnt = 0 for path in working.extras(): if working.is_ignored(path): ignore_cnt += 1 else: unknown_cnt += 1 print ' %8d unknown' % unknown_cnt print ' %8d ignored' % ignore_cnt dir_cnt = 0 for file_id in work_inv: if work_inv.get_file_kind(file_id) == 'directory': dir_cnt += 1 print ' %8d versioned %s' \ % (dir_cnt, plural(dir_cnt, 'subdirectory', 'subdirectories')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %8d revision%s' % (revno, plural(revno)) committers = Set() for rev in history: committers.add(b.get_revision(rev).committer) print ' %8d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %8d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) print print 'text store:' c, t = b.text_store.total_size() print ' %8d file texts' % c print ' %8d kB' % (t/1024) print print 'revision store:' c, t = b.revision_store.total_size() print ' %8d revisions' % c print ' %8d kB' % (t/1024) print print 'inventory store:' c, t = b.inventory_store.total_size() print ' %8d inventories' % c print ' %8d kB' % (t/1024) M 644 inline bzrlib/log.py data 5076 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def find_touching_revisions(branch, file_id): """Yield a description of revisions which affect the file_id. Each returned element is (revno, revision_id, description) This is the list of revisions where the file is either added, modified, renamed or deleted. Revisions are returned in chronological order. TODO: Perhaps some way to limit this to only particular revisions, or to traverse a non-branch set of revisions? TODO: If a directory is given, then by default look for all changes under that directory. """ last_ie = None last_path = None revno = 1 for revision_id in branch.revision_history(): this_inv = branch.get_revision_inventory(revision_id) if file_id in this_inv: this_ie = this_inv[file_id] this_path = this_inv.id2path(file_id) else: this_ie = this_path = None # now we know how it was last time, and how it is in this revision. # are those two states effectively the same or not? if not this_ie and not last_ie: # not present in either pass elif this_ie and not last_ie: yield revno, revision_id, "added " + this_path elif not this_ie and last_ie: # deleted here yield revno, revision_id, "deleted " + last_path elif this_path != last_path: yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path)) elif (this_ie.text_size != last_ie.text_size or this_ie.text_sha1 != last_ie.text_sha1): yield revno, revision_id, "modified " + this_path last_ie = this_ie last_path = this_path revno += 1 def show_log(branch, filename=None, show_timezone='original', verbose=False, show_ids=False, to_file=None): """Write out human-readable log of commits to this branch. filename If true, list only the commits affecting the specified file, rather than all commits. show_timezone 'original' (committer's timezone), 'utc' (universal time), or 'local' (local user's timezone) verbose If true show added/changed/deleted/renamed files. show_ids If true, show revision and file ids. to_file File to send log to; by default stdout. """ from osutils import format_date from errors import BzrCheckError from diff import compare_trees from textui import show_status if to_file == None: import sys to_file = sys.stdout if filename: file_id = branch.read_working_inventory().path2id(filename) def which_revs(): for revno, revid, why in find_touching_revisions(branch, file_id): yield revno, revid else: def which_revs(): for i, revid in enumerate(branch.revision_history()): yield i+1, revid branch._need_readlock() precursor = None if verbose: from tree import EmptyTree prev_tree = EmptyTree() for revno, revision_id in which_revs(): print >>to_file, '-' * 60 print >>to_file, 'revno:', revno rev = branch.get_revision(revision_id) if show_ids: print >>to_file, 'revision-id:', revision_id print >>to_file, 'committer:', rev.committer print >>to_file, 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) if revision_id != rev.revision_id: raise BzrCheckError("retrieved wrong revision: %r" % (revision_id, rev.revision_id)) print >>to_file, 'message:' if not rev.message: print >>to_file, ' (no message)' else: for l in rev.message.split('\n'): print >>to_file, ' ' + l # Don't show a list of changed files if we were asked about # one specific file. if verbose: this_tree = branch.revision_tree(revision_id) delta = compare_trees(prev_tree, this_tree, want_unchanged=False) delta.show(to_file, show_ids) prev_tree = this_tree precursor = revision_id M 644 inline bzrlib/status.py data 1698 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def show_status(branch, show_unchanged=False, file_list=None, show_ids=False): """Display single-line status for non-ignored working files. show_all If true, show unmodified files too. file_list If set, only show the status of files in this list. """ import sys import diff branch._need_readlock() old = branch.basis_tree() new = branch.working_tree() if file_list: raise NotImplementedError("sorry, status on selected files is not implemented " "at the moment") delta = diff.compare_trees(old, new, want_unchanged=show_unchanged) delta.show(sys.stdout, show_ids=show_ids, show_unchanged=show_unchanged) unknowns = new.unknowns() done_header = False for path in unknowns: if not done_header: print 'unknown files:' done_header = True print ' ', path commit refs/heads/master mark :471 committer Martin Pool 1115792094 +1000 data 58 - actually avoid reporting unchanged files if not required from :470 M 644 inline bzrlib/diff.py data 12571 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set from trace import mutter from errors import BzrError def diff_trees(old_tree, new_tree): """Compute diff between two trees. They may be in different branches and may be working or historical trees. This only compares the versioned files, paying no attention to files which are ignored or unknown. Those can only be present in working trees and can be reported on separately. Yields a sequence of (state, id, old_name, new_name, kind). Each filename and each id is listed only once. """ ## TODO: Allow specifying a list of files to compare, rather than ## doing the whole tree? (Not urgent.) ## TODO: Allow diffing any two inventories, not just the ## current one against one. We mgiht need to specify two ## stores to look for the files if diffing two branches. That ## might imply this shouldn't be primarily a Branch method. sha_match_cnt = modified_cnt = 0 old_it = old_tree.list_files() new_it = new_tree.list_files() def next(it): try: return it.next() except StopIteration: return None old_item = next(old_it) new_item = next(new_it) # We step through the two sorted iterators in parallel, trying to # keep them lined up. while (old_item != None) or (new_item != None): # OK, we still have some remaining on both, but they may be # out of step. if old_item != None: old_name, old_class, old_kind, old_id = old_item else: old_name = None if new_item != None: new_name, new_class, new_kind, new_id = new_item else: new_name = None if old_item: # can't handle the old tree being a WorkingTree assert old_class == 'V' if new_item and (new_class != 'V'): yield new_class, None, None, new_name, new_kind new_item = next(new_it) elif (not new_item) or (old_item and (old_name < new_name)): if new_tree.has_id(old_id): # will be mentioned as renamed under new name pass else: yield 'D', old_id, old_name, None, old_kind old_item = next(old_it) elif (not old_item) or (new_item and (new_name < old_name)): if old_tree.has_id(new_id): yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind else: yield 'A', new_id, None, new_name, new_kind new_item = next(new_it) elif old_id != new_id: assert old_name == new_name # both trees have a file of this name, but it is not the # same file. in other words, the old filename has been # overwritten by either a newly-added or a renamed file. # (should we return something about the overwritten file?) if old_tree.has_id(new_id): # renaming, overlying a deleted file yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind else: yield 'A', new_id, None, new_name, new_kind new_item = next(new_it) old_item = next(old_it) else: assert old_id == new_id assert old_id != None assert old_name == new_name assert old_kind == new_kind if old_kind == 'directory': yield '.', new_id, old_name, new_name, new_kind elif old_tree.get_file_sha1(old_id) == new_tree.get_file_sha1(old_id): sha_match_cnt += 1 yield '.', new_id, old_name, new_name, new_kind else: modified_cnt += 1 yield 'M', new_id, old_name, new_name, new_kind new_item = next(new_it) old_item = next(old_it) mutter("diff finished: %d SHA matches, %d modified" % (sha_match_cnt, modified_cnt)) def show_diff(b, revision, file_list): import difflib, sys, types if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. if file_list: file_list = [b.relpath(f) for f in file_list] # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in diff_trees(old_tree, new_tree): if file_list and (new_name not in file_list): continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') sys.stdout.writelines(ud) if nonl: print "\\ No newline at end of file" sys.stdout.write('\n') if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: raise BzrError("can't represent state %s {%s}" % (file_state, fid)) class TreeDelta: """Describes changes from one tree to another. Contains four lists: added (path, id) removed (path, id) renamed (oldpath, newpath, id, text_modified) modified (path, id) unchanged (path, id) Each id is listed only once. Files that are both modified and renamed are listed only in renamed, with the text_modified flag true. The lists are normally sorted when the delta is created. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] self.unchanged = [] def show(self, to_file, show_ids=False, show_unchanged=False): def show_list(files): for path, fid in files: if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if self.removed: print >>to_file, 'removed files:' show_list(self.removed) if self.added: print >>to_file, 'added files:' show_list(self.added) if self.renamed: print >>to_file, 'renamed files:' for oldpath, newpath, fid, text_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if self.modified: print >>to_file, 'modified files:' show_list(self.modified) if show_unchanged and self.unchanged: print >>to_file, 'unchanged files:' show_list(self.unchanged) def compare_trees(old_tree, new_tree, want_unchanged): old_inv = old_tree.inventory new_inv = new_tree.inventory delta = TreeDelta() for file_id in old_tree: if file_id in new_tree: old_path = old_inv.id2path(file_id) new_path = new_inv.id2path(file_id) kind = old_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ 'invalid file kind %r' % kind if kind == 'file': old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False # TODO: Can possibly avoid calculating path strings if the # two files are unchanged and their names and parents are # the same and the parents are unchanged all the way up. # May not be worthwhile. if old_path != new_path: delta.renamed.append((old_path, new_path, file_id, text_modified)) elif text_modified: delta.modified.append((new_path, file_id)) elif want_unchanged: delta.unchanged.append((new_path, file_id)) else: delta.removed.append((old_inv.id2path(file_id), file_id)) for file_id in new_inv: if file_id in old_inv: continue delta.added.append((new_inv.id2path(file_id), file_id)) delta.removed.sort() delta.added.sort() delta.renamed.sort() delta.modified.sort() return delta commit refs/heads/master mark :472 committer Martin Pool 1115792325 +1000 data 43 - Optional branch parameter to info command from :471 M 644 inline NEWS data 7238 bzr-0.0.5 NOT RELEASED YET CHANGES: * ``bzr`` with no command now shows help rather than giving an error. Suggested by Michael Ellerman. * ``bzr status`` output format changed, because svn-style output doesn't really match the model of bzr. Now files are grouped by status and can be shown with their IDs. ``bzr status --all`` shows all versioned files and unknown files but not ignored files. ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New ``bzr ignore PATTERN`` command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. File may be in a branch other than the working directory. * ``bzr log`` and ``bzr root`` can be given an http URL instead of a filename. * Commands can now be defined by external programs or scripts in a directory on $BZRPATH. * New "stat cache" avoids reading the contents of files if they haven't changed since the previous time. * If the Python interpreter is too old, try to find a better one or give an error. Based on a patch from Fredrik Lundh. * New optional parameter ``bzr info [BRANCH]``. BUG FIXES: * Fixed diff format so that added and removed files will be handled properly by patch. Fix from Lalo Martins. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. * New developer commands ``added``, ``modified``. PORTABILITY: * Cope on Windows on python2.3 by using the weaker random seed. 2.4 is now only recommended. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 34569 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0], lock_mode='r') file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.', lock_mode='r') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, file_list=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from branch import find_branch b = find_branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') # FIXME: probably doesn't handle non-ascii patterns if os.path.exists(ifn): f = b.controlfile(ifn, 'rt') igns = f.read() f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' f = AtomicFile(ifn, 'wt') f.write(igns) f.commit() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False): ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :473 committer Martin Pool 1115792405 +1000 data 40 - Don't lose first line of command help! from :472 M 644 inline bzrlib/help.py data 4119 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA global_help = \ """Bazaar-NG -- a free distributed version-control tool http://bazaar-ng.org/ **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** * Metadata format is not stable yet -- you may need to discard history in the future. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. To make a branch, use 'bzr init' in an existing directory, then 'bzr add' to make files versioned. 'bzr add .' will recursively add all non-ignored files. 'bzr status' describes files that are unknown, ignored, or modified. 'bzr diff' shows the text changes to the tree or named files. 'bzr commit -m ' commits all changes in that branch. 'bzr move' and 'bzr rename' allow you to rename files or directories. 'bzr remove' makes a file unversioned but keeps the working copy; to delete that too simply delete the file. 'bzr log' shows a history of changes, and 'bzr info' gives summary statistical information. 'bzr check' validates all files are stored safely. Files can be ignored by giving a path or a glob in .bzrignore at the top of the tree. Use 'bzr ignored' to see what files are ignored and why, and 'bzr unknowns' to see files that are neither versioned or ignored. For more help on any command, type 'bzr help COMMAND', or 'bzr help commands' for a list. """ def help(topic=None): if topic == None: print global_help elif topic == 'commands': help_commands() else: help_on_command(topic) def command_usage(cmdname, cmdclass): """Return single-line grammar for command. Only describes arguments, not options. """ s = cmdname + ' ' for aname in cmdclass.takes_args: aname = aname.upper() if aname[-1] in ['$', '+']: aname = aname[:-1] + '...' elif aname[-1] == '?': aname = '[' + aname[:-1] + ']' elif aname[-1] == '*': aname = '[' + aname[:-1] + '...]' s += aname + ' ' assert s[-1] == ' ' s = s[:-1] return s def help_on_command(cmdname): cmdname = str(cmdname) from inspect import getdoc import commands topic, cmdclass = commands.get_cmd_class(cmdname) doc = getdoc(cmdclass) if doc == None: raise NotImplementedError("sorry, no detailed help yet for %r" % cmdname) print 'usage:', command_usage(topic, cmdclass) if cmdclass.aliases: print 'aliases: ' + ', '.join(cmdclass.aliases) print doc help_on_option(cmdclass.takes_options) def help_on_option(options): import commands if not options: return print print 'options:' for on in options: l = ' --' + on for shortname, longname in commands.SHORT_OPTIONS.items(): if longname == on: l += ', -' + shortname break print l def help_commands(): """List all commands""" import inspect import commands accu = [] for cmdname, cmdclass in commands.get_all_cmds(): accu.append((cmdname, cmdclass)) accu.sort() for cmdname, cmdclass in accu: if cmdclass.hidden: continue print command_usage(cmdname, cmdclass) help = inspect.getdoc(cmdclass) if help: print " " + help.split('\n', 1)[0] commit refs/heads/master mark :474 committer Martin Pool 1115795857 +1000 data 22 - sort unchanged files from :473 M 644 inline bzrlib/diff.py data 12598 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set from trace import mutter from errors import BzrError def diff_trees(old_tree, new_tree): """Compute diff between two trees. They may be in different branches and may be working or historical trees. This only compares the versioned files, paying no attention to files which are ignored or unknown. Those can only be present in working trees and can be reported on separately. Yields a sequence of (state, id, old_name, new_name, kind). Each filename and each id is listed only once. """ ## TODO: Allow specifying a list of files to compare, rather than ## doing the whole tree? (Not urgent.) ## TODO: Allow diffing any two inventories, not just the ## current one against one. We mgiht need to specify two ## stores to look for the files if diffing two branches. That ## might imply this shouldn't be primarily a Branch method. sha_match_cnt = modified_cnt = 0 old_it = old_tree.list_files() new_it = new_tree.list_files() def next(it): try: return it.next() except StopIteration: return None old_item = next(old_it) new_item = next(new_it) # We step through the two sorted iterators in parallel, trying to # keep them lined up. while (old_item != None) or (new_item != None): # OK, we still have some remaining on both, but they may be # out of step. if old_item != None: old_name, old_class, old_kind, old_id = old_item else: old_name = None if new_item != None: new_name, new_class, new_kind, new_id = new_item else: new_name = None if old_item: # can't handle the old tree being a WorkingTree assert old_class == 'V' if new_item and (new_class != 'V'): yield new_class, None, None, new_name, new_kind new_item = next(new_it) elif (not new_item) or (old_item and (old_name < new_name)): if new_tree.has_id(old_id): # will be mentioned as renamed under new name pass else: yield 'D', old_id, old_name, None, old_kind old_item = next(old_it) elif (not old_item) or (new_item and (new_name < old_name)): if old_tree.has_id(new_id): yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind else: yield 'A', new_id, None, new_name, new_kind new_item = next(new_it) elif old_id != new_id: assert old_name == new_name # both trees have a file of this name, but it is not the # same file. in other words, the old filename has been # overwritten by either a newly-added or a renamed file. # (should we return something about the overwritten file?) if old_tree.has_id(new_id): # renaming, overlying a deleted file yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind else: yield 'A', new_id, None, new_name, new_kind new_item = next(new_it) old_item = next(old_it) else: assert old_id == new_id assert old_id != None assert old_name == new_name assert old_kind == new_kind if old_kind == 'directory': yield '.', new_id, old_name, new_name, new_kind elif old_tree.get_file_sha1(old_id) == new_tree.get_file_sha1(old_id): sha_match_cnt += 1 yield '.', new_id, old_name, new_name, new_kind else: modified_cnt += 1 yield 'M', new_id, old_name, new_name, new_kind new_item = next(new_it) old_item = next(old_it) mutter("diff finished: %d SHA matches, %d modified" % (sha_match_cnt, modified_cnt)) def show_diff(b, revision, file_list): import difflib, sys, types if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. # TODO: Better to return them in sorted order I think. if file_list: file_list = [b.relpath(f) for f in file_list] # FIXME: If given a file list, compare only those files rather # than comparing everything and then throwing stuff away. for file_state, fid, old_name, new_name, kind in diff_trees(old_tree, new_tree): if file_list and (new_name not in file_list): continue # Don't show this by default; maybe do it if an option is passed # idlabel = ' {%s}' % fid idlabel = '' def diffit(oldlines, newlines, **kw): # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') sys.stdout.writelines(ud) if nonl: print "\\ No newline at end of file" sys.stdout.write('\n') if file_state in ['.', '?', 'I']: continue elif file_state == 'A': print '*** added %s %r' % (kind, new_name) if kind == 'file': diffit([], new_tree.get_file(fid).readlines(), fromfile=DEVNULL, tofile=new_label + new_name + idlabel) elif file_state == 'D': assert isinstance(old_name, types.StringTypes) print '*** deleted %s %r' % (kind, old_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), [], fromfile=old_label + old_name + idlabel, tofile=DEVNULL) elif file_state in ['M', 'R']: if file_state == 'M': assert kind == 'file' assert old_name == new_name print '*** modified %s %r' % (kind, new_name) elif file_state == 'R': print '*** renamed %s %r => %r' % (kind, old_name, new_name) if kind == 'file': diffit(old_tree.get_file(fid).readlines(), new_tree.get_file(fid).readlines(), fromfile=old_label + old_name + idlabel, tofile=new_label + new_name) else: raise BzrError("can't represent state %s {%s}" % (file_state, fid)) class TreeDelta: """Describes changes from one tree to another. Contains four lists: added (path, id) removed (path, id) renamed (oldpath, newpath, id, text_modified) modified (path, id) unchanged (path, id) Each id is listed only once. Files that are both modified and renamed are listed only in renamed, with the text_modified flag true. The lists are normally sorted when the delta is created. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] self.unchanged = [] def show(self, to_file, show_ids=False, show_unchanged=False): def show_list(files): for path, fid in files: if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if self.removed: print >>to_file, 'removed files:' show_list(self.removed) if self.added: print >>to_file, 'added files:' show_list(self.added) if self.renamed: print >>to_file, 'renamed files:' for oldpath, newpath, fid, text_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if self.modified: print >>to_file, 'modified files:' show_list(self.modified) if show_unchanged and self.unchanged: print >>to_file, 'unchanged files:' show_list(self.unchanged) def compare_trees(old_tree, new_tree, want_unchanged): old_inv = old_tree.inventory new_inv = new_tree.inventory delta = TreeDelta() for file_id in old_tree: if file_id in new_tree: old_path = old_inv.id2path(file_id) new_path = new_inv.id2path(file_id) kind = old_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ 'invalid file kind %r' % kind if kind == 'file': old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False # TODO: Can possibly avoid calculating path strings if the # two files are unchanged and their names and parents are # the same and the parents are unchanged all the way up. # May not be worthwhile. if old_path != new_path: delta.renamed.append((old_path, new_path, file_id, text_modified)) elif text_modified: delta.modified.append((new_path, file_id)) elif want_unchanged: delta.unchanged.append((new_path, file_id)) else: delta.removed.append((old_inv.id2path(file_id), file_id)) for file_id in new_inv: if file_id in old_inv: continue delta.added.append((new_inv.id2path(file_id), file_id)) delta.removed.sort() delta.added.sort() delta.renamed.sort() delta.modified.sort() delta.unchanged.sort() return delta commit refs/heads/master mark :475 committer Martin Pool 1115797556 +1000 data 71 - rewrite diff using compare_trees() - include entry kind in TreeDelta from :474 M 644 inline bzrlib/branch.py data 34260 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. base Base directory of the branch. """ _lockmode = None def __init__(self, base, init=False, find_root=True, lock_mode='w'): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.lock(lock_mode) self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def lock(self, mode='w'): """Lock the on-disk branch, excluding other processes.""" try: import fcntl, errno if mode == 'w': lm = fcntl.LOCK_EX om = os.O_WRONLY | os.O_CREAT elif mode == 'r': lm = fcntl.LOCK_SH om = os.O_RDONLY else: raise BzrError("invalid locking mode %r" % mode) try: lockfile = os.open(self.controlfilename('branch-lock'), om) except OSError, e: if e.errno == errno.ENOENT: # might not exist on branches from <0.0.4 self.controlfile('branch-lock', 'w').close() lockfile = os.open(self.controlfilename('branch-lock'), om) else: raise e fcntl.lockf(lockfile, lm) def unlock(): fcntl.lockf(lockfile, fcntl.LOCK_UN) os.close(lockfile) self._lockmode = None self.unlock = unlock self._lockmode = mode except ImportError: warning("please write a locking method for platform %r" % sys.platform) def unlock(): self._lockmode = None self.unlock = unlock self._lockmode = mode def _need_readlock(self): if self._lockmode not in ['r', 'w']: raise BzrError('need read lock on branch, only have %r' % self._lockmode) def _need_writelock(self): if self._lockmode not in ['w']: raise BzrError('need write lock on branch, only have %r' % self._lockmode) def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" self._need_readlock() before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self._need_writelock() ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. >>> b = ScratchBranch(files=['foo']) >>> 'foo' in b.unknowns() True >>> b.show_status() ? foo >>> b.add('foo') >>> 'foo' in b.unknowns() False >>> bool(b.inventory.path2id('foo')) True >>> b.show_status() A foo >>> b.add('foo') Traceback (most recent call last): ... BzrError: ('foo is already versioned', []) >>> b.add(['nothere']) Traceback (most recent call last): BzrError: ('cannot add: not a regular file or directory: nothere', []) """ self._need_writelock() # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" self._need_readlock() tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.inventory.has_filename('foo') True >>> b.remove('foo') >>> b.working_tree().has_filename('foo') True >>> b.inventory.has_filename('foo') False >>> b = ScratchBranch(files=['foo']) >>> b.add('foo') >>> b.commit('one') >>> b.remove('foo') >>> b.commit('two') >>> b.inventory.has_filename('foo') False >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability self._need_writelock() if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def commit(self, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. """ self._need_writelock() ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = self.read_working_inventory() inv = Inventory() basis = self.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = self.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: bailout("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) self.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itself. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) self.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) self._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = self.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) self.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (self.revno() + 1)) self.append_revision(rev_id) if verbose: note("commited r%d" % self.revno()) def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" self._need_readlock() r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" self._need_readlock() i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" self._need_readlock() if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self._need_readlock() return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise BzrError('invalid history direction %r' % direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. >>> b = ScratchBranch() >>> b.revno() 0 >>> b.commit('no foo') >>> b.revno() 1 """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. >>> ScratchBranch().last_patch() == None True """ ph = self.revision_history() if ph: return ph[-1] else: return None def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" self._need_readlock() if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. >>> b = ScratchBranch(files=['foo']) >>> b.basis_tree().has_filename('foo') False >>> b.working_tree().has_filename('foo') True >>> b.add('foo') >>> b.commit('add foo') >>> b.basis_tree().has_filename('foo') True """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self._need_writelock() tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self._need_writelock() ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def _gen_revision_id(when): """Return new revision-id.""" s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/diff.py data 12368 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set from trace import mutter from errors import BzrError def diff_trees(old_tree, new_tree): """Compute diff between two trees. They may be in different branches and may be working or historical trees. This only compares the versioned files, paying no attention to files which are ignored or unknown. Those can only be present in working trees and can be reported on separately. Yields a sequence of (state, id, old_name, new_name, kind). Each filename and each id is listed only once. """ ## TODO: Allow specifying a list of files to compare, rather than ## doing the whole tree? (Not urgent.) ## TODO: Allow diffing any two inventories, not just the ## current one against one. We mgiht need to specify two ## stores to look for the files if diffing two branches. That ## might imply this shouldn't be primarily a Branch method. sha_match_cnt = modified_cnt = 0 old_it = old_tree.list_files() new_it = new_tree.list_files() def next(it): try: return it.next() except StopIteration: return None old_item = next(old_it) new_item = next(new_it) # We step through the two sorted iterators in parallel, trying to # keep them lined up. while (old_item != None) or (new_item != None): # OK, we still have some remaining on both, but they may be # out of step. if old_item != None: old_name, old_class, old_kind, old_id = old_item else: old_name = None if new_item != None: new_name, new_class, new_kind, new_id = new_item else: new_name = None if old_item: # can't handle the old tree being a WorkingTree assert old_class == 'V' if new_item and (new_class != 'V'): yield new_class, None, None, new_name, new_kind new_item = next(new_it) elif (not new_item) or (old_item and (old_name < new_name)): if new_tree.has_id(old_id): # will be mentioned as renamed under new name pass else: yield 'D', old_id, old_name, None, old_kind old_item = next(old_it) elif (not old_item) or (new_item and (new_name < old_name)): if old_tree.has_id(new_id): yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind else: yield 'A', new_id, None, new_name, new_kind new_item = next(new_it) elif old_id != new_id: assert old_name == new_name # both trees have a file of this name, but it is not the # same file. in other words, the old filename has been # overwritten by either a newly-added or a renamed file. # (should we return something about the overwritten file?) if old_tree.has_id(new_id): # renaming, overlying a deleted file yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind else: yield 'A', new_id, None, new_name, new_kind new_item = next(new_it) old_item = next(old_it) else: assert old_id == new_id assert old_id != None assert old_name == new_name assert old_kind == new_kind if old_kind == 'directory': yield '.', new_id, old_name, new_name, new_kind elif old_tree.get_file_sha1(old_id) == new_tree.get_file_sha1(old_id): sha_match_cnt += 1 yield '.', new_id, old_name, new_name, new_kind else: modified_cnt += 1 yield 'M', new_id, old_name, new_name, new_kind new_item = next(new_it) old_item = next(old_it) mutter("diff finished: %d SHA matches, %d modified" % (sha_match_cnt, modified_cnt)) def _diff_one(oldlines, newlines, to_file, **kw): import difflib # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') to_file.writelines(ud) if nonl: print >>to_file, "\\ No newline at end of file" print >>to_file def show_diff(b, revision, file_list): import sys if file_list: raise NotImplementedError('diff on restricted files broken at the moment') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. delta = compare_trees(old_tree, new_tree, want_unchanged=False) for path, file_id, kind in delta.removed: print '*** removed %s %r' % (kind, path) if kind == 'file': _diff_one(old_tree.get_file(file_id).readlines(), [], sys.stdout, fromfile=old_label + path, tofile=DEVNULL) for path, file_id, kind in delta.added: print '*** added %s %r' % (kind, path) if kind == 'file': _diff_one([], new_tree.get_file(file_id).readlines(), sys.stdout, fromfile=DEVNULL, tofile=new_label + path) for old_path, new_path, file_id, kind, text_modified in delta.renamed: print '*** renamed %s %r => %r' % (kind, old_path, new_path) if text_modified: _diff_one(old_tree.get_file(file_id).readlines(), new_tree.get_file(file_id).readlines(), sys.stdout, fromfile=old_label + old_path, tofile=new_label + new_path) for path, file_id, kind in delta.modified: print '*** modified %s %r' % (kind, path) if kind == 'file': _diff_one(old_tree.get_file(file_id).readlines(), new_tree.get_file(file_id).readlines(), sys.stdout, fromfile=old_label + path, tofile=new_label + path) class TreeDelta: """Describes changes from one tree to another. Contains four lists: added (path, id, kind) removed (path, id, kind) renamed (oldpath, newpath, id, kind, text_modified) modified (path, id, kind) unchanged (path, id, kind) Each id is listed only once. Files that are both modified and renamed are listed only in renamed, with the text_modified flag true. The lists are normally sorted when the delta is created. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] self.unchanged = [] def show(self, to_file, show_ids=False, show_unchanged=False): def show_list(files): for path, fid, kind in files: if kind == 'directory': path += '/' elif kind == 'symlink': path += '@' if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if self.removed: print >>to_file, 'removed:' show_list(self.removed) if self.added: print >>to_file, 'added:' show_list(self.added) if self.renamed: print >>to_file, 'renamed:' for oldpath, newpath, fid, kind, text_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if self.modified: print >>to_file, 'modified:' show_list(self.modified) if show_unchanged and self.unchanged: print >>to_file, 'unchanged:' show_list(self.unchanged) def compare_trees(old_tree, new_tree, want_unchanged): old_inv = old_tree.inventory new_inv = new_tree.inventory delta = TreeDelta() mutter('start compare_trees') for file_id in old_tree: if file_id in new_tree: old_path = old_inv.id2path(file_id) new_path = new_inv.id2path(file_id) kind = old_inv.get_file_kind(file_id) assert kind == new_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ 'invalid file kind %r' % kind if kind == 'file': old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False # TODO: Can possibly avoid calculating path strings if the # two files are unchanged and their names and parents are # the same and the parents are unchanged all the way up. # May not be worthwhile. if old_path != new_path: delta.renamed.append((old_path, new_path, file_id, kind, text_modified)) elif text_modified: delta.modified.append((new_path, file_id, kind)) elif want_unchanged: delta.unchanged.append((new_path, file_id, kind)) else: delta.removed.append((old_inv.id2path(file_id), file_id, kind)) mutter('start looking for new files') for file_id in new_inv: if file_id in old_inv: continue kind = new_inv.get_file_kind(file_id) delta.added.append((new_inv.id2path(file_id), file_id, kind)) delta.removed.sort() delta.added.sort() delta.renamed.sort() delta.modified.sort() delta.unchanged.sort() return delta M 644 inline bzrlib/tree.py data 7857 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tree classes, representing directory at point in time. """ from sets import Set import os.path, os, fnmatch from osutils import pumpfile, filesize, quotefn, sha_file, \ joinpath, splitpath, appendpath, isdir, isfile, file_kind, fingerprint_file import errno from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE from inventory import Inventory from trace import mutter, note from errors import bailout import branch import bzrlib class Tree: """Abstract file tree. There are several subclasses: * `WorkingTree` exists as files on disk editable by the user. * `RevisionTree` is a tree as recorded at some point in the past. * `EmptyTree` Trees contain an `Inventory` object, and also know how to retrieve file texts mentioned in the inventory, either from a working directory or from a store. It is possible for trees to contain files that are not described in their inventory or vice versa; for this use `filenames()`. Trees can be compared, etc, regardless of whether they are working trees or versioned trees. """ def has_filename(self, filename): """True if the tree has given filename.""" raise NotImplementedError() def has_id(self, file_id): return self.inventory.has_id(file_id) __contains__ = has_id def id_set(self): """Return set of all ids in this tree.""" return self.inventory.id_set() def __iter__(self): return iter(self.inventory) def id2path(self, file_id): return self.inventory.id2path(file_id) def _get_inventory(self): return self._inventory inventory = property(_get_inventory, doc="Inventory of this Tree") def _check_retrieved(self, ie, f): fp = fingerprint_file(f) f.seek(0) if ie.text_size != None: if ie.text_size != fp['size']: bailout("mismatched size for file %r in %r" % (ie.file_id, self._store), ["inventory expects %d bytes" % ie.text_size, "file is actually %d bytes" % fp['size'], "store is probably damaged/corrupt"]) if ie.text_sha1 != fp['sha1']: bailout("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store), ["inventory expects %s" % ie.text_sha1, "file is actually %s" % fp['sha1'], "store is probably damaged/corrupt"]) def print_file(self, fileid): """Print file with id `fileid` to stdout.""" import sys pumpfile(self.get_file(fileid), sys.stdout) def export(self, dest): """Export this tree to a new directory. `dest` should not exist, and will be created holding the contents of this tree. TODO: To handle subdirectories we need to create the directories first. :note: If the export fails, the destination directory will be left in a half-assed state. """ os.mkdir(dest) mutter('export version %r' % self) inv = self.inventory for dp, ie in inv.iter_entries(): kind = ie.kind fullpath = appendpath(dest, dp) if kind == 'directory': os.mkdir(fullpath) elif kind == 'file': pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb')) else: bailout("don't know how to export {%s} of kind %r" % (ie.file_id, kind)) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) class RevisionTree(Tree): """Tree viewing a previous revision. File text can be retrieved from the text store. TODO: Some kind of `__repr__` method, but a good one probably means knowing the branch and revision number, or at least passing a description to the constructor. """ def __init__(self, store, inv): self._store = store self._inventory = inv def get_file(self, file_id): ie = self._inventory[file_id] f = self._store[ie.text_id] mutter(" get fileid{%s} from %r" % (file_id, self)) self._check_retrieved(ie, f) return f def get_file_size(self, file_id): return self._inventory[file_id].text_size def get_file_sha1(self, file_id): ie = self._inventory[file_id] return ie.text_sha1 def has_filename(self, filename): return bool(self.inventory.path2id(filename)) def list_files(self): # The only files returned by this are those from the version for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id class EmptyTree(Tree): def __init__(self): self._inventory = Inventory() def has_filename(self, filename): return False def list_files(self): if False: # just to make it a generator yield None ###################################################################### # diff # TODO: Merge these two functions into a single one that can operate # on either a whole tree or a set of files. # TODO: Return the diff in order by filename, not by category or in # random order. Can probably be done by lock-stepping through the # filenames from both trees. def file_status(filename, old_tree, new_tree): """Return single-letter status, old and new names for a file. The complexity here is in deciding how to represent renames; many complex cases are possible. """ old_inv = old_tree.inventory new_inv = new_tree.inventory new_id = new_inv.path2id(filename) old_id = old_inv.path2id(filename) if not new_id and not old_id: # easy: doesn't exist in either; not versioned at all if new_tree.is_ignored(filename): return 'I', None, None else: return '?', None, None elif new_id: # There is now a file of this name, great. pass else: # There is no longer a file of this name, but we can describe # what happened to the file that used to have # this name. There are two possibilities: either it was # deleted entirely, or renamed. assert old_id if new_inv.has_id(old_id): return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id) else: return 'D', old_inv.id2path(old_id), None # if the file_id is new in this revision, it is added if new_id and not old_inv.has_id(new_id): return 'A' # if there used to be a file of this name, but that ID has now # disappeared, it is deleted if old_id and not new_inv.has_id(old_id): return 'D' return 'wtf?' def find_renames(old_inv, new_inv): for file_id in old_inv: if file_id not in new_inv: continue old_name = old_inv.id2path(file_id) new_name = new_inv.id2path(file_id) if old_name != new_name: yield (old_name, new_name) commit refs/heads/master mark :476 committer Martin Pool 1115797606 +1000 data 33 - remove dead diff_trees function from :475 M 644 inline bzrlib/diff.py data 8435 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set from trace import mutter from errors import BzrError def _diff_one(oldlines, newlines, to_file, **kw): import difflib # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') to_file.writelines(ud) if nonl: print >>to_file, "\\ No newline at end of file" print >>to_file def show_diff(b, revision, file_list): import sys if file_list: raise NotImplementedError('diff on restricted files broken at the moment') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. delta = compare_trees(old_tree, new_tree, want_unchanged=False) for path, file_id, kind in delta.removed: print '*** removed %s %r' % (kind, path) if kind == 'file': _diff_one(old_tree.get_file(file_id).readlines(), [], sys.stdout, fromfile=old_label + path, tofile=DEVNULL) for path, file_id, kind in delta.added: print '*** added %s %r' % (kind, path) if kind == 'file': _diff_one([], new_tree.get_file(file_id).readlines(), sys.stdout, fromfile=DEVNULL, tofile=new_label + path) for old_path, new_path, file_id, kind, text_modified in delta.renamed: print '*** renamed %s %r => %r' % (kind, old_path, new_path) if text_modified: _diff_one(old_tree.get_file(file_id).readlines(), new_tree.get_file(file_id).readlines(), sys.stdout, fromfile=old_label + old_path, tofile=new_label + new_path) for path, file_id, kind in delta.modified: print '*** modified %s %r' % (kind, path) if kind == 'file': _diff_one(old_tree.get_file(file_id).readlines(), new_tree.get_file(file_id).readlines(), sys.stdout, fromfile=old_label + path, tofile=new_label + path) class TreeDelta: """Describes changes from one tree to another. Contains four lists: added (path, id, kind) removed (path, id, kind) renamed (oldpath, newpath, id, kind, text_modified) modified (path, id, kind) unchanged (path, id, kind) Each id is listed only once. Files that are both modified and renamed are listed only in renamed, with the text_modified flag true. The lists are normally sorted when the delta is created. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] self.unchanged = [] def show(self, to_file, show_ids=False, show_unchanged=False): def show_list(files): for path, fid, kind in files: if kind == 'directory': path += '/' elif kind == 'symlink': path += '@' if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if self.removed: print >>to_file, 'removed:' show_list(self.removed) if self.added: print >>to_file, 'added:' show_list(self.added) if self.renamed: print >>to_file, 'renamed:' for oldpath, newpath, fid, kind, text_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if self.modified: print >>to_file, 'modified:' show_list(self.modified) if show_unchanged and self.unchanged: print >>to_file, 'unchanged:' show_list(self.unchanged) def compare_trees(old_tree, new_tree, want_unchanged): old_inv = old_tree.inventory new_inv = new_tree.inventory delta = TreeDelta() mutter('start compare_trees') for file_id in old_tree: if file_id in new_tree: old_path = old_inv.id2path(file_id) new_path = new_inv.id2path(file_id) kind = old_inv.get_file_kind(file_id) assert kind == new_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ 'invalid file kind %r' % kind if kind == 'file': old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False # TODO: Can possibly avoid calculating path strings if the # two files are unchanged and their names and parents are # the same and the parents are unchanged all the way up. # May not be worthwhile. if old_path != new_path: delta.renamed.append((old_path, new_path, file_id, kind, text_modified)) elif text_modified: delta.modified.append((new_path, file_id, kind)) elif want_unchanged: delta.unchanged.append((new_path, file_id, kind)) else: delta.removed.append((old_inv.id2path(file_id), file_id, kind)) mutter('start looking for new files') for file_id in new_inv: if file_id in old_inv: continue kind = new_inv.get_file_kind(file_id) delta.added.append((new_inv.id2path(file_id), file_id, kind)) delta.removed.sort() delta.added.sort() delta.renamed.sort() delta.modified.sort() delta.unchanged.sort() return delta commit refs/heads/master mark :477 committer Martin Pool 1115797807 +1000 data 150 - fix header for listing of unknown files - don't report root directory in status listing, since it's never changed - fix up status format in tests from :476 M 644 inline bzrlib/diff.py data 8515 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set from trace import mutter from errors import BzrError def _diff_one(oldlines, newlines, to_file, **kw): import difflib # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') to_file.writelines(ud) if nonl: print >>to_file, "\\ No newline at end of file" print >>to_file def show_diff(b, revision, file_list): import sys if file_list: raise NotImplementedError('diff on restricted files broken at the moment') if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. delta = compare_trees(old_tree, new_tree, want_unchanged=False) for path, file_id, kind in delta.removed: print '*** removed %s %r' % (kind, path) if kind == 'file': _diff_one(old_tree.get_file(file_id).readlines(), [], sys.stdout, fromfile=old_label + path, tofile=DEVNULL) for path, file_id, kind in delta.added: print '*** added %s %r' % (kind, path) if kind == 'file': _diff_one([], new_tree.get_file(file_id).readlines(), sys.stdout, fromfile=DEVNULL, tofile=new_label + path) for old_path, new_path, file_id, kind, text_modified in delta.renamed: print '*** renamed %s %r => %r' % (kind, old_path, new_path) if text_modified: _diff_one(old_tree.get_file(file_id).readlines(), new_tree.get_file(file_id).readlines(), sys.stdout, fromfile=old_label + old_path, tofile=new_label + new_path) for path, file_id, kind in delta.modified: print '*** modified %s %r' % (kind, path) if kind == 'file': _diff_one(old_tree.get_file(file_id).readlines(), new_tree.get_file(file_id).readlines(), sys.stdout, fromfile=old_label + path, tofile=new_label + path) class TreeDelta: """Describes changes from one tree to another. Contains four lists: added (path, id, kind) removed (path, id, kind) renamed (oldpath, newpath, id, kind, text_modified) modified (path, id, kind) unchanged (path, id, kind) Each id is listed only once. Files that are both modified and renamed are listed only in renamed, with the text_modified flag true. The lists are normally sorted when the delta is created. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] self.unchanged = [] def show(self, to_file, show_ids=False, show_unchanged=False): def show_list(files): for path, fid, kind in files: if kind == 'directory': path += '/' elif kind == 'symlink': path += '@' if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if self.removed: print >>to_file, 'removed:' show_list(self.removed) if self.added: print >>to_file, 'added:' show_list(self.added) if self.renamed: print >>to_file, 'renamed:' for oldpath, newpath, fid, kind, text_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if self.modified: print >>to_file, 'modified:' show_list(self.modified) if show_unchanged and self.unchanged: print >>to_file, 'unchanged:' show_list(self.unchanged) def compare_trees(old_tree, new_tree, want_unchanged): old_inv = old_tree.inventory new_inv = new_tree.inventory delta = TreeDelta() mutter('start compare_trees') for file_id in old_tree: if file_id in new_tree: kind = old_inv.get_file_kind(file_id) assert kind == new_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ 'invalid file kind %r' % kind if kind == 'root_directory': continue old_path = old_inv.id2path(file_id) new_path = new_inv.id2path(file_id) if kind == 'file': old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False # TODO: Can possibly avoid calculating path strings if the # two files are unchanged and their names and parents are # the same and the parents are unchanged all the way up. # May not be worthwhile. if old_path != new_path: delta.renamed.append((old_path, new_path, file_id, kind, text_modified)) elif text_modified: delta.modified.append((new_path, file_id, kind)) elif want_unchanged: delta.unchanged.append((new_path, file_id, kind)) else: delta.removed.append((old_inv.id2path(file_id), file_id, kind)) mutter('start looking for new files') for file_id in new_inv: if file_id in old_inv: continue kind = new_inv.get_file_kind(file_id) delta.added.append((new_inv.id2path(file_id), file_id, kind)) delta.removed.sort() delta.added.sort() delta.renamed.sort() delta.modified.sort() delta.unchanged.sort() return delta M 644 inline bzrlib/status.py data 1692 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def show_status(branch, show_unchanged=False, file_list=None, show_ids=False): """Display single-line status for non-ignored working files. show_all If true, show unmodified files too. file_list If set, only show the status of files in this list. """ import sys import diff branch._need_readlock() old = branch.basis_tree() new = branch.working_tree() if file_list: raise NotImplementedError("sorry, status on selected files is not implemented " "at the moment") delta = diff.compare_trees(old, new, want_unchanged=show_unchanged) delta.show(sys.stdout, show_ids=show_ids, show_unchanged=show_unchanged) unknowns = new.unknowns() done_header = False for path in unknowns: if not done_header: print 'unknown:' done_header = True print ' ', path M 644 inline testbzr data 8934 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): logfile.write('$ %s\n' % cmd) cmd = cmd.split() else: logfile.write('$ %r\n' % cmd) if cmd[0] == 'bzr': cmd[0] = BZRPATH return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open(LOGFILENAME, 'wt', buffering=1) try: mypath = os.path.abspath(sys.argv[0]) print '%-30s %s' % ('running tests from', mypath) global BZRPATH if len(sys.argv) > 1: BZRPATH = sys.argv[1] else: BZRPATH = os.path.join(os.path.split(mypath)[0], 'bzr') print '%-30s %s' % ('against bzr', BZRPATH) print '%-30s %s' % ('in directory', os.getcwd()) print print backtick([BZRPATH, 'version']) runcmd(['mkdir', TESTDIR]) cd(TESTDIR) test_root = os.getcwd() progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("internal tests") runcmd("bzr selftest") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) runcmd("bzr diff --message foo", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == 'unknown:\n test.txt\n' out = backtick("bzr status --all") assert out == "unknown:\n test.txt\n" out = backtick("bzr status test.txt --all") assert out == "unknown:\n test.txt\n" f = file('test2.txt', 'wt') f.write('goodbye cruel world...\n') f.close() out = backtick("bzr status test.txt") assert out == "? test.txt\n" out = backtick("bzr status") assert out == "? test.txt\n" \ + "? test2.txt\n" os.unlink('test2.txt') progress("command aliases") out = backtick("bzr st --all") assert out == "? test.txt\n" out = backtick("bzr stat") assert out == "? test.txt\n" progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == "A test.txt\n" progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == "sub2/hello.txt\n" assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == 'sub1/sub2/hello.txt\n' assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') assert backtick('bzr relpath sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') runcmd('bzr log') runcmd('bzr log -v') cd('..') cd('..') progress('ignore patterns') mkdir('ignorebranch') cd('ignorebranch') runcmd('bzr init') assert backtick('bzr unknowns') == '' file('foo.tmp', 'wt').write('tmp files are ignored') assert backtick('bzr unknowns') == '' file('foo.c', 'wt').write('int main() {}') assert backtick('bzr unknowns') == 'foo.c\n' runcmd('bzr add foo.c') assert backtick('bzr unknowns') == '' file('foo.blah', 'wt').write('blah') assert backtick('bzr unknowns') == 'foo.blah\n' runcmd('bzr ignore *.blah') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rt').read() == '*.blah\n' progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :478 committer Martin Pool 1115798487 +1000 data 72 - put back support for running diff or status on only selected files. from :477 M 644 inline TODO data 9993 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * testbzr should by default test the bzr binary in the same directory as the testbzr script, or take a path to it as a first parameter. Should show the version from bzr and the path name. * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. Medium things ------------- * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Merge Aaron's merge code. * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. We should be able to just get the id for the selected files, look up their location and diff just those files. No need to traverse the entire inventories. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Should be a signature at the top of the cache file. * Paranoid mode where we never trust SHA-1 matches. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` M 644 inline bzrlib/diff.py data 9233 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set, ImmutableSet from trace import mutter from errors import BzrError def _diff_one(oldlines, newlines, to_file, **kw): import difflib # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') to_file.writelines(ud) if nonl: print >>to_file, "\\ No newline at end of file" print >>to_file def show_diff(b, revision, file_list): import sys if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. delta = compare_trees(old_tree, new_tree, want_unchanged=False, file_list=file_list) for path, file_id, kind in delta.removed: print '*** removed %s %r' % (kind, path) if kind == 'file': _diff_one(old_tree.get_file(file_id).readlines(), [], sys.stdout, fromfile=old_label + path, tofile=DEVNULL) for path, file_id, kind in delta.added: print '*** added %s %r' % (kind, path) if kind == 'file': _diff_one([], new_tree.get_file(file_id).readlines(), sys.stdout, fromfile=DEVNULL, tofile=new_label + path) for old_path, new_path, file_id, kind, text_modified in delta.renamed: print '*** renamed %s %r => %r' % (kind, old_path, new_path) if text_modified: _diff_one(old_tree.get_file(file_id).readlines(), new_tree.get_file(file_id).readlines(), sys.stdout, fromfile=old_label + old_path, tofile=new_label + new_path) for path, file_id, kind in delta.modified: print '*** modified %s %r' % (kind, path) if kind == 'file': _diff_one(old_tree.get_file(file_id).readlines(), new_tree.get_file(file_id).readlines(), sys.stdout, fromfile=old_label + path, tofile=new_label + path) class TreeDelta: """Describes changes from one tree to another. Contains four lists: added (path, id, kind) removed (path, id, kind) renamed (oldpath, newpath, id, kind, text_modified) modified (path, id, kind) unchanged (path, id, kind) Each id is listed only once. Files that are both modified and renamed are listed only in renamed, with the text_modified flag true. The lists are normally sorted when the delta is created. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] self.unchanged = [] def show(self, to_file, show_ids=False, show_unchanged=False): def show_list(files): for path, fid, kind in files: if kind == 'directory': path += '/' elif kind == 'symlink': path += '@' if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if self.removed: print >>to_file, 'removed:' show_list(self.removed) if self.added: print >>to_file, 'added:' show_list(self.added) if self.renamed: print >>to_file, 'renamed:' for oldpath, newpath, fid, kind, text_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if self.modified: print >>to_file, 'modified:' show_list(self.modified) if show_unchanged and self.unchanged: print >>to_file, 'unchanged:' show_list(self.unchanged) def compare_trees(old_tree, new_tree, want_unchanged, file_list=None): """Describe changes from one tree to another. Returns a TreeDelta with details of added, modified, renamed, and deleted entries. The root entry is specifically exempt. This only considers versioned files. want_unchanged If true, also list files unchanged from one version to the next. file_list If true, only check for changes to specified files. """ old_inv = old_tree.inventory new_inv = new_tree.inventory delta = TreeDelta() mutter('start compare_trees') if file_list: file_list = ImmutableSet(file_list) for file_id in old_tree: if file_id in new_tree: kind = old_inv.get_file_kind(file_id) assert kind == new_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ 'invalid file kind %r' % kind if kind == 'root_directory': continue old_path = old_inv.id2path(file_id) new_path = new_inv.id2path(file_id) if file_list: if (old_path not in file_list and new_path not in file_list): continue if kind == 'file': old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False # TODO: Can possibly avoid calculating path strings if the # two files are unchanged and their names and parents are # the same and the parents are unchanged all the way up. # May not be worthwhile. if old_path != new_path: delta.renamed.append((old_path, new_path, file_id, kind, text_modified)) elif text_modified: delta.modified.append((new_path, file_id, kind)) elif want_unchanged: delta.unchanged.append((new_path, file_id, kind)) else: delta.removed.append((old_inv.id2path(file_id), file_id, kind)) mutter('start looking for new files') for file_id in new_inv: if file_id in old_inv: continue new_path = new_inv.id2path(file_id) if file_list: if new_path not in file_list: continue kind = new_inv.get_file_kind(file_id) delta.added.append((new_path, file_id, kind)) delta.removed.sort() delta.added.sort() delta.renamed.sort() delta.modified.sort() delta.unchanged.sort() return delta M 644 inline bzrlib/status.py data 1586 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def show_status(branch, show_unchanged=False, file_list=None, show_ids=False): """Display single-line status for non-ignored working files. show_all If true, show unmodified files too. file_list If set, only show the status of files in this list. """ import sys import diff branch._need_readlock() old = branch.basis_tree() new = branch.working_tree() delta = diff.compare_trees(old, new, want_unchanged=show_unchanged, file_list=file_list) delta.show(sys.stdout, show_ids=show_ids, show_unchanged=show_unchanged) unknowns = new.unknowns() done_header = False for path in unknowns: if not done_header: print 'unknown:' done_header = True print ' ', path commit refs/heads/master mark :479 committer Martin Pool 1115798514 +1000 data 4 todo from :478 M 644 inline TODO data 10088 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * testbzr should by default test the bzr binary in the same directory as the testbzr script, or take a path to it as a first parameter. Should show the version from bzr and the path name. * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. Medium things ------------- * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Merge Aaron's merge code. * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. We should be able to just get the id for the selected files, look up their location and diff just those files. No need to traverse the entire inventories. * ``bzr status DIR`` or ``bzr diff DIR`` should report on all changes under that directory. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Should be a signature at the top of the cache file. * Paranoid mode where we never trust SHA-1 matches. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :480 committer Martin Pool 1115798645 +1000 data 29 - more status form test fixes from :479 M 644 inline testbzr data 8959 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): logfile.write('$ %s\n' % cmd) cmd = cmd.split() else: logfile.write('$ %r\n' % cmd) if cmd[0] == 'bzr': cmd[0] = BZRPATH return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open(LOGFILENAME, 'wt', buffering=1) try: mypath = os.path.abspath(sys.argv[0]) print '%-30s %s' % ('running tests from', mypath) global BZRPATH if len(sys.argv) > 1: BZRPATH = sys.argv[1] else: BZRPATH = os.path.join(os.path.split(mypath)[0], 'bzr') print '%-30s %s' % ('against bzr', BZRPATH) print '%-30s %s' % ('in directory', os.getcwd()) print print backtick([BZRPATH, 'version']) runcmd(['mkdir', TESTDIR]) cd(TESTDIR) test_root = os.getcwd() progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("internal tests") runcmd("bzr selftest") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) runcmd("bzr diff --message foo", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == 'unknown:\n test.txt\n' out = backtick("bzr status --all") assert out == "unknown:\n test.txt\n" out = backtick("bzr status test.txt --all") assert out == "unknown:\n test.txt\n" f = file('test2.txt', 'wt') f.write('goodbye cruel world...\n') f.close() out = backtick("bzr status test.txt") assert out == "unknown:\n test.txt\n" out = backtick("bzr status") assert out == ("unknown:\n" " test.txt\n" " test2.txt\n") os.unlink('test2.txt') progress("command aliases") out = backtick("bzr st --all") assert out == "? test.txt\n" out = backtick("bzr stat") assert out == "? test.txt\n" progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == "A test.txt\n" progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == "sub2/hello.txt\n" assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == 'sub1/sub2/hello.txt\n' assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') assert backtick('bzr relpath sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') runcmd('bzr log') runcmd('bzr log -v') cd('..') cd('..') progress('ignore patterns') mkdir('ignorebranch') cd('ignorebranch') runcmd('bzr init') assert backtick('bzr unknowns') == '' file('foo.tmp', 'wt').write('tmp files are ignored') assert backtick('bzr unknowns') == '' file('foo.c', 'wt').write('int main() {}') assert backtick('bzr unknowns') == 'foo.c\n' runcmd('bzr add foo.c') assert backtick('bzr unknowns') == '' file('foo.blah', 'wt').write('blah') assert backtick('bzr unknowns') == 'foo.blah\n' runcmd('bzr ignore *.blah') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rt').read() == '*.blah\n' progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :481 committer Martin Pool 1115798804 +1000 data 61 - selected-file match also applies when listing unknown files from :480 M 644 inline bzrlib/status.py data 1769 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def show_status(branch, show_unchanged=False, file_list=None, show_ids=False): """Display single-line status for non-ignored working files. show_all If true, show unmodified files too. file_list If set, only show the status of files in this list. """ import sys import diff branch._need_readlock() old = branch.basis_tree() new = branch.working_tree() delta = diff.compare_trees(old, new, want_unchanged=show_unchanged, file_list=file_list) delta.show(sys.stdout, show_ids=show_ids, show_unchanged=show_unchanged) unknowns = new.unknowns() done_header = False for path in unknowns: # FIXME: Should also match if the unknown file is within a # specified directory. if file_list: if path not in file_list: continue if not done_header: print 'unknown:' done_header = True print ' ', path commit refs/heads/master mark :482 committer Martin Pool 1115798876 +1000 data 29 - more status form test fixes from :481 M 644 inline testbzr data 9071 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): logfile.write('$ %s\n' % cmd) cmd = cmd.split() else: logfile.write('$ %r\n' % cmd) if cmd[0] == 'bzr': cmd[0] = BZRPATH return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open(LOGFILENAME, 'wt', buffering=1) try: mypath = os.path.abspath(sys.argv[0]) print '%-30s %s' % ('running tests from', mypath) global BZRPATH if len(sys.argv) > 1: BZRPATH = sys.argv[1] else: BZRPATH = os.path.join(os.path.split(mypath)[0], 'bzr') print '%-30s %s' % ('against bzr', BZRPATH) print '%-30s %s' % ('in directory', os.getcwd()) print print backtick([BZRPATH, 'version']) runcmd(['mkdir', TESTDIR]) cd(TESTDIR) test_root = os.getcwd() progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("internal tests") runcmd("bzr selftest") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) runcmd("bzr diff --message foo", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == 'unknown:\n test.txt\n' out = backtick("bzr status --all") assert out == "unknown:\n test.txt\n" out = backtick("bzr status test.txt --all") assert out == "unknown:\n test.txt\n" f = file('test2.txt', 'wt') f.write('goodbye cruel world...\n') f.close() out = backtick("bzr status test.txt") assert out == "unknown:\n test.txt\n" out = backtick("bzr status") assert out == ("unknown:\n" " test.txt\n" " test2.txt\n") os.unlink('test2.txt') progress("command aliases") out = backtick("bzr st --all") assert out == ("unknown:\n" " test.txt\n") out = backtick("bzr stat") assert out == ("unknown:\n" " test.txt\n") progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == ("added:\n" " test.txt\n") progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == "sub2/hello.txt\n" assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == 'sub1/sub2/hello.txt\n' assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') assert backtick('bzr relpath sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') runcmd('bzr log') runcmd('bzr log -v') cd('..') cd('..') progress('ignore patterns') mkdir('ignorebranch') cd('ignorebranch') runcmd('bzr init') assert backtick('bzr unknowns') == '' file('foo.tmp', 'wt').write('tmp files are ignored') assert backtick('bzr unknowns') == '' file('foo.c', 'wt').write('int main() {}') assert backtick('bzr unknowns') == 'foo.c\n' runcmd('bzr add foo.c') assert backtick('bzr unknowns') == '' file('foo.blah', 'wt').write('blah') assert backtick('bzr unknowns') == 'foo.blah\n' runcmd('bzr ignore *.blah') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rt').read() == '*.blah\n' progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :483 committer Martin Pool 1115799097 +1000 data 57 - change 'file_list' to more explanatory 'specific_files' from :482 M 644 inline bzrlib/commands.py data 34589 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0], lock_mode='r') file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.', lock_mode='r') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, specific_files=file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from branch import find_branch b = find_branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') # FIXME: probably doesn't handle non-ascii patterns if os.path.exists(ifn): f = b.controlfile(ifn, 'rt') igns = f.read() f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' f = AtomicFile(ifn, 'wt') f.write(igns) f.commit() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False): ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() Branch('.').commit(message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/diff.py data 9290 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set, ImmutableSet from trace import mutter from errors import BzrError def _diff_one(oldlines, newlines, to_file, **kw): import difflib # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') to_file.writelines(ud) if nonl: print >>to_file, "\\ No newline at end of file" print >>to_file def show_diff(b, revision, specific_files): import sys if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. delta = compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=specific_files) for path, file_id, kind in delta.removed: print '*** removed %s %r' % (kind, path) if kind == 'file': _diff_one(old_tree.get_file(file_id).readlines(), [], sys.stdout, fromfile=old_label + path, tofile=DEVNULL) for path, file_id, kind in delta.added: print '*** added %s %r' % (kind, path) if kind == 'file': _diff_one([], new_tree.get_file(file_id).readlines(), sys.stdout, fromfile=DEVNULL, tofile=new_label + path) for old_path, new_path, file_id, kind, text_modified in delta.renamed: print '*** renamed %s %r => %r' % (kind, old_path, new_path) if text_modified: _diff_one(old_tree.get_file(file_id).readlines(), new_tree.get_file(file_id).readlines(), sys.stdout, fromfile=old_label + old_path, tofile=new_label + new_path) for path, file_id, kind in delta.modified: print '*** modified %s %r' % (kind, path) if kind == 'file': _diff_one(old_tree.get_file(file_id).readlines(), new_tree.get_file(file_id).readlines(), sys.stdout, fromfile=old_label + path, tofile=new_label + path) class TreeDelta: """Describes changes from one tree to another. Contains four lists: added (path, id, kind) removed (path, id, kind) renamed (oldpath, newpath, id, kind, text_modified) modified (path, id, kind) unchanged (path, id, kind) Each id is listed only once. Files that are both modified and renamed are listed only in renamed, with the text_modified flag true. The lists are normally sorted when the delta is created. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] self.unchanged = [] def show(self, to_file, show_ids=False, show_unchanged=False): def show_list(files): for path, fid, kind in files: if kind == 'directory': path += '/' elif kind == 'symlink': path += '@' if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if self.removed: print >>to_file, 'removed:' show_list(self.removed) if self.added: print >>to_file, 'added:' show_list(self.added) if self.renamed: print >>to_file, 'renamed:' for oldpath, newpath, fid, kind, text_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if self.modified: print >>to_file, 'modified:' show_list(self.modified) if show_unchanged and self.unchanged: print >>to_file, 'unchanged:' show_list(self.unchanged) def compare_trees(old_tree, new_tree, want_unchanged, specific_files=None): """Describe changes from one tree to another. Returns a TreeDelta with details of added, modified, renamed, and deleted entries. The root entry is specifically exempt. This only considers versioned files. want_unchanged If true, also list files unchanged from one version to the next. specific_files If true, only check for changes to specified files. """ old_inv = old_tree.inventory new_inv = new_tree.inventory delta = TreeDelta() mutter('start compare_trees') if specific_files: specific_files = ImmutableSet(specific_files) for file_id in old_tree: if file_id in new_tree: kind = old_inv.get_file_kind(file_id) assert kind == new_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ 'invalid file kind %r' % kind if kind == 'root_directory': continue old_path = old_inv.id2path(file_id) new_path = new_inv.id2path(file_id) if specific_files: if (old_path not in specific_files and new_path not in specific_files): continue if kind == 'file': old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False # TODO: Can possibly avoid calculating path strings if the # two files are unchanged and their names and parents are # the same and the parents are unchanged all the way up. # May not be worthwhile. if old_path != new_path: delta.renamed.append((old_path, new_path, file_id, kind, text_modified)) elif text_modified: delta.modified.append((new_path, file_id, kind)) elif want_unchanged: delta.unchanged.append((new_path, file_id, kind)) else: delta.removed.append((old_inv.id2path(file_id), file_id, kind)) mutter('start looking for new files') for file_id in new_inv: if file_id in old_inv: continue new_path = new_inv.id2path(file_id) if specific_files: if new_path not in specific_files: continue kind = new_inv.get_file_kind(file_id) delta.added.append((new_path, file_id, kind)) delta.removed.sort() delta.added.sort() delta.renamed.sort() delta.modified.sort() delta.unchanged.sort() return delta M 644 inline bzrlib/status.py data 1799 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def show_status(branch, show_unchanged=False, specific_files=None, show_ids=False): """Display single-line status for non-ignored working files. show_all If true, show unmodified files too. specific_files If set, only show the status of files in this list. """ import sys import diff branch._need_readlock() old = branch.basis_tree() new = branch.working_tree() delta = diff.compare_trees(old, new, want_unchanged=show_unchanged, specific_files=specific_files) delta.show(sys.stdout, show_ids=show_ids, show_unchanged=show_unchanged) unknowns = new.unknowns() done_header = False for path in unknowns: # FIXME: Should also match if the unknown file is within a # specified directory. if specific_files: if path not in specific_files: continue if not done_header: print 'unknown:' done_header = True print ' ', path commit refs/heads/master mark :484 committer Martin Pool 1115799143 +1000 data 4 todo from :483 M 644 inline TODO data 10152 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * ``bzr log DIR`` should give changes to any files within DIR. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * testbzr should by default test the bzr binary in the same directory as the testbzr script, or take a path to it as a first parameter. Should show the version from bzr and the path name. * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. Medium things ------------- * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Merge Aaron's merge code. * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. We should be able to just get the id for the selected files, look up their location and diff just those files. No need to traverse the entire inventories. * ``bzr status DIR`` or ``bzr diff DIR`` should report on all changes under that directory. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Should be a signature at the top of the cache file. * Paranoid mode where we never trust SHA-1 matches. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :485 committer Martin Pool 1115806449 +1000 data 183 - move commit code into its own module - remove some doctest tests in favour of black-box tests - specific-file parameters for diff and status now cover all files inside a directory from :484 M 644 inline bzrlib/commit.py data 7400 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def commit(branch, message, timestamp=None, timezone=None, committer=None, verbose=False): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. """ import os, time, tempfile from inventory import Inventory from osutils import isdir, isfile, sha_string, quotefn, \ local_time_offset, username from branch import gen_file_id from errors import BzrError from revision import Revision from textui import show_status from trace import mutter, note branch._need_writelock() ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_inv = branch.read_working_inventory() inv = Inventory() basis = branch.basis_tree() basis_inv = basis.inventory missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = branch.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if not os.path.exists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) missing_ids.append(file_id) continue # TODO: Handle files that have been deleted # TODO: Maybe a special case for empty files? Seems a # waste to store them many times. inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: raise BzrError("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): raise BzrError("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): raise BzrError("%s is entered as file but is not a file" % quotefn(p)) content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and (old_ie.text_size == entry.text_size) and (old_ie.text_sha1 == entry.text_sha1)): ## assert content == basis.get_file(file_id).read() entry.text_id = basis_inv[file_id].text_id mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: entry.text_id = gen_file_id(entry.name) branch.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: state = 'A' elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): state = 'M' else: state = 'R' show_status(state, entry.kind, quotefn(path)) for file_id in missing_ids: # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itbranch. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) branch.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) branch._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = branch.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) branch.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (branch.revno() + 1)) branch.append_revision(rev_id) if verbose: note("commited r%d" % branch.revno()) def _gen_revision_id(when): """Return new revision-id.""" from binascii import hexlify from osutils import rand_bytes, compact_date, user_email s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s M 644 inline bzrlib/branch.py data 25933 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. base Base directory of the branch. """ _lockmode = None def __init__(self, base, init=False, find_root=True, lock_mode='w'): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.lock(lock_mode) self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def lock(self, mode='w'): """Lock the on-disk branch, excluding other processes.""" try: import fcntl, errno if mode == 'w': lm = fcntl.LOCK_EX om = os.O_WRONLY | os.O_CREAT elif mode == 'r': lm = fcntl.LOCK_SH om = os.O_RDONLY else: raise BzrError("invalid locking mode %r" % mode) try: lockfile = os.open(self.controlfilename('branch-lock'), om) except OSError, e: if e.errno == errno.ENOENT: # might not exist on branches from <0.0.4 self.controlfile('branch-lock', 'w').close() lockfile = os.open(self.controlfilename('branch-lock'), om) else: raise e fcntl.lockf(lockfile, lm) def unlock(): fcntl.lockf(lockfile, fcntl.LOCK_UN) os.close(lockfile) self._lockmode = None self.unlock = unlock self._lockmode = mode except ImportError: warning("please write a locking method for platform %r" % sys.platform) def unlock(): self._lockmode = None self.unlock = unlock self._lockmode = mode def _need_readlock(self): if self._lockmode not in ['r', 'w']: raise BzrError('need read lock on branch, only have %r' % self._lockmode) def _need_writelock(self): if self._lockmode not in ['w']: raise BzrError('need write lock on branch, only have %r' % self._lockmode) def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" self._need_readlock() before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self._need_writelock() ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ self._need_writelock() # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): files = [files] inv = self.read_working_inventory() for f in files: if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" self._need_readlock() tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability self._need_writelock() if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" self._need_readlock() r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" self._need_readlock() i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" self._need_readlock() if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self._need_readlock() return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise BzrError('invalid history direction %r' % direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def commit(self, *args, **kw): """Deprecated""" from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" self._need_readlock() if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self._need_writelock() tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self._need_writelock() ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/commands.py data 34646 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0], lock_mode='r') file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.', lock_mode='r') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, specific_files=file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from branch import find_branch b = find_branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') # FIXME: probably doesn't handle non-ascii patterns if os.path.exists(ifn): f = b.controlfile(ifn, 'rt') igns = f.read() f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' f = AtomicFile(ifn, 'wt') f.write(igns) f.commit() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. TODO: Commit only selected files. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/diff.py data 9630 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set, ImmutableSet from trace import mutter from errors import BzrError def _diff_one(oldlines, newlines, to_file, **kw): import difflib # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') to_file.writelines(ud) if nonl: print >>to_file, "\\ No newline at end of file" print >>to_file def show_diff(b, revision, specific_files): import sys if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. delta = compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=specific_files) for path, file_id, kind in delta.removed: print '*** removed %s %r' % (kind, path) if kind == 'file': _diff_one(old_tree.get_file(file_id).readlines(), [], sys.stdout, fromfile=old_label + path, tofile=DEVNULL) for path, file_id, kind in delta.added: print '*** added %s %r' % (kind, path) if kind == 'file': _diff_one([], new_tree.get_file(file_id).readlines(), sys.stdout, fromfile=DEVNULL, tofile=new_label + path) for old_path, new_path, file_id, kind, text_modified in delta.renamed: print '*** renamed %s %r => %r' % (kind, old_path, new_path) if text_modified: _diff_one(old_tree.get_file(file_id).readlines(), new_tree.get_file(file_id).readlines(), sys.stdout, fromfile=old_label + old_path, tofile=new_label + new_path) for path, file_id, kind in delta.modified: print '*** modified %s %r' % (kind, path) if kind == 'file': _diff_one(old_tree.get_file(file_id).readlines(), new_tree.get_file(file_id).readlines(), sys.stdout, fromfile=old_label + path, tofile=new_label + path) class TreeDelta: """Describes changes from one tree to another. Contains four lists: added (path, id, kind) removed (path, id, kind) renamed (oldpath, newpath, id, kind, text_modified) modified (path, id, kind) unchanged (path, id, kind) Each id is listed only once. Files that are both modified and renamed are listed only in renamed, with the text_modified flag true. The lists are normally sorted when the delta is created. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] self.unchanged = [] def show(self, to_file, show_ids=False, show_unchanged=False): def show_list(files): for path, fid, kind in files: if kind == 'directory': path += '/' elif kind == 'symlink': path += '@' if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if self.removed: print >>to_file, 'removed:' show_list(self.removed) if self.added: print >>to_file, 'added:' show_list(self.added) if self.renamed: print >>to_file, 'renamed:' for oldpath, newpath, fid, kind, text_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if self.modified: print >>to_file, 'modified:' show_list(self.modified) if show_unchanged and self.unchanged: print >>to_file, 'unchanged:' show_list(self.unchanged) def compare_trees(old_tree, new_tree, want_unchanged, specific_files=None): """Describe changes from one tree to another. Returns a TreeDelta with details of added, modified, renamed, and deleted entries. The root entry is specifically exempt. This only considers versioned files. want_unchanged If true, also list files unchanged from one version to the next. specific_files If true, only check for changes to specified names or files within them. """ from osutils import is_inside_any old_inv = old_tree.inventory new_inv = new_tree.inventory delta = TreeDelta() mutter('start compare_trees') # TODO: match for specific files can be rather smarter by finding # the IDs of those files up front and then considering only that. for file_id in old_tree: if file_id in new_tree: kind = old_inv.get_file_kind(file_id) assert kind == new_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ 'invalid file kind %r' % kind if kind == 'root_directory': continue old_path = old_inv.id2path(file_id) new_path = new_inv.id2path(file_id) if specific_files: if (not is_inside_any(specific_files, old_path) and not is_inside_any(specific_files, new_path)): continue if kind == 'file': old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False # TODO: Can possibly avoid calculating path strings if the # two files are unchanged and their names and parents are # the same and the parents are unchanged all the way up. # May not be worthwhile. if old_path != new_path: delta.renamed.append((old_path, new_path, file_id, kind, text_modified)) elif text_modified: delta.modified.append((new_path, file_id, kind)) elif want_unchanged: delta.unchanged.append((new_path, file_id, kind)) else: old_path = old_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, old_path): continue delta.removed.append((old_path, file_id, kind)) mutter('start looking for new files') for file_id in new_inv: if file_id in old_inv: continue new_path = new_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, new_path): continue kind = new_inv.get_file_kind(file_id) delta.added.append((new_path, file_id, kind)) delta.removed.sort() delta.added.sort() delta.renamed.sort() delta.modified.sort() delta.unchanged.sort() return delta M 644 inline bzrlib/osutils.py data 9201 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, types, re, time, errno, sys from stat import S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE from errors import bailout, BzrError from trace import mutter import bzrlib def make_readonly(filename): """Make a filename read-only.""" # TODO: probably needs to be fixed for windows mod = os.stat(filename).st_mode mod = mod & 0777555 os.chmod(filename, mod) def make_writable(filename): mod = os.stat(filename).st_mode mod = mod | 0200 os.chmod(filename, mod) _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])') def quotefn(f): """Return shell-quoted filename""" ## We could be a bit more terse by using double-quotes etc f = _QUOTE_RE.sub(r'\\\1', f) if f[0] == '~': f[0:1] = r'\~' return f def file_kind(f): mode = os.lstat(f)[ST_MODE] if S_ISREG(mode): return 'file' elif S_ISDIR(mode): return 'directory' elif S_ISLNK(mode): return 'symlink' else: raise BzrError("can't handle file kind with mode %o of %r" % (mode, f)) def isdir(f): """True if f is an accessible directory.""" try: return S_ISDIR(os.lstat(f)[ST_MODE]) except OSError: return False def isfile(f): """True if f is a regular file.""" try: return S_ISREG(os.lstat(f)[ST_MODE]) except OSError: return False def is_inside(dir, fname): """True if fname is inside dir. """ return os.path.commonprefix([dir, fname]) == dir def is_inside_any(dir_list, fname): """True if fname is inside any of given dirs.""" # quick scan for perfect match if fname in dir_list: return True for dirname in dir_list: if is_inside(dirname, fname): return True else: return False def pumpfile(fromfile, tofile): """Copy contents of one file to another.""" tofile.write(fromfile.read()) def uuid(): """Return a new UUID""" try: return file('/proc/sys/kernel/random/uuid').readline().rstrip('\n') except IOError: return chomp(os.popen('uuidgen').readline()) def sha_file(f): import sha if hasattr(f, 'tell'): assert f.tell() == 0 s = sha.new() BUFSIZE = 128<<10 while True: b = f.read(BUFSIZE) if not b: break s.update(b) return s.hexdigest() def sha_string(f): import sha s = sha.new() s.update(f) return s.hexdigest() def fingerprint_file(f): import sha s = sha.new() b = f.read() s.update(b) size = len(b) return {'size': size, 'sha1': s.hexdigest()} def config_dir(): """Return per-user configuration directory. By default this is ~/.bzr.conf/ TODO: Global option --config-dir to override this. """ return os.path.expanduser("~/.bzr.conf") def _auto_user_id(): """Calculate automatic user identification. Returns (realname, email). Only used when none is set in the environment or the id file. This previously used the FQDN as the default domain, but that can be very slow on machines where DNS is broken. So now we simply use the hostname. """ import socket # XXX: Any good way to get real user name on win32? try: import pwd uid = os.getuid() w = pwd.getpwuid(uid) gecos = w.pw_gecos.decode(bzrlib.user_encoding) username = w.pw_name.decode(bzrlib.user_encoding) comma = gecos.find(',') if comma == -1: realname = gecos else: realname = gecos[:comma] if not realname: realname = username except ImportError: import getpass realname = username = getpass.getuser().decode(bzrlib.user_encoding) return realname, (username + '@' + socket.gethostname()) def _get_user_id(): """Return the full user id from a file or environment variable. TODO: Allow taking this from a file in the branch directory too for per-branch ids.""" v = os.environ.get('BZREMAIL') if v: return v.decode(bzrlib.user_encoding) try: return (open(os.path.join(config_dir(), "email")) .read() .decode(bzrlib.user_encoding) .rstrip("\r\n")) except IOError, e: if e.errno != errno.ENOENT: raise e v = os.environ.get('EMAIL') if v: return v.decode(bzrlib.user_encoding) else: return None def username(): """Return email-style username. Something similar to 'Martin Pool ' TODO: Check it's reasonably well-formed. """ v = _get_user_id() if v: return v name, email = _auto_user_id() if name: return '%s <%s>' % (name, email) else: return email _EMAIL_RE = re.compile(r'[\w+.-]+@[\w+.-]+') def user_email(): """Return just the email component of a username.""" e = _get_user_id() if e: m = _EMAIL_RE.search(e) if not m: bailout("%r doesn't seem to contain a reasonable email address" % e) return m.group(0) return _auto_user_id()[1] def compare_files(a, b): """Returns true if equal in contents""" BUFSIZE = 4096 while True: ai = a.read(BUFSIZE) bi = b.read(BUFSIZE) if ai != bi: return False if ai == '': return True def local_time_offset(t=None): """Return offset of local zone from GMT, either at present or at time t.""" # python2.3 localtime() can't take None if t == None: t = time.time() if time.localtime(t).tm_isdst and time.daylight: return -time.altzone else: return -time.timezone def format_date(t, offset=0, timezone='original'): ## TODO: Perhaps a global option to use either universal or local time? ## Or perhaps just let people set $TZ? assert isinstance(t, float) if timezone == 'utc': tt = time.gmtime(t) offset = 0 elif timezone == 'original': if offset == None: offset = 0 tt = time.gmtime(t + offset) elif timezone == 'local': tt = time.localtime(t) offset = local_time_offset(t) else: bailout("unsupported timezone format %r", ['options are "utc", "original", "local"']) return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt) + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)) def compact_date(when): return time.strftime('%Y%m%d%H%M%S', time.gmtime(when)) def filesize(f): """Return size of given open file.""" return os.fstat(f.fileno())[ST_SIZE] if hasattr(os, 'urandom'): # python 2.4 and later rand_bytes = os.urandom elif sys.platform == 'linux2': rand_bytes = file('/dev/urandom', 'rb').read else: # not well seeded, but better than nothing def rand_bytes(n): import random s = '' while n: s += chr(random.randint(0, 255)) n -= 1 return s ## TODO: We could later have path objects that remember their list ## decomposition (might be too tricksy though.) def splitpath(p): """Turn string into list of parts. >>> splitpath('a') ['a'] >>> splitpath('a/b') ['a', 'b'] >>> splitpath('a/./b') ['a', 'b'] >>> splitpath('a/.b') ['a', '.b'] >>> splitpath('a/../b') Traceback (most recent call last): ... BzrError: ("sorry, '..' not allowed in path", []) """ assert isinstance(p, types.StringTypes) # split on either delimiter because people might use either on # Windows ps = re.split(r'[\\/]', p) rps = [] for f in ps: if f == '..': bailout("sorry, %r not allowed in path" % f) elif (f == '.') or (f == ''): pass else: rps.append(f) return rps def joinpath(p): assert isinstance(p, list) for f in p: if (f == '..') or (f == None) or (f == ''): bailout("sorry, %r not allowed in path" % f) return os.path.join(*p) def appendpath(p1, p2): if p1 == '': return p2 else: return os.path.join(p1, p2) def extern_command(cmd, ignore_errors = False): mutter('external command: %s' % `cmd`) if os.system(cmd): if not ignore_errors: bailout('command failed') commit refs/heads/master mark :486 committer Martin Pool 1115859258 +1000 data 88 - Write out statcache if any files are updated, even if their content has not changed from :485 M 644 inline bzrlib/statcache.py data 6579 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import stat, os, sha, time from binascii import b2a_qp, a2b_qp from trace import mutter from errors import BzrError """File stat cache to speed up tree comparisons. This module basically gives a quick way to find the SHA-1 and related information of a file in the working directory, without actually reading and hashing the whole file. This is done by maintaining a cache indexed by a file fingerprint of (path, size, mtime, ctime, ino, dev) pointing to the SHA-1. If the fingerprint has changed, we assume the file content has not changed either and the SHA-1 is therefore the same. If any of the fingerprint fields have changed then the file content *may* have changed, or it may not have. We need to reread the file contents to make sure, but this is not visible to the user or higher-level code (except as a delay of course). The mtime and ctime are stored with nanosecond fields, but not all filesystems give this level of precision. There is therefore a possible race: the file might be modified twice within a second without changing the size or mtime, and a SHA-1 cached from the first version would be wrong. We handle this by not recording a cached hash for any files which were modified in the current second and that therefore have the chance to change again before the second is up. The only known hole in this design is if the system clock jumps backwards crossing invocations of bzr. Please don't do that; use ntp to gradually adjust your clock or don't use bzr over the step. At the moment this is stored in a simple textfile; it might be nice to use a tdb instead. The cache is represented as a map from file_id to a tuple of (file_id, sha1, path, size, mtime, ctime, ino, dev). """ FP_SIZE = 0 FP_MTIME = 1 FP_CTIME = 2 FP_INO = 3 FP_DEV = 4 SC_FILE_ID = 0 SC_SHA1 = 1 def fingerprint(abspath): try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) def _write_cache(basedir, entry_iter, dangerfiles): from atomicfile import AtomicFile cachefn = os.path.join(basedir, '.bzr', 'stat-cache') outf = AtomicFile(cachefn, 'wb', 'utf-8') try: for entry in entry_iter: if entry[0] in dangerfiles: continue outf.write(entry[0] + ' ' + entry[1] + ' ') outf.write(b2a_qp(entry[2], True)) outf.write(' %d %d %d %d %d\n' % entry[3:]) outf.commit() finally: if not outf.closed: outf.abort() def load_cache(basedir): import codecs cache = {} try: cachefn = os.path.join(basedir, '.bzr', 'stat-cache') cachefile = codecs.open(cachefn, 'r', 'utf-8') except IOError: return cache for l in cachefile: f = l.split(' ') file_id = f[0] if file_id in cache: raise BzrError("duplicated file_id in cache: {%s}" % file_id) cache[file_id] = (f[0], f[1], a2b_qp(f[2])) + tuple([long(x) for x in f[3:]]) return cache def _files_from_inventory(inv): for path, ie in inv.iter_entries(): if ie.kind != 'file': continue yield ie.file_id, path def update_cache(basedir, inv, flush=False): """Update and return the cache for the branch. The returned cache may contain entries that have not been written to disk for files recently touched. flush -- discard any previous cache and recalculate from scratch. """ # TODO: It's supposed to be faster to stat the files in order by inum. # We don't directly know the inum of the files of course but we do # know where they were last sighted, so we can sort by that. assert isinstance(flush, bool) if flush: cache = {} else: cache = load_cache(basedir) return _update_cache_from_list(basedir, cache, _files_from_inventory(inv)) def _update_cache_from_list(basedir, cache, to_update): """Update and return the cache for given files. cache -- Previously cached values to be validated. to_update -- Sequence of (file_id, path) pairs to check. """ from sets import Set stat_cnt = missing_cnt = hardcheck = change_cnt = 0 # files that have been recently touched and can't be # committed to a persistent cache yet. dangerfiles = Set() now = int(time.time()) ## mutter('update statcache under %r' % basedir) for file_id, path in to_update: abspath = os.path.join(basedir, path) fp = fingerprint(abspath) stat_cnt += 1 cacheentry = cache.get(file_id) if fp == None: # not here if cacheentry: del cache[file_id] change_cnt += 1 missing_cnt += 1 continue if (fp[FP_MTIME] >= now) or (fp[FP_CTIME] >= now): dangerfiles.add(file_id) if cacheentry and (cacheentry[3:] == fp): continue # all stat fields unchanged hardcheck += 1 dig = sha.new(file(abspath, 'rb').read()).hexdigest() # We update the cache even if the digest has not changed from # last time we looked, so that the fingerprint fields will # match in future. cacheentry = (file_id, dig, path) + fp cache[file_id] = cacheentry change_cnt += 1 mutter('statcache: statted %d files, read %d files, %d changed, %d dangerous, ' '%d in cache' % (stat_cnt, hardcheck, change_cnt, len(dangerfiles), len(cache))) if change_cnt: mutter('updating on-disk statcache') _write_cache(basedir, cache.itervalues(), dangerfiles) return cache commit refs/heads/master mark :487 committer Martin Pool 1115863934 +1000 data 4 todo from :486 M 644 inline TODO data 10258 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * ``bzr log DIR`` should give changes to any files within DIR. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * testbzr should by default test the bzr binary in the same directory as the testbzr script, or take a path to it as a first parameter. Should show the version from bzr and the path name. * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. * Maybe also store directories in the statcache so that we can quickly identify that they still exist. Medium things ------------- * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Selective commit of only some files. * Merge Aaron's merge code. * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. We should be able to just get the id for the selected files, look up their location and diff just those files. No need to traverse the entire inventories. * ``bzr status DIR`` or ``bzr diff DIR`` should report on all changes under that directory. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Should be a signature at the top of the cache file. * Paranoid mode where we never trust SHA-1 matches. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :488 committer Martin Pool 1115864075 +1000 data 35 - new helper function kind_marker() from :487 M 644 inline bzrlib/osutils.py data 9426 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, types, re, time, errno, sys from stat import S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE from errors import bailout, BzrError from trace import mutter import bzrlib def make_readonly(filename): """Make a filename read-only.""" # TODO: probably needs to be fixed for windows mod = os.stat(filename).st_mode mod = mod & 0777555 os.chmod(filename, mod) def make_writable(filename): mod = os.stat(filename).st_mode mod = mod | 0200 os.chmod(filename, mod) _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])') def quotefn(f): """Return shell-quoted filename""" ## We could be a bit more terse by using double-quotes etc f = _QUOTE_RE.sub(r'\\\1', f) if f[0] == '~': f[0:1] = r'\~' return f def file_kind(f): mode = os.lstat(f)[ST_MODE] if S_ISREG(mode): return 'file' elif S_ISDIR(mode): return 'directory' elif S_ISLNK(mode): return 'symlink' else: raise BzrError("can't handle file kind with mode %o of %r" % (mode, f)) def kind_marker(kind): if kind == 'file': return '' elif kind == 'directory': return '/' elif kind == 'symlink': return '@' else: raise BzrError('invalid file kind %r' % kind) def isdir(f): """True if f is an accessible directory.""" try: return S_ISDIR(os.lstat(f)[ST_MODE]) except OSError: return False def isfile(f): """True if f is a regular file.""" try: return S_ISREG(os.lstat(f)[ST_MODE]) except OSError: return False def is_inside(dir, fname): """True if fname is inside dir. """ return os.path.commonprefix([dir, fname]) == dir def is_inside_any(dir_list, fname): """True if fname is inside any of given dirs.""" # quick scan for perfect match if fname in dir_list: return True for dirname in dir_list: if is_inside(dirname, fname): return True else: return False def pumpfile(fromfile, tofile): """Copy contents of one file to another.""" tofile.write(fromfile.read()) def uuid(): """Return a new UUID""" try: return file('/proc/sys/kernel/random/uuid').readline().rstrip('\n') except IOError: return chomp(os.popen('uuidgen').readline()) def sha_file(f): import sha if hasattr(f, 'tell'): assert f.tell() == 0 s = sha.new() BUFSIZE = 128<<10 while True: b = f.read(BUFSIZE) if not b: break s.update(b) return s.hexdigest() def sha_string(f): import sha s = sha.new() s.update(f) return s.hexdigest() def fingerprint_file(f): import sha s = sha.new() b = f.read() s.update(b) size = len(b) return {'size': size, 'sha1': s.hexdigest()} def config_dir(): """Return per-user configuration directory. By default this is ~/.bzr.conf/ TODO: Global option --config-dir to override this. """ return os.path.expanduser("~/.bzr.conf") def _auto_user_id(): """Calculate automatic user identification. Returns (realname, email). Only used when none is set in the environment or the id file. This previously used the FQDN as the default domain, but that can be very slow on machines where DNS is broken. So now we simply use the hostname. """ import socket # XXX: Any good way to get real user name on win32? try: import pwd uid = os.getuid() w = pwd.getpwuid(uid) gecos = w.pw_gecos.decode(bzrlib.user_encoding) username = w.pw_name.decode(bzrlib.user_encoding) comma = gecos.find(',') if comma == -1: realname = gecos else: realname = gecos[:comma] if not realname: realname = username except ImportError: import getpass realname = username = getpass.getuser().decode(bzrlib.user_encoding) return realname, (username + '@' + socket.gethostname()) def _get_user_id(): """Return the full user id from a file or environment variable. TODO: Allow taking this from a file in the branch directory too for per-branch ids.""" v = os.environ.get('BZREMAIL') if v: return v.decode(bzrlib.user_encoding) try: return (open(os.path.join(config_dir(), "email")) .read() .decode(bzrlib.user_encoding) .rstrip("\r\n")) except IOError, e: if e.errno != errno.ENOENT: raise e v = os.environ.get('EMAIL') if v: return v.decode(bzrlib.user_encoding) else: return None def username(): """Return email-style username. Something similar to 'Martin Pool ' TODO: Check it's reasonably well-formed. """ v = _get_user_id() if v: return v name, email = _auto_user_id() if name: return '%s <%s>' % (name, email) else: return email _EMAIL_RE = re.compile(r'[\w+.-]+@[\w+.-]+') def user_email(): """Return just the email component of a username.""" e = _get_user_id() if e: m = _EMAIL_RE.search(e) if not m: bailout("%r doesn't seem to contain a reasonable email address" % e) return m.group(0) return _auto_user_id()[1] def compare_files(a, b): """Returns true if equal in contents""" BUFSIZE = 4096 while True: ai = a.read(BUFSIZE) bi = b.read(BUFSIZE) if ai != bi: return False if ai == '': return True def local_time_offset(t=None): """Return offset of local zone from GMT, either at present or at time t.""" # python2.3 localtime() can't take None if t == None: t = time.time() if time.localtime(t).tm_isdst and time.daylight: return -time.altzone else: return -time.timezone def format_date(t, offset=0, timezone='original'): ## TODO: Perhaps a global option to use either universal or local time? ## Or perhaps just let people set $TZ? assert isinstance(t, float) if timezone == 'utc': tt = time.gmtime(t) offset = 0 elif timezone == 'original': if offset == None: offset = 0 tt = time.gmtime(t + offset) elif timezone == 'local': tt = time.localtime(t) offset = local_time_offset(t) else: bailout("unsupported timezone format %r", ['options are "utc", "original", "local"']) return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt) + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)) def compact_date(when): return time.strftime('%Y%m%d%H%M%S', time.gmtime(when)) def filesize(f): """Return size of given open file.""" return os.fstat(f.fileno())[ST_SIZE] if hasattr(os, 'urandom'): # python 2.4 and later rand_bytes = os.urandom elif sys.platform == 'linux2': rand_bytes = file('/dev/urandom', 'rb').read else: # not well seeded, but better than nothing def rand_bytes(n): import random s = '' while n: s += chr(random.randint(0, 255)) n -= 1 return s ## TODO: We could later have path objects that remember their list ## decomposition (might be too tricksy though.) def splitpath(p): """Turn string into list of parts. >>> splitpath('a') ['a'] >>> splitpath('a/b') ['a', 'b'] >>> splitpath('a/./b') ['a', 'b'] >>> splitpath('a/.b') ['a', '.b'] >>> splitpath('a/../b') Traceback (most recent call last): ... BzrError: ("sorry, '..' not allowed in path", []) """ assert isinstance(p, types.StringTypes) # split on either delimiter because people might use either on # Windows ps = re.split(r'[\\/]', p) rps = [] for f in ps: if f == '..': bailout("sorry, %r not allowed in path" % f) elif (f == '.') or (f == ''): pass else: rps.append(f) return rps def joinpath(p): assert isinstance(p, list) for f in p: if (f == '..') or (f == None) or (f == ''): bailout("sorry, %r not allowed in path" % f) return os.path.join(*p) def appendpath(p1, p2): if p1 == '': return p2 else: return os.path.join(p1, p2) def extern_command(cmd, ignore_errors = False): mutter('external command: %s' % `cmd`) if os.system(cmd): if not ignore_errors: bailout('command failed') commit refs/heads/master mark :489 committer Martin Pool 1115864328 +1000 data 71 - WorkingTree loads statcache in constructor and holds it permanently from :488 M 644 inline bzrlib/workingtree.py data 9043 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os import bzrlib.tree from errors import BzrCheckError from trace import mutter import statcache class WorkingTree(bzrlib.tree.Tree): """Working copy tree. The inventory is held in the `Branch` working-inventory, and the files are in a directory on disk. It is possible for a `WorkingTree` to have a filename which is not listed in the Inventory and vice versa. """ _statcache = None def __init__(self, basedir, inv): self._inventory = inv self.basedir = basedir self.path2id = inv.path2id self._update_statcache() def __iter__(self): """Iterate through file_ids for this tree. file_ids are in a WorkingTree if they are in the working inventory and the working file exists. """ inv = self._inventory for file_id in self._inventory: # TODO: This is slightly redundant; we should be able to just # check the statcache but it only includes regular files. # only include files which still exist on disk ie = inv[file_id] if ie.kind == 'file': if ((file_id in self._statcache) or (os.path.exists(self.abspath(inv.id2path(file_id))))): yield file_id def __repr__(self): return "<%s of %s>" % (self.__class__.__name__, self.basedir) def abspath(self, filename): return os.path.join(self.basedir, filename) def has_filename(self, filename): return os.path.exists(self.abspath(filename)) def get_file(self, file_id): return self.get_file_byname(self.id2path(file_id)) def get_file_byname(self, filename): return file(self.abspath(filename), 'rb') def _get_store_filename(self, file_id): ## XXX: badly named; this isn't in the store at all return self.abspath(self.id2path(file_id)) def has_id(self, file_id): # files that have been deleted are excluded if not self.inventory.has_id(file_id): return False if file_id in self._statcache: return True return os.path.exists(self.abspath(self.id2path(file_id))) __contains__ = has_id def _update_statcache(self): import statcache if not self._statcache: self._statcache = statcache.update_cache(self.basedir, self.inventory) def get_file_size(self, file_id): import os, stat return os.stat(self._get_store_filename(file_id))[stat.ST_SIZE] def get_file_sha1(self, file_id): return self._statcache[file_id][statcache.SC_SHA1] def file_class(self, filename): if self.path2id(filename): return 'V' elif self.is_ignored(filename): return 'I' else: return '?' def list_files(self): """Recursively list all files as (path, class, kind, id). Lists, but does not descend into unversioned directories. This does not include files that have been deleted in this tree. Skips the control directory. """ from osutils import appendpath, file_kind import os inv = self.inventory def descend(from_dir_relpath, from_dir_id, dp): ls = os.listdir(dp) ls.sort() for f in ls: ## TODO: If we find a subdirectory with its own .bzr ## directory, then that is a separate tree and we ## should exclude it. if bzrlib.BZRDIR == f: continue # path within tree fp = appendpath(from_dir_relpath, f) # absolute path fap = appendpath(dp, f) f_ie = inv.get_child(from_dir_id, f) if f_ie: c = 'V' elif self.is_ignored(fp): c = 'I' else: c = '?' fk = file_kind(fap) if f_ie: if f_ie.kind != fk: raise BzrCheckError("file %r entered as kind %r id %r, " "now of kind %r" % (fap, f_ie.kind, f_ie.file_id, fk)) yield fp, c, fk, (f_ie and f_ie.file_id) if fk != 'directory': continue if c != 'V': # don't descend unversioned directories continue for ff in descend(fp, f_ie.file_id, fap): yield ff for f in descend('', inv.root.file_id, self.basedir): yield f def unknowns(self): for subp in self.extras(): if not self.is_ignored(subp): yield subp def extras(self): """Yield all unknown files in this WorkingTree. If there are any unknown directories then only the directory is returned, not all its children. But if there are unknown files under a versioned subdirectory, they are returned. Currently returned depth-first, sorted by name within directories. """ ## TODO: Work from given directory downwards from osutils import isdir, appendpath for path, dir_entry in self.inventory.directories(): mutter("search for unknowns in %r" % path) dirabs = self.abspath(path) if not isdir(dirabs): # e.g. directory deleted continue fl = [] for subf in os.listdir(dirabs): if (subf != '.bzr' and (subf not in dir_entry.children)): fl.append(subf) fl.sort() for subf in fl: subp = appendpath(path, subf) yield subp def ignored_files(self): """Yield list of PATH, IGNORE_PATTERN""" for subp in self.extras(): pat = self.is_ignored(subp) if pat != None: yield subp, pat def get_ignore_list(self): """Return list of ignore patterns. Cached in the Tree object after the first call. """ if hasattr(self, '_ignorelist'): return self._ignorelist l = bzrlib.DEFAULT_IGNORE[:] if self.has_filename(bzrlib.IGNORE_FILENAME): f = self.get_file_byname(bzrlib.IGNORE_FILENAME) l.extend([line.rstrip("\n\r") for line in f.readlines()]) self._ignorelist = l return l def is_ignored(self, filename): r"""Check whether the filename matches an ignore pattern. Patterns containing '/' or '\' need to match the whole path; others match against only the last component. If the file is ignored, returns the pattern which caused it to be ignored, otherwise None. So this can simply be used as a boolean if desired.""" # TODO: Use '**' to match directories, and other extended # globbing stuff from cvs/rsync. # XXX: fnmatch is actually not quite what we want: it's only # approximately the same as real Unix fnmatch, and doesn't # treat dotfiles correctly and allows * to match /. # Eventually it should be replaced with something more # accurate. import fnmatch from osutils import splitpath for pat in self.get_ignore_list(): if '/' in pat or '\\' in pat: # as a special case, you can put ./ at the start of a # pattern; this is good to match in the top-level # only; if (pat[:2] == './') or (pat[:2] == '.\\'): newpat = pat[2:] else: newpat = pat if fnmatch.fnmatchcase(filename, newpat): return pat else: if fnmatch.fnmatchcase(splitpath(filename)[-1], pat): return pat else: return None commit refs/heads/master mark :490 committer Martin Pool 1115864349 +1000 data 3 doc from :489 M 644 inline bzrlib/inventory.py data 18793 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # This should really be an id randomly assigned when the tree is # created, but it's not for now. ROOT_ID = "TREE_ROOT" import sys, os.path, types, re from sets import Set try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from xml import XMLMixin from errors import bailout, BzrError, BzrCheckError import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') 'TREE_ROOT' >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID)) >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123')) >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' TODO: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ # TODO: split InventoryEntry into subclasses for files, # directories, etc etc. text_sha1 = None text_size = None def __init__(self, file_id, name, kind, parent_id, text_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID) >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID) Traceback (most recent call last): BzrCheckError: InventoryEntry name 'src/hello.c' is invalid """ if '/' in name or '\\' in name: raise BzrCheckError('InventoryEntry name %r is invalid' % name) self.file_id = file_id self.name = name self.kind = kind self.text_id = text_id self.parent_id = parent_id if kind == 'directory': self.children = {} elif kind == 'file': pass else: raise BzrError("unhandled entry kind %r" % kind) def sorted_children(self): l = self.children.items() l.sort() return l def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.parent_id, text_id=self.text_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size # note that children are *not* copied; they're pulled across when # others are added return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size != None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1']: v = getattr(self, f) if v != None: e.set(f, v) # to be conservative, we don't externalize the root pointers # for now, leaving them as null in the xml form. in a future # version it will be implied by nested elements. if self.parent_id != ROOT_ID: assert isinstance(self.parent_id, basestring) e.set('parent_id', self.parent_id) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' ## original format inventories don't have a parent_id for ## nodes in the root directory, but it's cleaner to use one ## internally. parent_id = elt.get('parent_id') if parent_id == None: parent_id = ROOT_ID self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __cmp__(self, other): if self is other: return 0 if not isinstance(other, InventoryEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.name, other.name) \ or cmp(self.text_sha1, other.text_sha1) \ or cmp(self.text_size, other.text_size) \ or cmp(self.text_id, other.text_id) \ or cmp(self.parent_id, other.parent_id) \ or cmp(self.kind, other.kind) class RootEntry(InventoryEntry): def __init__(self, file_id): self.file_id = file_id self.children = {} self.kind = 'root_directory' self.parent_id = None self.name = '' def __cmp__(self, other): if self is other: return 0 if not isinstance(other, RootEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.children, other.children) class Inventory(XMLMixin): """Inventory of versioned files in a tree. This describes which file_id is present at each point in the tree, and possibly the SHA-1 or other information about the file. Entries can be looked up either by path or by file_id. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted, other than through the Inventory API. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID)) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. The inventory is created with a default root directory, with an id of None. """ self.root = RootEntry(ROOT_ID) self._byid = {self.root.file_id: self.root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, from_dir=None): """Return (path, entry) pairs, in order by name.""" if from_dir == None: assert self.root from_dir = self.root elif isinstance(from_dir, basestring): from_dir = self._byid[from_dir] kids = from_dir.children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(from_dir=ie.file_id): yield os.path.join(name, cn), cie def directories(self): """Return (path, entry) pairs for all directories. """ def descend(parent_ie): parent_name = parent_ie.name yield parent_name, parent_ie # directory children in sorted order dn = [] for ie in parent_ie.children.itervalues(): if ie.kind == 'directory': dn.append((ie.name, ie)) dn.sort() for name, child_ie in dn: for sub_name, sub_ie in descend(child_ie): yield appendpath(parent_name, sub_name), sub_ie for name, ie in descend(self.root): yield name, ie def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID)) >>> inv['123123'].name 'hello.c' """ try: return self._byid[file_id] except KeyError: if file_id == None: raise BzrError("can't look up file_id None") else: raise BzrError("file_id {%s} not in inventory" % file_id) def get_file_kind(self, file_id): return self._byid[file_id].kind def get_child(self, parent_id, filename): return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self._byid: bailout("inventory already contains entry with id {%s}" % entry.file_id) try: parent = self._byid[entry.parent_id] except KeyError: bailout("parent_id {%s} not in inventory" % entry.parent_id) if parent.children.has_key(entry.name): bailout("%s is already versioned" % appendpath(self.id2path(parent.file_id), entry.name)) self._byid[entry.file_id] = entry parent.children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: bailout("cannot re-add root of inventory") if file_id == None: file_id = bzrlib.branch.gen_file_id(relpath) parent_id = self.path2id(parts[:-1]) assert parent_id != None ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def id_set(self): return Set(self._byid) def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID)) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __cmp__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 True """ if self is other: return 0 if not isinstance(other, Inventory): return NotImplemented if self.id_set() ^ other.id_set(): return 1 for file_id in self._byid: c = cmp(self[file_id], other[file_id]) if c: return c return 0 def get_idpath(self, file_id): """Return a list of file_ids for the path to an entry. The list contains one element for each directory followed by the id of the file itself. So the length of the returned list is equal to the depth of the file in the tree, counting the root directory as depth 1. """ p = [] while file_id != None: try: ie = self._byid[file_id] except KeyError: bailout("file_id {%s} not found in inventory" % file_id) p.insert(0, ie.file_id) file_id = ie.parent_id return p def id2path(self, file_id): """Return as a list the path to file_id.""" # get all names, skipping root p = [self[fid].name for fid in self.get_idpath(file_id)[1:]] return os.sep.join(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. Returns None iff the path is not found. """ if isinstance(name, types.StringTypes): name = splitpath(name) mutter("lookup path %r" % name) parent = self.root for f in name: try: cie = parent.children[f] assert cie.name == f assert cie.parent_id == parent.file_id parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): return self._byid.has_key(file_id) def rename(self, file_id, new_parent_id, new_name): """Move a file within the inventory. This can change either the name, or the parent, or both. This does not move the working file.""" if not is_valid_name(new_name): bailout("not an acceptable filename: %r" % new_name) new_parent = self._byid[new_parent_id] if new_name in new_parent.children: bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id))) new_parent_idpath = self.get_idpath(new_parent_id) if file_id in new_parent_idpath: bailout("cannot move directory %r into a subdirectory of itself, %r" % (self.id2path(file_id), self.id2path(new_parent_id))) file_ie = self._byid[file_id] old_parent = self._byid[file_ie.parent_id] # TODO: Don't leave things messed up if this fails del old_parent.children[file_ie.name] new_parent.children[new_name] = file_ie file_ie.name = new_name file_ie.parent_id = new_parent_id _NAME_RE = re.compile(r'^[^/\\]+$') def is_valid_name(name): return bool(_NAME_RE.match(name)) commit refs/heads/master mark :491 committer Martin Pool 1115867429 +1000 data 59 - Selective commit! - commit is now more verbose by default from :490 M 644 inline NEWS data 7309 bzr-0.0.5 NOT RELEASED YET CHANGES: * ``bzr`` with no command now shows help rather than giving an error. Suggested by Michael Ellerman. * ``bzr status`` output format changed, because svn-style output doesn't really match the model of bzr. Now files are grouped by status and can be shown with their IDs. ``bzr status --all`` shows all versioned files and unknown files but not ignored files. ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New ``bzr ignore PATTERN`` command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. File may be in a branch other than the working directory. * ``bzr log`` and ``bzr root`` can be given an http URL instead of a filename. * Commands can now be defined by external programs or scripts in a directory on $BZRPATH. * New "stat cache" avoids reading the contents of files if they haven't changed since the previous time. * If the Python interpreter is too old, try to find a better one or give an error. Based on a patch from Fredrik Lundh. * New optional parameter ``bzr info [BRANCH]``. * New form ``bzr commit SELECTED`` to commit only selected files. BUG FIXES: * Fixed diff format so that added and removed files will be handled properly by patch. Fix from Lalo Martins. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. * New developer commands ``added``, ``modified``. PORTABILITY: * Cope on Windows on python2.3 by using the weaker random seed. 2.4 is now only recommended. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline TODO data 10159 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * ``bzr log DIR`` should give changes to any files within DIR. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. * Maybe also store directories in the statcache so that we can quickly identify that they still exist. Medium things ------------- * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Merge Aaron's merge code. * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. We should be able to just get the id for the selected files, look up their location and diff just those files. No need to traverse the entire inventories. * ``bzr status DIR`` or ``bzr diff DIR`` should report on all changes under that directory. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. - Selected-file commit - Impossible selected-file commit: adding things in non-versioned directories, crossing renames, etc. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Should be a signature at the top of the cache file. * Paranoid mode where we never trust SHA-1 matches. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` M 644 inline bzrlib/commands.py data 35035 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0], lock_mode='r') file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.', lock_mode='r') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, specific_files=file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from branch import find_branch b = find_branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') # FIXME: probably doesn't handle non-ascii patterns if os.path.exists(ifn): f = b.controlfile(ifn, 'rt') igns = f.read() f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' f = AtomicFile(ifn, 'wt') f.write(igns) f.commit() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/commit.py data 7976 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def commit(branch, message, timestamp=None, timezone=None, committer=None, verbose=True, specific_files=None): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. specific_files If true, commit only those files. """ import os, time, tempfile from inventory import Inventory from osutils import isdir, isfile, sha_string, quotefn, \ local_time_offset, username, kind_marker, is_inside_any from branch import gen_file_id from errors import BzrError from revision import Revision from trace import mutter, note branch._need_writelock() ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_tree = branch.working_tree() work_inv = work_tree.inventory inv = Inventory() basis = branch.basis_tree() basis_inv = basis.inventory missing_ids = [] print 'looking for changes...' for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = branch.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if specific_files and not is_inside_any(specific_files, path): if basis_inv.has_id(file_id): # carry over with previous state inv.add(basis_inv[file_id].copy()) else: # omit this from committed inventory pass continue if not work_tree.has_id(file_id): note('deleted %s%s' % (path, kind_marker(entry.kind))) mutter(" file is missing, removing from inventory") missing_ids.append(file_id) continue inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: raise BzrError("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): raise BzrError("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): raise BzrError("%s is entered as file but is not a file" % quotefn(p)) new_sha1 = work_tree.get_file_sha1(file_id) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and old_ie.text_sha1 == new_sha1): ## assert content == basis.get_file(file_id).read() entry.text_id = old_ie.text_id entry.text_sha1 = new_sha1 entry.text_size = old_ie.text_size mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) entry.text_id = gen_file_id(entry.name) branch.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if not old_ie: note('added %s' % path) elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): note('modified %s' % path) else: note('renamed %s' % path) for file_id in missing_ids: # Any files that have been deleted are now removed from the # working inventory. Files that were not selected for commit # are left as they were in the working inventory and ommitted # from the revision inventory. # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itbranch. if work_inv.has_id(file_id): del work_inv[file_id] inv_id = rev_id = _gen_revision_id(time.time()) inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) branch.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) branch._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = branch.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) branch.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (branch.revno() + 1)) branch.append_revision(rev_id) note("commited r%d" % branch.revno()) def _gen_revision_id(when): """Return new revision-id.""" from binascii import hexlify from osutils import rand_bytes, compact_date, user_email s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s commit refs/heads/master mark :492 committer Martin Pool 1115945852 +1000 data 23 - more notes on tagging from :491 M 644 inline doc/tagging.txt data 3395 ******* Tagging ******* It is useful to be able to point to particular revisions on a branch by symbolic names, rather than revision numbers or hashes. Proposal: just use branches --------------------------- This is probably the simplest model. In both Subversion and Arch, tags are basically just branches that do not differ from their parent revision. This has a few advantages: * We do not need to introduce another type of entity, which can simplify both understanding and implementation. We also do not need a special * If branches are cheap (through shared storage, etc) then tags are also appropriately cheap. * The general idea is familiar to people from those two systems (and probably some others). * People often do put tags in the wrong place, and need to update them, or want tags that move along from one state to another (e.g. ``STABLE``.) Tags-as-branches capture this through the usual versioning mechanism, rather than needing to invent a parallel versioning system for tags. There are some problems: * Users want tags to stay fixed, but branches are things that can be committed-to. So at one level there is a mismatch. * In particular, it can be easy to accidentally commit onto a tag branch rather than a "branch branch", and that commit can easily be 'lost' and never integrated back to the right place. One possible resolution is to have a voluntary write-protect bit on a branch, which prevents accidental updates. (Similar to the unix owner write bit.) When it is necessary to update the branch, that bit can be removed or bypassed. This is an alternative to the Arch ``--seal`` mechanism. Proposal: tags within a branch ------------------------------ You can place tags on a branch as shorthand for a particular revision:: bzr tag rel3.14.18 bzr branch foo-project--main@rel3.14.18 Tags are alphanumeric identifiers that do not begin with a digit. Tags will cover an entire revision, not particular files. Another term sometimes used is "labels"; I think we're close enough to CVS's "tags" that it's good to be consistent. However, it does possibly clash with Arch's ``tag`` command and ``id-tagging-method`` (sheesh). In Subversion a tag is just a branch you don't commit to. You *can* work this way in Bazaar if you want to. (And until tags are implemented, this will be the way to do it.) I'm not sure if tags should be versioned objects or not. Options: * Tags are added or changed by a commit; they mark previous revisions but are only visible when looking from a later commit. This has the fairly strange effect that if you check out a previous tag, you can't see the tag anymore (or you see the value it previously had.) * Tags are not versioned; if you move them they're gone. * Tags exist within their own versioning space. It is useful to have mutable tags, in case they're incorrectly placed or need to be updated. At the same time we do not want to lose history. I think in this model it is not helpful to update tags within revisions. One approach would be to version tags within a separate namespace, so | STABLE.0 | STABLE.1 | STABLE.2 as just STABLE it finds the most recent tag. Should tags propagate across merges, and if so how? Implementation Plan ------------------- Add tags as non-versioned objects, that for the moment do not propagate across branches. commit refs/heads/master mark :493 committer Martin Pool 1116124564 +1000 data 29 - Merge aaron's merge command from :492 M 644 inline bzrlib/changeset.py data 53368 # Copyright (C) 2004 Aaron Bentley # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os.path import errno import patch import stat """ Represent and apply a changeset """ __docformat__ = "restructuredtext" NULL_ID = "!NULL" def invert_dict(dict): newdict = {} for (key,value) in dict.iteritems(): newdict[value] = key return newdict class PatchApply: """Patch application as a kind of content change""" def __init__(self, contents): """Constructor. :param contents: The text of the patch to apply :type contents: str""" self.contents = contents def __eq__(self, other): if not isinstance(other, PatchApply): return False elif self.contents != other.contents: return False else: return True def __ne__(self, other): return not (self == other) def apply(self, filename, conflict_handler, reverse=False): """Applies the patch to the specified file. :param filename: the file to apply the patch to :type filename: str :param reverse: If true, apply the patch in reverse :type reverse: bool """ input_name = filename+".orig" try: os.rename(filename, input_name) except OSError, e: if e.errno != errno.ENOENT: raise if conflict_handler.patch_target_missing(filename, self.contents)\ == "skip": return os.rename(filename, input_name) status = patch.patch(self.contents, input_name, filename, reverse) os.chmod(filename, os.stat(input_name).st_mode) if status == 0: os.unlink(input_name) elif status == 1: conflict_handler.failed_hunks(filename) class ChangeUnixPermissions: """This is two-way change, suitable for file modification, creation, deletion""" def __init__(self, old_mode, new_mode): self.old_mode = old_mode self.new_mode = new_mode def apply(self, filename, conflict_handler, reverse=False): if not reverse: from_mode = self.old_mode to_mode = self.new_mode else: from_mode = self.new_mode to_mode = self.old_mode try: current_mode = os.stat(filename).st_mode &0777 except OSError, e: if e.errno == errno.ENOENT: if conflict_handler.missing_for_chmod(filename) == "skip": return else: current_mode = from_mode if from_mode is not None and current_mode != from_mode: if conflict_handler.wrong_old_perms(filename, from_mode, current_mode) != "continue": return if to_mode is not None: try: os.chmod(filename, to_mode) except IOError, e: if e.errno == errno.ENOENT: conflict_handler.missing_for_chmod(filename) def __eq__(self, other): if not isinstance(other, ChangeUnixPermissions): return False elif self.old_mode != other.old_mode: return False elif self.new_mode != other.new_mode: return False else: return True def __ne__(self, other): return not (self == other) def dir_create(filename, conflict_handler, reverse): """Creates the directory, or deletes it if reverse is true. Intended to be used with ReplaceContents. :param filename: The name of the directory to create :type filename: str :param reverse: If true, delete the directory, instead :type reverse: bool """ if not reverse: try: os.mkdir(filename) except OSError, e: if e.errno != errno.EEXIST: raise if conflict_handler.dir_exists(filename) == "continue": os.mkdir(filename) except IOError, e: if e.errno == errno.ENOENT: if conflict_handler.missing_parent(filename)=="continue": file(filename, "wb").write(self.contents) else: try: os.rmdir(filename) except OSError, e: if e.errno != 39: raise if conflict_handler.rmdir_non_empty(filename) == "skip": return os.rmdir(filename) class SymlinkCreate: """Creates or deletes a symlink (for use with ReplaceContents)""" def __init__(self, contents): """Constructor. :param contents: The filename of the target the symlink should point to :type contents: str """ self.target = contents def __call__(self, filename, conflict_handler, reverse): """Creates or destroys the symlink. :param filename: The name of the symlink to create :type filename: str """ if reverse: assert(os.readlink(filename) == self.target) os.unlink(filename) else: try: os.symlink(self.target, filename) except OSError, e: if e.errno != errno.EEXIST: raise if conflict_handler.link_name_exists(filename) == "continue": os.symlink(self.target, filename) def __eq__(self, other): if not isinstance(other, SymlinkCreate): return False elif self.target != other.target: return False else: return True def __ne__(self, other): return not (self == other) class FileCreate: """Create or delete a file (for use with ReplaceContents)""" def __init__(self, contents): """Constructor :param contents: The contents of the file to write :type contents: str """ self.contents = contents def __repr__(self): return "FileCreate(%i b)" % len(self.contents) def __eq__(self, other): if not isinstance(other, FileCreate): return False elif self.contents != other.contents: return False else: return True def __ne__(self, other): return not (self == other) def __call__(self, filename, conflict_handler, reverse): """Create or delete a file :param filename: The name of the file to create :type filename: str :param reverse: Delete the file instead of creating it :type reverse: bool """ if not reverse: try: file(filename, "wb").write(self.contents) except IOError, e: if e.errno == errno.ENOENT: if conflict_handler.missing_parent(filename)=="continue": file(filename, "wb").write(self.contents) else: raise else: try: if (file(filename, "rb").read() != self.contents): direction = conflict_handler.wrong_old_contents(filename, self.contents) if direction != "continue": return os.unlink(filename) except IOError, e: if e.errno != errno.ENOENT: raise if conflict_handler.missing_for_rm(filename, undo) == "skip": return def reversed(sequence): max = len(sequence) - 1 for i in range(len(sequence)): yield sequence[max - i] class ReplaceContents: """A contents-replacement framework. It allows a file/directory/symlink to be created, deleted, or replaced with another file/directory/symlink. Arguments must be callable with (filename, reverse). """ def __init__(self, old_contents, new_contents): """Constructor. :param old_contents: The change to reverse apply (e.g. a deletion), \ when going forwards. :type old_contents: `dir_create`, `SymlinkCreate`, `FileCreate`, \ NoneType, etc. :param new_contents: The second change to apply (e.g. a creation), \ when going forwards. :type new_contents: `dir_create`, `SymlinkCreate`, `FileCreate`, \ NoneType, etc. """ self.old_contents=old_contents self.new_contents=new_contents def __repr__(self): return "ReplaceContents(%r -> %r)" % (self.old_contents, self.new_contents) def __eq__(self, other): if not isinstance(other, ReplaceContents): return False elif self.old_contents != other.old_contents: return False elif self.new_contents != other.new_contents: return False else: return True def __ne__(self, other): return not (self == other) def apply(self, filename, conflict_handler, reverse=False): """Applies the FileReplacement to the specified filename :param filename: The name of the file to apply changes to :type filename: str :param reverse: If true, apply the change in reverse :type reverse: bool """ if not reverse: undo = self.old_contents perform = self.new_contents else: undo = self.new_contents perform = self.old_contents mode = None if undo is not None: try: mode = os.lstat(filename).st_mode if stat.S_ISLNK(mode): mode = None except OSError, e: if e.errno != errno.ENOENT: raise if conflict_handler.missing_for_rm(filename, undo) == "skip": return undo(filename, conflict_handler, reverse=True) if perform is not None: perform(filename, conflict_handler, reverse=False) if mode is not None: os.chmod(filename, mode) class ApplySequence: def __init__(self, changes=None): self.changes = [] if changes is not None: self.changes.extend(changes) def __eq__(self, other): if not isinstance(other, ApplySequence): return False elif len(other.changes) != len(self.changes): return False else: for i in range(len(self.changes)): if self.changes[i] != other.changes[i]: return False return True def __ne__(self, other): return not (self == other) def apply(self, filename, conflict_handler, reverse=False): if not reverse: iter = self.changes else: iter = reversed(self.changes) for change in iter: change.apply(filename, conflict_handler, reverse) class Diff3Merge: def __init__(self, base_file, other_file): self.base_file = base_file self.other_file = other_file def __eq__(self, other): if not isinstance(other, Diff3Merge): return False return (self.base_file == other.base_file and self.other_file == other.other_file) def __ne__(self, other): return not (self == other) def apply(self, filename, conflict_handler, reverse=False): new_file = filename+".new" if not reverse: base = self.base_file other = self.other_file else: base = self.other_file other = self.base_file status = patch.diff3(new_file, filename, base, other) if status == 0: os.chmod(new_file, os.stat(filename).st_mode) os.rename(new_file, filename) return else: assert(status == 1) conflict_handler.merge_conflict(new_file, filename, base, other) def CreateDir(): """Convenience function to create a directory. :return: A ReplaceContents that will create a directory :rtype: `ReplaceContents` """ return ReplaceContents(None, dir_create) def DeleteDir(): """Convenience function to delete a directory. :return: A ReplaceContents that will delete a directory :rtype: `ReplaceContents` """ return ReplaceContents(dir_create, None) def CreateFile(contents): """Convenience fucntion to create a file. :param contents: The contents of the file to create :type contents: str :return: A ReplaceContents that will create a file :rtype: `ReplaceContents` """ return ReplaceContents(None, FileCreate(contents)) def DeleteFile(contents): """Convenience fucntion to delete a file. :param contents: The contents of the file to delete :type contents: str :return: A ReplaceContents that will delete a file :rtype: `ReplaceContents` """ return ReplaceContents(FileCreate(contents), None) def ReplaceFileContents(old_contents, new_contents): """Convenience fucntion to replace the contents of a file. :param old_contents: The contents of the file to replace :type old_contents: str :param new_contents: The contents to replace the file with :type new_contents: str :return: A ReplaceContents that will replace the contents of a file a file :rtype: `ReplaceContents` """ return ReplaceContents(FileCreate(old_contents), FileCreate(new_contents)) def CreateSymlink(target): """Convenience fucntion to create a symlink. :param target: The path the link should point to :type target: str :return: A ReplaceContents that will delete a file :rtype: `ReplaceContents` """ return ReplaceContents(None, SymlinkCreate(target)) def DeleteSymlink(target): """Convenience fucntion to delete a symlink. :param target: The path the link should point to :type target: str :return: A ReplaceContents that will delete a file :rtype: `ReplaceContents` """ return ReplaceContents(SymlinkCreate(target), None) def ChangeTarget(old_target, new_target): """Convenience fucntion to change the target of a symlink. :param old_target: The current link target :type old_target: str :param new_target: The new link target to use :type new_target: str :return: A ReplaceContents that will delete a file :rtype: `ReplaceContents` """ return ReplaceContents(SymlinkCreate(old_target), SymlinkCreate(new_target)) class InvalidEntry(Exception): """Raise when a ChangesetEntry is invalid in some way""" def __init__(self, entry, problem): """Constructor. :param entry: The invalid ChangesetEntry :type entry: `ChangesetEntry` :param problem: The problem with the entry :type problem: str """ msg = "Changeset entry for %s (%s) is invalid.\n%s" % (entry.id, entry.path, problem) Exception.__init__(self, msg) self.entry = entry class SourceRootHasName(InvalidEntry): """This changeset entry has a name other than "", but its parent is !NULL""" def __init__(self, entry, name): """Constructor. :param entry: The invalid ChangesetEntry :type entry: `ChangesetEntry` :param name: The name of the entry :type name: str """ msg = 'Child of !NULL is named "%s", not "./.".' % name InvalidEntry.__init__(self, entry, msg) class NullIDAssigned(InvalidEntry): """The id !NULL was assigned to a real entry""" def __init__(self, entry): """Constructor. :param entry: The invalid ChangesetEntry :type entry: `ChangesetEntry` """ msg = '"!NULL" id assigned to a file "%s".' % entry.path InvalidEntry.__init__(self, entry, msg) class ParentIDIsSelf(InvalidEntry): """An entry is marked as its own parent""" def __init__(self, entry): """Constructor. :param entry: The invalid ChangesetEntry :type entry: `ChangesetEntry` """ msg = 'file %s has "%s" id for both self id and parent id.' % \ (entry.path, entry.id) InvalidEntry.__init__(self, entry, msg) class ChangesetEntry(object): """An entry the changeset""" def __init__(self, id, parent, path): """Constructor. Sets parent and name assuming it was not renamed/created/deleted. :param id: The id associated with the entry :param parent: The id of the parent of this entry (or !NULL if no parent) :param path: The file path relative to the tree root of this entry """ self.id = id self.path = path self.new_path = path self.parent = parent self.new_parent = parent self.contents_change = None self.metadata_change = None if parent == NULL_ID and path !='./.': raise SourceRootHasName(self, path) if self.id == NULL_ID: raise NullIDAssigned(self) if self.id == self.parent: raise ParentIDIsSelf(self) def __str__(self): return "ChangesetEntry(%s)" % self.id def __get_dir(self): if self.path is None: return None return os.path.dirname(self.path) def __set_dir(self, dir): self.path = os.path.join(dir, os.path.basename(self.path)) dir = property(__get_dir, __set_dir) def __get_name(self): if self.path is None: return None return os.path.basename(self.path) def __set_name(self, name): self.path = os.path.join(os.path.dirname(self.path), name) name = property(__get_name, __set_name) def __get_new_dir(self): if self.new_path is None: return None return os.path.dirname(self.new_path) def __set_new_dir(self, dir): self.new_path = os.path.join(dir, os.path.basename(self.new_path)) new_dir = property(__get_new_dir, __set_new_dir) def __get_new_name(self): if self.new_path is None: return None return os.path.basename(self.new_path) def __set_new_name(self, name): self.new_path = os.path.join(os.path.dirname(self.new_path), name) new_name = property(__get_new_name, __set_new_name) def needs_rename(self): """Determines whether the entry requires renaming. :rtype: bool """ return (self.parent != self.new_parent or self.name != self.new_name) def is_deletion(self, reverse): """Return true if applying the entry would delete a file/directory. :param reverse: if true, the changeset is being applied in reverse :rtype: bool """ return ((self.new_parent is None and not reverse) or (self.parent is None and reverse)) def is_creation(self, reverse): """Return true if applying the entry would create a file/directory. :param reverse: if true, the changeset is being applied in reverse :rtype: bool """ return ((self.parent is None and not reverse) or (self.new_parent is None and reverse)) def is_creation_or_deletion(self): """Return true if applying the entry would create or delete a file/directory. :rtype: bool """ return self.parent is None or self.new_parent is None def get_cset_path(self, mod=False): """Determine the path of the entry according to the changeset. :param changeset: The changeset to derive the path from :type changeset: `Changeset` :param mod: If true, generate the MOD path. Otherwise, generate the \ ORIG path. :return: the path of the entry, or None if it did not exist in the \ requested tree. :rtype: str or NoneType """ if mod: if self.new_parent == NULL_ID: return "./." elif self.new_parent is None: return None return self.new_path else: if self.parent == NULL_ID: return "./." elif self.parent is None: return None return self.path def summarize_name(self, changeset, reverse=False): """Produce a one-line summary of the filename. Indicates renames as old => new, indicates creation as None => new, indicates deletion as old => None. :param changeset: The changeset to get paths from :type changeset: `Changeset` :param reverse: If true, reverse the names in the output :type reverse: bool :rtype: str """ orig_path = self.get_cset_path(False) mod_path = self.get_cset_path(True) if orig_path is not None: orig_path = orig_path[2:] if mod_path is not None: mod_path = mod_path[2:] if orig_path == mod_path: return orig_path else: if not reverse: return "%s => %s" % (orig_path, mod_path) else: return "%s => %s" % (mod_path, orig_path) def get_new_path(self, id_map, changeset, reverse=False): """Determine the full pathname to rename to :param id_map: The map of ids to filenames for the tree :type id_map: Dictionary :param changeset: The changeset to get data from :type changeset: `Changeset` :param reverse: If true, we're applying the changeset in reverse :type reverse: bool :rtype: str """ if reverse: parent = self.parent to_dir = self.dir from_dir = self.new_dir to_name = self.name from_name = self.new_name else: parent = self.new_parent to_dir = self.new_dir from_dir = self.dir to_name = self.new_name from_name = self.name if to_name is None: return None if parent == NULL_ID or parent is None: if to_name != '.': raise SourceRootHasName(self, to_name) else: return '.' if from_dir == to_dir: dir = os.path.dirname(id_map[self.id]) else: parent_entry = changeset.entries[parent] dir = parent_entry.get_new_path(id_map, changeset, reverse) if from_name == to_name: name = os.path.basename(id_map[self.id]) else: name = to_name assert(from_name is None or from_name == os.path.basename(id_map[self.id])) return os.path.join(dir, name) def is_boring(self): """Determines whether the entry does nothing :return: True if the entry does no renames or content changes :rtype: bool """ if self.contents_change is not None: return False elif self.metadata_change is not None: return False elif self.parent != self.new_parent: return False elif self.name != self.new_name: return False else: return True def apply(self, filename, conflict_handler, reverse=False): """Applies the file content and/or metadata changes. :param filename: the filename of the entry :type filename: str :param reverse: If true, apply the changes in reverse :type reverse: bool """ if self.is_deletion(reverse) and self.metadata_change is not None: self.metadata_change.apply(filename, conflict_handler, reverse) if self.contents_change is not None: self.contents_change.apply(filename, conflict_handler, reverse) if not self.is_deletion(reverse) and self.metadata_change is not None: self.metadata_change.apply(filename, conflict_handler, reverse) class IDPresent(Exception): def __init__(self, id): msg = "Cannot add entry because that id has already been used:\n%s" %\ id Exception.__init__(self, msg) self.id = id class Changeset: """A set of changes to apply""" def __init__(self): self.entries = {} def add_entry(self, entry): """Add an entry to the list of entries""" if self.entries.has_key(entry.id): raise IDPresent(entry.id) self.entries[entry.id] = entry def my_sort(sequence, key, reverse=False): """A sort function that supports supplying a key for comparison :param sequence: The sequence to sort :param key: A callable object that returns the values to be compared :param reverse: If true, sort in reverse order :type reverse: bool """ def cmp_by_key(entry_a, entry_b): if reverse: tmp=entry_a entry_a = entry_b entry_b = tmp return cmp(key(entry_a), key(entry_b)) sequence.sort(cmp_by_key) def get_rename_entries(changeset, inventory, reverse): """Return a list of entries that will be renamed. Entries are sorted from longest to shortest source path and from shortest to longest target path. :param changeset: The changeset to look in :type changeset: `Changeset` :param inventory: The source of current tree paths for the given ids :type inventory: Dictionary :param reverse: If true, the changeset is being applied in reverse :type reverse: bool :return: source entries and target entries as a tuple :rtype: (List, List) """ source_entries = [x for x in changeset.entries.itervalues() if x.needs_rename()] # these are done from longest path to shortest, to avoid deleting a # parent before its children are deleted/renamed def longest_to_shortest(entry): path = inventory.get(entry.id) if path is None: return 0 else: return len(path) my_sort(source_entries, longest_to_shortest, reverse=True) target_entries = source_entries[:] # These are done from shortest to longest path, to avoid creating a # child before its parent has been created/renamed def shortest_to_longest(entry): path = entry.get_new_path(inventory, changeset, reverse) if path is None: return 0 else: return len(path) my_sort(target_entries, shortest_to_longest) return (source_entries, target_entries) def rename_to_temp_delete(source_entries, inventory, dir, conflict_handler, reverse): """Delete and rename entries as appropriate. Entries are renamed to temp names. A map of id -> temp name is returned. :param source_entries: The entries to rename and delete :type source_entries: List of `ChangesetEntry` :param inventory: The map of id -> filename in the current tree :type inventory: Dictionary :param dir: The directory to apply changes to :type dir: str :param reverse: Apply changes in reverse :type reverse: bool :return: a mapping of id to temporary name :rtype: Dictionary """ temp_dir = os.path.join(dir, "temp") temp_name = {} for i in range(len(source_entries)): entry = source_entries[i] if entry.is_deletion(reverse): path = os.path.join(dir, inventory[entry.id]) entry.apply(path, conflict_handler, reverse) else: to_name = temp_dir+"/"+str(i) src_path = inventory.get(entry.id) if src_path is not None: src_path = os.path.join(dir, src_path) try: os.rename(src_path, to_name) temp_name[entry.id] = to_name except OSError, e: if e.errno != errno.ENOENT: raise if conflict_handler.missing_for_rename(src_path) == "skip": continue return temp_name def rename_to_new_create(temp_name, target_entries, inventory, changeset, dir, conflict_handler, reverse): """Rename entries with temp names to their final names, create new files. :param temp_name: A mapping of id to temporary name :type temp_name: Dictionary :param target_entries: The entries to apply changes to :type target_entries: List of `ChangesetEntry` :param changeset: The changeset to apply :type changeset: `Changeset` :param dir: The directory to apply changes to :type dir: str :param reverse: If true, apply changes in reverse :type reverse: bool """ for entry in target_entries: new_path = entry.get_new_path(inventory, changeset, reverse) if new_path is None: continue new_path = os.path.join(dir, new_path) old_path = temp_name.get(entry.id) if os.path.exists(new_path): if conflict_handler.target_exists(entry, new_path, old_path) == \ "skip": continue if entry.is_creation(reverse): entry.apply(new_path, conflict_handler, reverse) else: if old_path is None: continue try: os.rename(old_path, new_path) except OSError, e: raise Exception ("%s is missing" % new_path) class TargetExists(Exception): def __init__(self, entry, target): msg = "The path %s already exists" % target Exception.__init__(self, msg) self.entry = entry self.target = target class RenameConflict(Exception): def __init__(self, id, this_name, base_name, other_name): msg = """Trees all have different names for a file this: %s base: %s other: %s id: %s""" % (this_name, base_name, other_name, id) Exception.__init__(self, msg) self.this_name = this_name self.base_name = base_name self_other_name = other_name class MoveConflict(Exception): def __init__(self, id, this_parent, base_parent, other_parent): msg = """The file is in different directories in every tree this: %s base: %s other: %s id: %s""" % (this_parent, base_parent, other_parent, id) Exception.__init__(self, msg) self.this_parent = this_parent self.base_parent = base_parent self_other_parent = other_parent class MergeConflict(Exception): def __init__(self, this_path): Exception.__init__(self, "Conflict applying changes to %s" % this_path) self.this_path = this_path class MergePermissionConflict(Exception): def __init__(self, this_path, base_path, other_path): this_perms = os.stat(this_path).st_mode & 0755 base_perms = os.stat(base_path).st_mode & 0755 other_perms = os.stat(other_path).st_mode & 0755 msg = """Conflicting permission for %s this: %o base: %o other: %o """ % (this_path, this_perms, base_perms, other_perms) self.this_path = this_path self.base_path = base_path self.other_path = other_path Exception.__init__(self, msg) class WrongOldContents(Exception): def __init__(self, filename): msg = "Contents mismatch deleting %s" % filename self.filename = filename Exception.__init__(self, msg) class WrongOldPermissions(Exception): def __init__(self, filename, old_perms, new_perms): msg = "Permission missmatch on %s:\n" \ "Expected 0%o, got 0%o." % (filename, old_perms, new_perms) self.filename = filename Exception.__init__(self, msg) class RemoveContentsConflict(Exception): def __init__(self, filename): msg = "Conflict deleting %s, which has different contents in BASE"\ " and THIS" % filename self.filename = filename Exception.__init__(self, msg) class DeletingNonEmptyDirectory(Exception): def __init__(self, filename): msg = "Trying to remove dir %s while it still had files" % filename self.filename = filename Exception.__init__(self, msg) class PatchTargetMissing(Exception): def __init__(self, filename): msg = "Attempt to patch %s, which does not exist" % filename Exception.__init__(self, msg) self.filename = filename class MissingPermsFile(Exception): def __init__(self, filename): msg = "Attempt to change permissions on %s, which does not exist" %\ filename Exception.__init__(self, msg) self.filename = filename class MissingForRm(Exception): def __init__(self, filename): msg = "Attempt to remove missing path %s" % filename Exception.__init__(self, msg) self.filename = filename class MissingForRename(Exception): def __init__(self, filename): msg = "Attempt to move missing path %s" % (filename) Exception.__init__(self, msg) self.filename = filename class ExceptionConflictHandler: def __init__(self, dir): self.dir = dir def missing_parent(self, pathname): parent = os.path.dirname(pathname) raise Exception("Parent directory missing for %s" % pathname) def dir_exists(self, pathname): raise Exception("Directory already exists for %s" % pathname) def failed_hunks(self, pathname): raise Exception("Failed to apply some hunks for %s" % pathname) def target_exists(self, entry, target, old_path): raise TargetExists(entry, target) def rename_conflict(self, id, this_name, base_name, other_name): raise RenameConflict(id, this_name, base_name, other_name) def move_conflict(self, id, inventory): this_dir = inventory.this.get_dir(id) base_dir = inventory.base.get_dir(id) other_dir = inventory.other.get_dir(id) raise MoveConflict(id, this_dir, base_dir, other_dir) def merge_conflict(self, new_file, this_path, base_path, other_path): os.unlink(new_file) raise MergeConflict(this_path) def permission_conflict(self, this_path, base_path, other_path): raise MergePermissionConflict(this_path, base_path, other_path) def wrong_old_contents(self, filename, expected_contents): raise WrongOldContents(filename) def rem_contents_conflict(self, filename, this_contents, base_contents): raise RemoveContentsConflict(filename) def wrong_old_perms(self, filename, old_perms, new_perms): raise WrongOldPermissions(filename, old_perms, new_perms) def rmdir_non_empty(self, filename): raise DeletingNonEmptyDirectory(filename) def link_name_exists(self, filename): raise TargetExists(filename) def patch_target_missing(self, filename, contents): raise PatchTargetMissing(filename) def missing_for_chmod(self, filename): raise MissingPermsFile(filename) def missing_for_rm(self, filename, change): raise MissingForRm(filename) def missing_for_rename(self, filename): raise MissingForRename(filename) def apply_changeset(changeset, inventory, dir, conflict_handler=None, reverse=False): """Apply a changeset to a directory. :param changeset: The changes to perform :type changeset: `Changeset` :param inventory: The mapping of id to filename for the directory :type inventory: Dictionary :param dir: The path of the directory to apply the changes to :type dir: str :param reverse: If true, apply the changes in reverse :type reverse: bool :return: The mapping of the changed entries :rtype: Dictionary """ if conflict_handler is None: conflict_handler = ExceptionConflictHandler(dir) temp_dir = dir+"/temp" os.mkdir(temp_dir) #apply changes that don't affect filenames for entry in changeset.entries.itervalues(): if not entry.is_creation_or_deletion(): path = os.path.join(dir, inventory[entry.id]) entry.apply(path, conflict_handler, reverse) # Apply renames in stages, to minimize conflicts: # Only files whose name or parent change are interesting, because their # target name may exist in the source tree. If a directory's name changes, # that doesn't make its children interesting. (source_entries, target_entries) = get_rename_entries(changeset, inventory, reverse) temp_name = rename_to_temp_delete(source_entries, inventory, dir, conflict_handler, reverse) rename_to_new_create(temp_name, target_entries, inventory, changeset, dir, conflict_handler, reverse) os.rmdir(temp_dir) r_inventory = invert_dict(inventory) new_entries, removed_entries = get_inventory_change(inventory, r_inventory, changeset, reverse) new_inventory = {} for path, file_id in new_entries.iteritems(): new_inventory[file_id] = path for file_id in removed_entries: new_inventory[file_id] = None return new_inventory def apply_changeset_tree(cset, tree, reverse=False): r_inventory = {} for entry in tree.source_inventory().itervalues(): inventory[entry.id] = entry.path new_inventory = apply_changeset(cset, r_inventory, tree.root, reverse=reverse) new_entries, remove_entries = \ get_inventory_change(inventory, new_inventory, cset, reverse) tree.update_source_inventory(new_entries, remove_entries) def get_inventory_change(inventory, new_inventory, cset, reverse=False): new_entries = {} remove_entries = [] r_inventory = invert_dict(inventory) r_new_inventory = invert_dict(new_inventory) for entry in cset.entries.itervalues(): if entry.needs_rename(): old_path = r_inventory.get(entry.id) if old_path is not None: remove_entries.append(old_path) else: new_path = entry.get_new_path(inventory, cset) if new_path is not None: new_entries[new_path] = entry.id return new_entries, remove_entries def print_changeset(cset): """Print all non-boring changeset entries :param cset: The changeset to print :type cset: `Changeset` """ for entry in cset.entries.itervalues(): if entry.is_boring(): continue print entry.id print entry.summarize_name(cset) class CompositionFailure(Exception): def __init__(self, old_entry, new_entry, problem): msg = "Unable to conpose entries.\n %s" % problem Exception.__init__(self, msg) class IDMismatch(CompositionFailure): def __init__(self, old_entry, new_entry): problem = "Attempt to compose entries with different ids: %s and %s" %\ (old_entry.id, new_entry.id) CompositionFailure.__init__(self, old_entry, new_entry, problem) def compose_changesets(old_cset, new_cset): """Combine two changesets into one. This works well for exact patching. Otherwise, not so well. :param old_cset: The first changeset that would be applied :type old_cset: `Changeset` :param new_cset: The second changeset that would be applied :type new_cset: `Changeset` :return: A changeset that combines the changes in both changesets :rtype: `Changeset` """ composed = Changeset() for old_entry in old_cset.entries.itervalues(): new_entry = new_cset.entries.get(old_entry.id) if new_entry is None: composed.add_entry(old_entry) else: composed_entry = compose_entries(old_entry, new_entry) if composed_entry.parent is not None or\ composed_entry.new_parent is not None: composed.add_entry(composed_entry) for new_entry in new_cset.entries.itervalues(): if not old_cset.entries.has_key(new_entry.id): composed.add_entry(new_entry) return composed def compose_entries(old_entry, new_entry): """Combine two entries into one. :param old_entry: The first entry that would be applied :type old_entry: ChangesetEntry :param old_entry: The second entry that would be applied :type old_entry: ChangesetEntry :return: A changeset entry combining both entries :rtype: `ChangesetEntry` """ if old_entry.id != new_entry.id: raise IDMismatch(old_entry, new_entry) output = ChangesetEntry(old_entry.id, old_entry.parent, old_entry.path) if (old_entry.parent != old_entry.new_parent or new_entry.parent != new_entry.new_parent): output.new_parent = new_entry.new_parent if (old_entry.path != old_entry.new_path or new_entry.path != new_entry.new_path): output.new_path = new_entry.new_path output.contents_change = compose_contents(old_entry, new_entry) output.metadata_change = compose_metadata(old_entry, new_entry) return output def compose_contents(old_entry, new_entry): """Combine the contents of two changeset entries. Entries are combined intelligently where possible, but the fallback behavior returns an ApplySequence. :param old_entry: The first entry that would be applied :type old_entry: `ChangesetEntry` :param new_entry: The second entry that would be applied :type new_entry: `ChangesetEntry` :return: A combined contents change :rtype: anything supporting the apply(reverse=False) method """ old_contents = old_entry.contents_change new_contents = new_entry.contents_change if old_entry.contents_change is None: return new_entry.contents_change elif new_entry.contents_change is None: return old_entry.contents_change elif isinstance(old_contents, ReplaceContents) and \ isinstance(new_contents, ReplaceContents): if old_contents.old_contents == new_contents.new_contents: return None else: return ReplaceContents(old_contents.old_contents, new_contents.new_contents) elif isinstance(old_contents, ApplySequence): output = ApplySequence(old_contents.changes) if isinstance(new_contents, ApplySequence): output.changes.extend(new_contents.changes) else: output.changes.append(new_contents) return output elif isinstance(new_contents, ApplySequence): output = ApplySequence((old_contents.changes,)) output.extend(new_contents.changes) return output else: return ApplySequence((old_contents, new_contents)) def compose_metadata(old_entry, new_entry): old_meta = old_entry.metadata_change new_meta = new_entry.metadata_change if old_meta is None: return new_meta elif new_meta is None: return old_meta elif isinstance(old_meta, ChangeUnixPermissions) and \ isinstance(new_meta, ChangeUnixPermissions): return ChangeUnixPermissions(old_meta.old_mode, new_meta.new_mode) else: return ApplySequence(old_meta, new_meta) def changeset_is_null(changeset): for entry in changeset.entries.itervalues(): if not entry.is_boring(): return False return True class UnsuppportedFiletype(Exception): def __init__(self, full_path, stat_result): msg = "The file \"%s\" is not a supported filetype." % full_path Exception.__init__(self, msg) self.full_path = full_path self.stat_result = stat_result def generate_changeset(tree_a, tree_b, inventory_a=None, inventory_b=None): return ChangesetGenerator(tree_a, tree_b, inventory_a, inventory_b)() class ChangesetGenerator(object): def __init__(self, tree_a, tree_b, inventory_a=None, inventory_b=None): object.__init__(self) self.tree_a = tree_a self.tree_b = tree_b if inventory_a is not None: self.inventory_a = inventory_a else: self.inventory_a = tree_a.inventory() if inventory_b is not None: self.inventory_b = inventory_b else: self.inventory_b = tree_b.inventory() self.r_inventory_a = self.reverse_inventory(self.inventory_a) self.r_inventory_b = self.reverse_inventory(self.inventory_b) def reverse_inventory(self, inventory): r_inventory = {} for entry in inventory.itervalues(): if entry.id is None: continue r_inventory[entry.id] = entry return r_inventory def __call__(self): cset = Changeset() for entry in self.inventory_a.itervalues(): if entry.id is None: continue cs_entry = self.make_entry(entry.id) if cs_entry is not None and not cs_entry.is_boring(): cset.add_entry(cs_entry) for entry in self.inventory_b.itervalues(): if entry.id is None: continue if not self.r_inventory_a.has_key(entry.id): cs_entry = self.make_entry(entry.id) if cs_entry is not None and not cs_entry.is_boring(): cset.add_entry(cs_entry) for entry in list(cset.entries.itervalues()): if entry.parent != entry.new_parent: if not cset.entries.has_key(entry.parent) and\ entry.parent != NULL_ID and entry.parent is not None: parent_entry = self.make_boring_entry(entry.parent) cset.add_entry(parent_entry) if not cset.entries.has_key(entry.new_parent) and\ entry.new_parent != NULL_ID and \ entry.new_parent is not None: parent_entry = self.make_boring_entry(entry.new_parent) cset.add_entry(parent_entry) return cset def get_entry_parent(self, entry, inventory): if entry is None: return None if entry.path == "./.": return NULL_ID dirname = os.path.dirname(entry.path) if dirname == ".": dirname = "./." parent = inventory[dirname] return parent.id def get_paths(self, entry, tree): if entry is None: return (None, None) full_path = tree.readonly_path(entry.id) if entry.path == ".": return ("", full_path) return (entry.path, full_path) def make_basic_entry(self, id, only_interesting): entry_a = self.r_inventory_a.get(id) entry_b = self.r_inventory_b.get(id) if only_interesting and not self.is_interesting(entry_a, entry_b): return (None, None, None) parent = self.get_entry_parent(entry_a, self.inventory_a) (path, full_path_a) = self.get_paths(entry_a, self.tree_a) cs_entry = ChangesetEntry(id, parent, path) new_parent = self.get_entry_parent(entry_b, self.inventory_b) (new_path, full_path_b) = self.get_paths(entry_b, self.tree_b) cs_entry.new_path = new_path cs_entry.new_parent = new_parent return (cs_entry, full_path_a, full_path_b) def is_interesting(self, entry_a, entry_b): if entry_a is not None: if entry_a.interesting: return True if entry_b is not None: if entry_b.interesting: return True return False def make_boring_entry(self, id): (cs_entry, full_path_a, full_path_b) = \ self.make_basic_entry(id, only_interesting=False) if cs_entry.is_creation_or_deletion(): return self.make_entry(id, only_interesting=False) else: return cs_entry def make_entry(self, id, only_interesting=True): (cs_entry, full_path_a, full_path_b) = \ self.make_basic_entry(id, only_interesting) if cs_entry is None: return None stat_a = self.lstat(full_path_a) stat_b = self.lstat(full_path_b) if stat_b is None: cs_entry.new_parent = None cs_entry.new_path = None cs_entry.metadata_change = self.make_mode_change(stat_a, stat_b) cs_entry.contents_change = self.make_contents_change(full_path_a, stat_a, full_path_b, stat_b) return cs_entry def make_mode_change(self, stat_a, stat_b): mode_a = None if stat_a is not None and not stat.S_ISLNK(stat_a.st_mode): mode_a = stat_a.st_mode & 0777 mode_b = None if stat_b is not None and not stat.S_ISLNK(stat_b.st_mode): mode_b = stat_b.st_mode & 0777 if mode_a == mode_b: return None return ChangeUnixPermissions(mode_a, mode_b) def make_contents_change(self, full_path_a, stat_a, full_path_b, stat_b): if stat_a is None and stat_b is None: return None if None not in (stat_a, stat_b) and stat.S_ISDIR(stat_a.st_mode) and\ stat.S_ISDIR(stat_b.st_mode): return None if None not in (stat_a, stat_b) and stat.S_ISREG(stat_a.st_mode) and\ stat.S_ISREG(stat_b.st_mode): if stat_a.st_ino == stat_b.st_ino and \ stat_a.st_dev == stat_b.st_dev: return None if file(full_path_a, "rb").read() == \ file(full_path_b, "rb").read(): return None patch_contents = patch.diff(full_path_a, file(full_path_b, "rb").read()) if patch_contents is None: return None return PatchApply(patch_contents) a_contents = self.get_contents(stat_a, full_path_a) b_contents = self.get_contents(stat_b, full_path_b) if a_contents == b_contents: return None return ReplaceContents(a_contents, b_contents) def get_contents(self, stat_result, full_path): if stat_result is None: return None elif stat.S_ISREG(stat_result.st_mode): return FileCreate(file(full_path, "rb").read()) elif stat.S_ISDIR(stat_result.st_mode): return dir_create elif stat.S_ISLNK(stat_result.st_mode): return SymlinkCreate(os.readlink(full_path)) else: raise UnsupportedFiletype(full_path, stat_result) def lstat(self, full_path): stat_result = None if full_path is not None: try: stat_result = os.lstat(full_path) except OSError, e: if e.errno != errno.ENOENT: raise return stat_result def full_path(entry, tree): return os.path.join(tree.root, entry.path) def new_delete_entry(entry, tree, inventory, delete): if entry.path == "": parent = NULL_ID else: parent = inventory[dirname(entry.path)].id cs_entry = ChangesetEntry(parent, entry.path) if delete: cs_entry.new_path = None cs_entry.new_parent = None else: cs_entry.path = None cs_entry.parent = None full_path = full_path(entry, tree) status = os.lstat(full_path) if stat.S_ISDIR(file_stat.st_mode): action = dir_create class Inventory: def __init__(self, inventory): self.inventory = inventory self.rinventory = None def get_rinventory(self): if self.rinventory is None: self.rinventory = invert_dict(self.inventory) return self.rinventory def get_path(self, id): return self.inventory.get(id) def get_name(self, id): return os.path.basename(self.get_path(id)) def get_dir(self, id): path = self.get_path(id) if path == "": return None return os.path.dirname(path) def get_parent(self, id): directory = self.get_dir(id) if directory == '.': directory = './.' if directory is None: return NULL_ID return self.get_rinventory().get(directory) M 644 inline bzrlib/merge.py data 8165 from merge_core import merge_flex from changeset import generate_changeset, ExceptionConflictHandler from changeset import Inventory from bzrlib import Branch import bzrlib.osutils from trace import mutter import os.path import tempfile import shutil import errno class MergeConflictHandler(ExceptionConflictHandler): """Handle conflicts encountered while merging""" def copy(self, source, dest): """Copy the text and mode of a file :param source: The path of the file to copy :param dest: The distination file to create """ s_file = file(source, "rb") d_file = file(dest, "wb") for line in s_file: d_file.write(line) os.chmod(dest, 0777 & os.stat(source).st_mode) def add_suffix(self, name, suffix, last_new_name=None): """Rename a file to append a suffix. If the new name exists, the suffix is added repeatedly until a non-existant name is found :param name: The path of the file :param suffix: The suffix to append :param last_new_name: (used for recursive calls) the last name tried """ if last_new_name is None: last_new_name = name new_name = last_new_name+suffix try: os.rename(name, new_name) except OSError, e: if e.errno != errno.EEXIST and e.errno != errno.ENOTEMPTY: raise self.add_suffix(name, suffix, last_new_name=new_name) def merge_conflict(self, new_file, this_path, base_path, other_path): """ Handle diff3 conflicts by producing a .THIS, .BASE and .OTHER. The main file will be a version with diff3 conflicts. :param new_file: Path to the output file with diff3 markers :param this_path: Path to the file text for the THIS tree :param base_path: Path to the file text for the BASE tree :param other_path: Path to the file text for the OTHER tree """ self.add_suffix(this_path, ".THIS") self.copy(base_path, this_path+".BASE") self.copy(other_path, this_path+".OTHER") os.rename(new_file, this_path) def target_exists(self, entry, target, old_path): """Handle the case when the target file or dir exists""" self.add_suffix(target, ".moved") class SourceFile: def __init__(self, path, id, present=None, isdir=None): self.path = path self.id = id self.present = present self.isdir = isdir self.interesting = True def __repr__(self): return "SourceFile(%s, %s)" % (self.path, self.id) def get_tree(treespec, temp_root, label): dir, revno = treespec branch = Branch(dir) if revno is None: base_tree = branch.working_tree() elif revno == -1: base_tree = branch.basis_tree() else: base_tree = branch.revision_tree(branch.lookup_revision(revno)) temp_path = os.path.join(temp_root, label) os.mkdir(temp_path) return MergeTree(base_tree, temp_path) def abspath(tree, file_id): path = tree.inventory.id2path(file_id) if path == "": return "./." return "./" + path def file_exists(tree, file_id): return tree.has_filename(tree.id2path(file_id)) def inventory_map(tree): inventory = {} for file_id in tree.inventory: if not file_exists(tree, file_id): continue path = abspath(tree, file_id) inventory[path] = SourceFile(path, file_id) return inventory class MergeTree(object): def __init__(self, tree, tempdir): object.__init__(self) if hasattr(tree, "basedir"): self.root = tree.basedir else: self.root = None self.inventory = inventory_map(tree) self.tree = tree self.tempdir = tempdir os.mkdir(os.path.join(self.tempdir, "texts")) self.cached = {} def readonly_path(self, id): if self.root is not None: return self.tree.abspath(self.tree.id2path(id)) else: if self.tree.inventory[id].kind in ("directory", "root_directory"): return self.tempdir if not self.cached.has_key(id): path = os.path.join(self.tempdir, "texts", id) outfile = file(path, "wb") outfile.write(self.tree.get_file(id).read()) assert(os.path.exists(path)) self.cached[id] = path return self.cached[id] def merge(other_revision, base_revision): tempdir = tempfile.mkdtemp(prefix="bzr-") try: this_branch = Branch('.') other_tree = get_tree(other_revision, tempdir, "other") base_tree = get_tree(base_revision, tempdir, "base") merge_inner(this_branch, other_tree, base_tree, tempdir) finally: shutil.rmtree(tempdir) def generate_cset_optimized(tree_a, tree_b, inventory_a, inventory_b): """Generate a changeset, using the text_id to mark really-changed files. This permits blazing comparisons when text_ids are present. It also disables metadata comparison for files with identical texts. """ for file_id in tree_a.tree.inventory: if file_id not in tree_b.tree.inventory: continue entry_a = tree_a.tree.inventory[file_id] entry_b = tree_b.tree.inventory[file_id] if (entry_a.kind, entry_b.kind) != ("file", "file"): continue if None in (entry_a.text_id, entry_b.text_id): continue if entry_a.text_id != entry_b.text_id: continue inventory_a[abspath(tree_a.tree, file_id)].interesting = False inventory_b[abspath(tree_b.tree, file_id)].interesting = False cset = generate_changeset(tree_a, tree_b, inventory_a, inventory_b) for entry in cset.entries.itervalues(): entry.metadata_change = None return cset def merge_inner(this_branch, other_tree, base_tree, tempdir): this_tree = get_tree(('.', None), tempdir, "this") def get_inventory(tree): return tree.inventory inv_changes = merge_flex(this_tree, base_tree, other_tree, generate_cset_optimized, get_inventory, MergeConflictHandler(base_tree.root)) adjust_ids = [] for id, path in inv_changes.iteritems(): if path is not None: if path == '.': path = '' else: assert path.startswith('./') path = path[2:] adjust_ids.append((path, id)) this_branch.set_inventory(regen_inventory(this_branch, this_tree.root, adjust_ids)) def regen_inventory(this_branch, root, new_entries): old_entries = this_branch.read_working_inventory() new_inventory = {} by_path = {} for file_id in old_entries: entry = old_entries[file_id] path = old_entries.id2path(file_id) new_inventory[file_id] = (path, file_id, entry.parent_id, entry.kind) by_path[path] = file_id deletions = 0 insertions = 0 new_path_list = [] for path, file_id in new_entries: if path is None: del new_inventory[file_id] deletions += 1 else: new_path_list.append((path, file_id)) if file_id not in old_entries: insertions += 1 # Ensure no file is added before its parent new_path_list.sort() for path, file_id in new_path_list: if path == '': parent = None else: parent = by_path[os.path.dirname(path)] kind = bzrlib.osutils.file_kind(os.path.join(root, path)) new_inventory[file_id] = (path, file_id, parent, kind) by_path[path] = file_id # Get a list in insertion order new_inventory_list = new_inventory.values() mutter ("""Inventory regeneration: old length: %i insertions: %i deletions: %i new_length: %i"""\ % (len(old_entries), insertions, deletions, len(new_inventory_list))) assert len(new_inventory_list) == len(old_entries) + insertions - deletions new_inventory_list.sort() return new_inventory_list M 644 inline bzrlib/merge_core.py data 19475 import changeset from changeset import Inventory, apply_changeset, invert_dict import os.path class ThreewayInventory: def __init__(self, this_inventory, base_inventory, other_inventory): self.this = this_inventory self.base = base_inventory self.other = other_inventory def invert_invent(inventory): invert_invent = {} for key, value in inventory.iteritems(): invert_invent[value.id] = key return invert_invent def make_inv(inventory): return Inventory(invert_invent(inventory)) def merge_flex(this, base, other, changeset_function, inventory_function, conflict_handler): this_inventory = inventory_function(this) base_inventory = inventory_function(base) other_inventory = inventory_function(other) inventory = ThreewayInventory(make_inv(this_inventory), make_inv(base_inventory), make_inv(other_inventory)) cset = changeset_function(base, other, base_inventory, other_inventory) new_cset = make_merge_changeset(cset, inventory, this, base, other, conflict_handler) return apply_changeset(new_cset, invert_invent(this_inventory), this.root, conflict_handler, False) def make_merge_changeset(cset, inventory, this, base, other, conflict_handler=None): new_cset = changeset.Changeset() def get_this_contents(id): path = os.path.join(this.root, inventory.this.get_path(id)) if os.path.isdir(path): return changeset.dir_create else: return changeset.FileCreate(file(path, "rb").read()) for entry in cset.entries.itervalues(): if entry.is_boring(): new_cset.add_entry(entry) elif entry.is_creation(False): if inventory.this.get_path(entry.id) is None: new_cset.add_entry(entry) else: this_contents = get_this_contents(entry.id) other_contents = entry.contents_change.new_contents if other_contents == this_contents: boring_entry = changeset.ChangesetEntry(entry.id, entry.new_parent, entry.new_path) new_cset.add_entry(boring_entry) else: conflict_handler.contents_conflict(this_contents, other_contents) elif entry.is_deletion(False): if inventory.this.get_path(entry.id) is None: boring_entry = changeset.ChangesetEntry(entry.id, entry.parent, entry.path) new_cset.add_entry(boring_entry) elif entry.contents_change is not None: this_contents = get_this_contents(entry.id) base_contents = entry.contents_change.old_contents if base_contents == this_contents: new_cset.add_entry(entry) else: entry_path = inventory.this.get_path(entry.id) conflict_handler.rem_contents_conflict(entry_path, this_contents, base_contents) else: new_cset.add_entry(entry) else: entry = get_merge_entry(entry, inventory, base, other, conflict_handler) if entry is not None: new_cset.add_entry(entry) return new_cset def get_merge_entry(entry, inventory, base, other, conflict_handler): this_name = inventory.this.get_name(entry.id) this_parent = inventory.this.get_parent(entry.id) this_dir = inventory.this.get_dir(entry.id) if this_dir is None: this_dir = "" if this_name is None: return conflict_handler.merge_missing(entry.id, inventory) base_name = inventory.base.get_name(entry.id) base_parent = inventory.base.get_parent(entry.id) base_dir = inventory.base.get_dir(entry.id) if base_dir is None: base_dir = "" other_name = inventory.other.get_name(entry.id) other_parent = inventory.other.get_parent(entry.id) other_dir = inventory.base.get_dir(entry.id) if other_dir is None: other_dir = "" if base_name == other_name: old_name = this_name new_name = this_name else: if this_name != base_name and this_name != other_name: conflict_handler.rename_conflict(entry.id, this_name, base_name, other_name) else: old_name = this_name new_name = other_name if base_parent == other_parent: old_parent = this_parent new_parent = this_parent old_dir = this_dir new_dir = this_dir else: if this_parent != base_parent and this_parent != other_parent: conflict_handler.move_conflict(entry.id, inventory) else: old_parent = this_parent old_dir = this_dir new_parent = other_parent new_dir = other_dir old_path = os.path.join(old_dir, old_name) new_entry = changeset.ChangesetEntry(entry.id, old_parent, old_name) if new_name is not None or new_parent is not None: new_entry.new_path = os.path.join(new_dir, new_name) else: new_entry.new_path = None new_entry.new_parent = new_parent base_path = base.readonly_path(entry.id) other_path = other.readonly_path(entry.id) if entry.contents_change is not None: new_entry.contents_change = changeset.Diff3Merge(base_path, other_path) if entry.metadata_change is not None: new_entry.metadata_change = PermissionsMerge(base_path, other_path) return new_entry class PermissionsMerge: def __init__(self, base_path, other_path): self.base_path = base_path self.other_path = other_path def apply(self, filename, conflict_handler, reverse=False): if not reverse: base = self.base_path other = self.other_path else: base = self.other_path other = self.base_path base_stat = os.stat(base).st_mode other_stat = os.stat(other).st_mode this_stat = os.stat(filename).st_mode if base_stat &0777 == other_stat &0777: return elif this_stat &0777 == other_stat &0777: return elif this_stat &0777 == base_stat &0777: os.chmod(filename, other_stat) else: conflict_handler.permission_conflict(filename, base, other) import unittest import tempfile import shutil class MergeTree: def __init__(self, dir): self.dir = dir; os.mkdir(dir) self.inventory = {'0': ""} def child_path(self, parent, name): return os.path.join(self.inventory[parent], name) def add_file(self, id, parent, name, contents, mode): path = self.child_path(parent, name) full_path = self.abs_path(path) assert not os.path.exists(full_path) file(full_path, "wb").write(contents) os.chmod(self.abs_path(path), mode) self.inventory[id] = path def add_dir(self, id, parent, name, mode): path = self.child_path(parent, name) full_path = self.abs_path(path) assert not os.path.exists(full_path) os.mkdir(self.abs_path(path)) os.chmod(self.abs_path(path), mode) self.inventory[id] = path def abs_path(self, path): return os.path.join(self.dir, path) def full_path(self, id): return self.abs_path(self.inventory[id]) def change_path(self, id, path): new = os.path.join(self.dir, self.inventory[id]) os.rename(self.abs_path(self.inventory[id]), self.abs_path(path)) self.inventory[id] = path class MergeBuilder: def __init__(self): self.dir = tempfile.mkdtemp(prefix="BaZing") self.base = MergeTree(os.path.join(self.dir, "base")) self.this = MergeTree(os.path.join(self.dir, "this")) self.other = MergeTree(os.path.join(self.dir, "other")) self.cset = changeset.Changeset() self.cset.add_entry(changeset.ChangesetEntry("0", changeset.NULL_ID, "./.")) def get_cset_path(self, parent, name): if name is None: assert (parent is None) return None return os.path.join(self.cset.entries[parent].path, name) def add_file(self, id, parent, name, contents, mode): self.base.add_file(id, parent, name, contents, mode) self.this.add_file(id, parent, name, contents, mode) self.other.add_file(id, parent, name, contents, mode) path = self.get_cset_path(parent, name) self.cset.add_entry(changeset.ChangesetEntry(id, parent, path)) def add_dir(self, id, parent, name, mode): path = self.get_cset_path(parent, name) self.base.add_dir(id, parent, name, mode) self.cset.add_entry(changeset.ChangesetEntry(id, parent, path)) self.this.add_dir(id, parent, name, mode) self.other.add_dir(id, parent, name, mode) def change_name(self, id, base=None, this=None, other=None): if base is not None: self.change_name_tree(id, self.base, base) self.cset.entries[id].name = base if this is not None: self.change_name_tree(id, self.this, this) if other is not None: self.change_name_tree(id, self.other, other) self.cset.entries[id].new_name = other def change_parent(self, id, base=None, this=None, other=None): if base is not None: self.change_parent_tree(id, self.base, base) self.cset.entries[id].parent = base self.cset.entries[id].dir = self.cset.entries[base].path if this is not None: self.change_parent_tree(id, self.this, this) if other is not None: self.change_parent_tree(id, self.other, other) self.cset.entries[id].new_parent = other self.cset.entries[id].new_dir = \ self.cset.entries[other].new_path def change_contents(self, id, base=None, this=None, other=None): if base is not None: self.change_contents_tree(id, self.base, base) if this is not None: self.change_contents_tree(id, self.this, this) if other is not None: self.change_contents_tree(id, self.other, other) if base is not None or other is not None: old_contents = file(self.base.full_path(id)).read() new_contents = file(self.other.full_path(id)).read() contents = changeset.ReplaceFileContents(old_contents, new_contents) self.cset.entries[id].contents_change = contents def change_perms(self, id, base=None, this=None, other=None): if base is not None: self.change_perms_tree(id, self.base, base) if this is not None: self.change_perms_tree(id, self.this, this) if other is not None: self.change_perms_tree(id, self.other, other) if base is not None or other is not None: old_perms = os.stat(self.base.full_path(id)).st_mode &077 new_perms = os.stat(self.other.full_path(id)).st_mode &077 contents = changeset.ChangeUnixPermissions(old_perms, new_perms) self.cset.entries[id].metadata_change = contents def change_name_tree(self, id, tree, name): new_path = tree.child_path(self.cset.entries[id].parent, name) tree.change_path(id, new_path) def change_parent_tree(self, id, tree, parent): new_path = tree.child_path(parent, self.cset.entries[id].name) tree.change_path(id, new_path) def change_contents_tree(self, id, tree, contents): path = tree.full_path(id) mode = os.stat(path).st_mode file(path, "w").write(contents) os.chmod(path, mode) def change_perms_tree(self, id, tree, mode): os.chmod(tree.full_path(id), mode) def merge_changeset(self): all_inventory = ThreewayInventory(Inventory(self.this.inventory), Inventory(self.base.inventory), Inventory(self.other.inventory)) conflict_handler = changeset.ExceptionConflictHandler(self.this.dir) return make_merge_changeset(self.cset, all_inventory, self.this.dir, self.base.dir, self.other.dir, conflict_handler) def apply_changeset(self, cset, conflict_handler=None, reverse=False): self.this.inventory = \ changeset.apply_changeset(cset, self.this.inventory, self.this.dir, conflict_handler, reverse) def cleanup(self): shutil.rmtree(self.dir) class MergeTest(unittest.TestCase): def test_change_name(self): """Test renames""" builder = MergeBuilder() builder.add_file("1", "0", "name1", "hello1", 0755) builder.change_name("1", other="name2") builder.add_file("2", "0", "name3", "hello2", 0755) builder.change_name("2", base="name4") builder.add_file("3", "0", "name5", "hello3", 0755) builder.change_name("3", this="name6") cset = builder.merge_changeset() assert(cset.entries["2"].is_boring()) assert(cset.entries["1"].name == "name1") assert(cset.entries["1"].new_name == "name2") assert(cset.entries["3"].is_boring()) for tree in (builder.this, builder.other, builder.base): assert(tree.dir != builder.dir and tree.dir.startswith(builder.dir)) for path in tree.inventory.itervalues(): fullpath = tree.abs_path(path) assert(fullpath.startswith(tree.dir)) assert(not path.startswith(tree.dir)) assert os.path.exists(fullpath) builder.apply_changeset(cset) builder.cleanup() builder = MergeBuilder() builder.add_file("1", "0", "name1", "hello1", 0644) builder.change_name("1", other="name2", this="name3") self.assertRaises(changeset.RenameConflict, builder.merge_changeset) builder.cleanup() def test_file_moves(self): """Test moves""" builder = MergeBuilder() builder.add_dir("1", "0", "dir1", 0755) builder.add_dir("2", "0", "dir2", 0755) builder.add_file("3", "1", "file1", "hello1", 0644) builder.add_file("4", "1", "file2", "hello2", 0644) builder.add_file("5", "1", "file3", "hello3", 0644) builder.change_parent("3", other="2") assert(Inventory(builder.other.inventory).get_parent("3") == "2") builder.change_parent("4", this="2") assert(Inventory(builder.this.inventory).get_parent("4") == "2") builder.change_parent("5", base="2") assert(Inventory(builder.base.inventory).get_parent("5") == "2") cset = builder.merge_changeset() for id in ("1", "2", "4", "5"): assert(cset.entries[id].is_boring()) assert(cset.entries["3"].parent == "1") assert(cset.entries["3"].new_parent == "2") builder.apply_changeset(cset) builder.cleanup() builder = MergeBuilder() builder.add_dir("1", "0", "dir1", 0755) builder.add_dir("2", "0", "dir2", 0755) builder.add_dir("3", "0", "dir3", 0755) builder.add_file("4", "1", "file1", "hello1", 0644) builder.change_parent("4", other="2", this="3") self.assertRaises(changeset.MoveConflict, builder.merge_changeset) builder.cleanup() def test_contents_merge(self): """Test diff3 merging""" builder = MergeBuilder() builder.add_file("1", "0", "name1", "text1", 0755) builder.change_contents("1", other="text4") builder.add_file("2", "0", "name3", "text2", 0655) builder.change_contents("2", base="text5") builder.add_file("3", "0", "name5", "text3", 0744) builder.change_contents("3", this="text6") cset = builder.merge_changeset() assert(cset.entries["1"].contents_change is not None) assert(isinstance(cset.entries["1"].contents_change, changeset.Diff3Merge)) assert(isinstance(cset.entries["2"].contents_change, changeset.Diff3Merge)) assert(cset.entries["3"].is_boring()) builder.apply_changeset(cset) assert(file(builder.this.full_path("1"), "rb").read() == "text4" ) assert(file(builder.this.full_path("2"), "rb").read() == "text2" ) assert(os.stat(builder.this.full_path("1")).st_mode &0777 == 0755) assert(os.stat(builder.this.full_path("2")).st_mode &0777 == 0655) assert(os.stat(builder.this.full_path("3")).st_mode &0777 == 0744) builder.cleanup() builder = MergeBuilder() builder.add_file("1", "0", "name1", "text1", 0755) builder.change_contents("1", other="text4", this="text3") cset = builder.merge_changeset() self.assertRaises(changeset.MergeConflict, builder.apply_changeset, cset) builder.cleanup() def test_perms_merge(self): builder = MergeBuilder() builder.add_file("1", "0", "name1", "text1", 0755) builder.change_perms("1", other=0655) builder.add_file("2", "0", "name2", "text2", 0755) builder.change_perms("2", base=0655) builder.add_file("3", "0", "name3", "text3", 0755) builder.change_perms("3", this=0655) cset = builder.merge_changeset() assert(cset.entries["1"].metadata_change is not None) assert(isinstance(cset.entries["1"].metadata_change, PermissionsMerge)) assert(isinstance(cset.entries["2"].metadata_change, PermissionsMerge)) assert(cset.entries["3"].is_boring()) builder.apply_changeset(cset) assert(os.stat(builder.this.full_path("1")).st_mode &0777 == 0655) assert(os.stat(builder.this.full_path("2")).st_mode &0777 == 0755) assert(os.stat(builder.this.full_path("3")).st_mode &0777 == 0655) builder.cleanup(); builder = MergeBuilder() builder.add_file("1", "0", "name1", "text1", 0755) builder.change_perms("1", other=0655, base=0555) cset = builder.merge_changeset() self.assertRaises(changeset.MergePermissionConflict, builder.apply_changeset, cset) builder.cleanup() def test(): changeset_suite = unittest.makeSuite(MergeTest, 'test_') runner = unittest.TextTestRunner() runner.run(changeset_suite) if __name__ == "__main__": test() M 644 inline bzrlib/patch.py data 2604 import os import popen2 """ Diff and patch functionality """ __docformat__ = "restructuredtext" def patch(patch_contents, filename, output_filename=None, reverse=False): """Apply a patch to a file, to produce another output file. This is should be suitable for our limited purposes. :param patch_contents: The contents of the patch to apply :type patch_contents: str :param filename: the name of the file to apply the patch to :type filename: str :param output_filename: The filename to produce. If None, file is \ modified in-place :type output_filename: str or NoneType :param reverse: If true, apply the patch in reverse :type reverse: bool :return: 0 on success, 1 if some hunks failed """ args = ["patch", "-f", "-s", "--posix", "--binary"] if reverse: args.append("--reverse") if output_filename is not None: args.extend(("-o", output_filename)) args.append(filename) process = popen2.Popen3(args, bufsize=len(patch_contents)) process.tochild.write(patch_contents) process.tochild.close() status = os.WEXITSTATUS(process.wait()) return status def diff(orig_file, mod_str, orig_label=None, mod_label=None): """Compare two files, and produce a patch. :param orig_file: path to the old file :type orig_file: str :param mod_str: Contents of the new file :type mod_str: str :param orig_label: The label to use for the old file :type orig_label: str :param mod_label: The label to use for the new file :type mod_label: str """ args = ["diff", "-u" ] if orig_label is not None and mod_label is not None: args.extend(("-L", orig_label, "-L", mod_label)) args.extend(("--", orig_file, "-")) process = popen2.Popen3(args, bufsize=len(mod_str)) process.tochild.write(mod_str) process.tochild.close() patch = process.fromchild.read() status = os.WEXITSTATUS(process.wait()) if status == 0: return None else: return patch def diff3(out_file, mine_path, older_path, yours_path): def add_label(args, label): args.extend(("-L", label)) args = ['diff3', "-E", "--merge"] add_label(args, "TREE") add_label(args, "ANCESTOR") add_label(args, "MERGE-SOURCE") args.extend((mine_path, older_path, yours_path)) process = popen2.Popen4(args) process.tochild.close() output = process.fromchild.read() status = os.WEXITSTATUS(process.wait()) if status not in (0, 1): raise Exception(output) file(out_file, "wb").write(output) return status M 644 inline bzrlib/add.py data 3060 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, sys import bzrlib from osutils import quotefn, appendpath from errors import bailout from trace import mutter, note def smart_add(file_list, verbose=False, recurse=True): """Add files to version, optionall recursing into directories. This is designed more towards DWIM for humans than API simplicity. For the specific behaviour see the help for cmd_add(). """ assert file_list user_list = file_list[:] assert not isinstance(file_list, basestring) b = bzrlib.branch.Branch(file_list[0], find_root=True) inv = b.read_working_inventory() tree = b.working_tree() count = 0 for f in file_list: kind = bzrlib.osutils.file_kind(f) if kind != 'file' and kind != 'directory': if f not in user_list: print "Skipping %s (can't add file of kind '%s')" % (f, kind) continue bailout("can't add file of kind %r" % kind) rf = b.relpath(f) af = b.abspath(rf) ## TODO: It's OK to add root but only in recursive mode bzrlib.mutter("smart add of %r" % f) if bzrlib.branch.is_control_file(af): bailout("cannot add control file %r" % af) versioned = (inv.path2id(rf) != None) if rf == '': mutter("branch root doesn't need to be added") elif versioned: mutter("%r is already versioned" % f) else: file_id = bzrlib.branch.gen_file_id(rf) inv.add_path(rf, kind=kind, file_id=file_id) bzrlib.mutter("added %r kind %r file_id={%s}" % (rf, kind, file_id)) count += 1 if verbose: bzrlib.textui.show_status('A', kind, quotefn(f)) if kind == 'directory' and recurse: for subf in os.listdir(af): subp = appendpath(rf, subf) if subf == bzrlib.BZRDIR: mutter("skip control directory %r" % subp) elif tree.is_ignored(subp): mutter("skip ignored sub-file %r" % subp) else: mutter("queue to add sub-file %r" % (subp)) file_list.append(subp) if count > 0: if verbose: note('added %d' % count) b._write_inventory(inv) M 644 inline bzrlib/branch.py data 26580 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. base Base directory of the branch. """ _lockmode = None def __init__(self, base, init=False, find_root=True, lock_mode='w'): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.lock(lock_mode) self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def lock(self, mode='w'): """Lock the on-disk branch, excluding other processes.""" try: import fcntl, errno if mode == 'w': lm = fcntl.LOCK_EX om = os.O_WRONLY | os.O_CREAT elif mode == 'r': lm = fcntl.LOCK_SH om = os.O_RDONLY else: raise BzrError("invalid locking mode %r" % mode) try: lockfile = os.open(self.controlfilename('branch-lock'), om) except OSError, e: if e.errno == errno.ENOENT: # might not exist on branches from <0.0.4 self.controlfile('branch-lock', 'w').close() lockfile = os.open(self.controlfilename('branch-lock'), om) else: raise e fcntl.lockf(lockfile, lm) def unlock(): fcntl.lockf(lockfile, fcntl.LOCK_UN) os.close(lockfile) self._lockmode = None self.unlock = unlock self._lockmode = mode except ImportError: warning("please write a locking method for platform %r" % sys.platform) def unlock(): self._lockmode = None self.unlock = unlock self._lockmode = mode def _need_readlock(self): if self._lockmode not in ['r', 'w']: raise BzrError('need read lock on branch, only have %r' % self._lockmode) def _need_writelock(self): if self._lockmode not in ['w']: raise BzrError('need write lock on branch, only have %r' % self._lockmode) def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" self._need_readlock() before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self._need_writelock() ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ self._need_writelock() # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" self._need_readlock() tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability self._need_writelock() if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" self._need_readlock() r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" self._need_readlock() i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" self._need_readlock() if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self._need_readlock() return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise BzrError('invalid history direction %r' % direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def commit(self, *args, **kw): """Deprecated""" from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" self._need_readlock() if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self._need_writelock() tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self._need_writelock() ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/commands.py data 35596 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date from bzrlib import merge def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0], lock_mode='r') file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.', lock_mode='r') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, specific_files=file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from branch import find_branch b = find_branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') # FIXME: probably doesn't handle non-ascii patterns if os.path.exists(ifn): f = b.controlfile(ifn, 'rt') igns = f.read() f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' f = AtomicFile(ifn, 'wt') f.write(igns) f.commit() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): merge.merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :494 committer Martin Pool 1116208456 +1000 data 55 - commit takes an optional caller-specified revision id from :493 M 644 inline bzrlib/commit.py data 8038 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def commit(branch, message, timestamp=None, timezone=None, committer=None, verbose=True, specific_files=None, rev_id=None): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. specific_files If true, commit only those files. """ import os, time, tempfile from inventory import Inventory from osutils import isdir, isfile, sha_string, quotefn, \ local_time_offset, username, kind_marker, is_inside_any from branch import gen_file_id from errors import BzrError from revision import Revision from trace import mutter, note branch._need_writelock() ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_tree = branch.working_tree() work_inv = work_tree.inventory inv = Inventory() basis = branch.basis_tree() basis_inv = basis.inventory missing_ids = [] print 'looking for changes...' for path, entry in work_inv.iter_entries(): ## TODO: Cope with files that have gone missing. ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = branch.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if specific_files and not is_inside_any(specific_files, path): if basis_inv.has_id(file_id): # carry over with previous state inv.add(basis_inv[file_id].copy()) else: # omit this from committed inventory pass continue if not work_tree.has_id(file_id): note('deleted %s%s' % (path, kind_marker(entry.kind))) mutter(" file is missing, removing from inventory") missing_ids.append(file_id) continue inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: raise BzrError("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): raise BzrError("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): raise BzrError("%s is entered as file but is not a file" % quotefn(p)) new_sha1 = work_tree.get_file_sha1(file_id) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and old_ie.text_sha1 == new_sha1): ## assert content == basis.get_file(file_id).read() entry.text_id = old_ie.text_id entry.text_sha1 = new_sha1 entry.text_size = old_ie.text_size mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) entry.text_id = gen_file_id(entry.name) branch.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if not old_ie: note('added %s' % path) elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): note('modified %s' % path) else: note('renamed %s' % path) for file_id in missing_ids: # Any files that have been deleted are now removed from the # working inventory. Files that were not selected for commit # are left as they were in the working inventory and ommitted # from the revision inventory. # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itbranch. if work_inv.has_id(file_id): del work_inv[file_id] if rev_id is None: rev_id = _gen_revision_id(time.time()) inv_id = rev_id inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) branch.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) branch._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = branch.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) branch.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (branch.revno() + 1)) branch.append_revision(rev_id) note("commited r%d" % branch.revno()) def _gen_revision_id(when): """Return new revision-id.""" from binascii import hexlify from osutils import rand_bytes, compact_date, user_email s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s commit refs/heads/master mark :495 committer Martin Pool 1116208668 +1000 data 29 - disallow slash in store ids from :494 M 644 inline bzrlib/store.py data 5386 # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Stores are the main data-storage mechanism for Bazaar-NG. A store is a simple write-once container indexed by a universally unique ID, which is typically the SHA-1 of the content.""" __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import os, tempfile, types, osutils, gzip, errno from stat import ST_SIZE from StringIO import StringIO from trace import mutter ###################################################################### # stores class StoreError(Exception): pass class ImmutableStore: """Store that holds files indexed by unique names. Files can be added, but not modified once they are in. Typically the hash is used as the name, or something else known to be unique, such as a UUID. >>> st = ImmutableScratchStore() >>> st.add(StringIO('hello'), 'aa') >>> 'aa' in st True >>> 'foo' in st False You are not allowed to add an id that is already present. Entries can be retrieved as files, which may then be read. >>> st.add(StringIO('goodbye'), '123123') >>> st['123123'].read() 'goodbye' TODO: Atomic add by writing to a temporary file and renaming. TODO: Perhaps automatically transform to/from XML in a method? Would just need to tell the constructor what class to use... TODO: Even within a simple disk store like this, we could gzip the files. But since many are less than one disk block, that might not help a lot. """ def __init__(self, basedir): """ImmutableStore constructor.""" self._basedir = basedir def _path(self, id): assert '/' not in id return os.path.join(self._basedir, id) def __repr__(self): return "%s(%r)" % (self.__class__.__name__, self._basedir) def add(self, f, fileid, compressed=True): """Add contents of a file into the store. f -- An open file, or file-like object.""" # FIXME: Only works on smallish files # TODO: Can be optimized by copying at the same time as # computing the sum. mutter("add store entry %r" % (fileid)) if isinstance(f, types.StringTypes): content = f else: content = f.read() p = self._path(fileid) if os.access(p, os.F_OK) or os.access(p + '.gz', os.F_OK): bailout("store %r already contains id %r" % (self._basedir, fileid)) if compressed: f = gzip.GzipFile(p + '.gz', 'wb') os.chmod(p + '.gz', 0444) else: f = file(p, 'wb') os.chmod(p, 0444) f.write(content) f.close() def __contains__(self, fileid): """""" p = self._path(fileid) return (os.access(p, os.R_OK) or os.access(p + '.gz', os.R_OK)) # TODO: Guard against the same thing being stored twice, compressed and uncompresse def __iter__(self): for f in os.listdir(self._basedir): if f[-3:] == '.gz': # TODO: case-insensitive? yield f[:-3] else: yield f def __len__(self): return len(os.listdir(self._basedir)) def __getitem__(self, fileid): """Returns a file reading from a particular entry.""" p = self._path(fileid) try: return gzip.GzipFile(p + '.gz', 'rb') except IOError, e: if e.errno == errno.ENOENT: return file(p, 'rb') else: raise e def total_size(self): """Return (count, bytes) This is the (compressed) size stored on disk, not the size of the content.""" total = 0 count = 0 for fid in self: count += 1 p = self._path(fid) try: total += os.stat(p)[ST_SIZE] except OSError: total += os.stat(p + '.gz')[ST_SIZE] return count, total class ImmutableScratchStore(ImmutableStore): """Self-destructing test subclass of ImmutableStore. The Store only exists for the lifetime of the Python object. Obviously you should not put anything precious in it. """ def __init__(self): ImmutableStore.__init__(self, tempfile.mkdtemp()) def __del__(self): for f in os.listdir(self._basedir): fpath = os.path.join(self._basedir, f) # needed on windows, and maybe some other filesystems os.chmod(fpath, 0600) os.remove(fpath) os.rmdir(self._basedir) mutter("%r destroyed" % self) commit refs/heads/master mark :496 committer Martin Pool 1116209953 +1000 data 70 - patch from ddaa to create api/ directory before building API docs from :495 M 644 inline .bzrignore data 121 *.diff ./doc/*.html *.py[oc] *~ .arch-ids .bzr.profile .arch-inventory {arch} CHANGELOG bzr-test.log ,,* testbzr.log api M 644 inline build-api data 88 mkdir -p api PYTHONPATH=$PWD epydoc -o api/html --docformat restructuredtext bzr bzrlib commit refs/heads/master mark :497 committer Martin Pool 1116211057 +1000 data 52 - new AtomicFile.close() aborts if appropriate - Doc from :496 M 644 inline bzrlib/atomicfile.py data 2260 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA class AtomicFile: """A file that does an atomic-rename to move into place. This also causes hardlinks to break when it's written out. Open this as for a regular file, then use commit() to move into place or abort() to cancel. An encoding can be specified; otherwise the default is ascii. """ def __init__(self, filename, mode='wb', encoding=None): if mode != 'wb' and mode != 'wt': raise ValueError("invalid AtomicFile mode %r" % mode) import os, socket self.tmpfilename = '%s.%d.%s.tmp' % (filename, os.getpid(), socket.gethostname()) self.realfilename = filename self.f = open(self.tmpfilename, mode) if encoding: import codecs self.f = codecs.EncodedFile(self.f, encoding) self.write = self.f.write self.closed = property(self.f.closed) def commit(self): """Close the file and move to final name.""" import sys, os self.f.close() if sys.platform == 'win32': os.remove(self.realfilename) os.rename(self.tmpfilename, self.realfilename) def abort(self): """Discard temporary file without committing changes.""" import os self.f.close() os.remove(self.tmpfilename) def close(self): """Discard the file unless already committed.""" if not self.closed: self.abort() commit refs/heads/master mark :498 committer Martin Pool 1116211182 +1000 data 119 bugfix for bzr ignore reported by ddaa: - read in old .bzrignore file correctly - make sure files get closed properly from :497 M 644 inline bzrlib/commands.py data 35668 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date from bzrlib import merge def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0], lock_mode='r') file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.', lock_mode='r') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, specific_files=file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from branch import find_branch b = find_branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): merge.merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: cmdobj = cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :499 committer Martin Pool 1116211279 +1000 data 37 - new bzr ignore test cases from ddaa from :498 M 644 inline testbzr data 9407 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): logfile.write('$ %s\n' % cmd) cmd = cmd.split() else: logfile.write('$ %r\n' % cmd) if cmd[0] == 'bzr': cmd[0] = BZRPATH return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open(LOGFILENAME, 'wt', buffering=1) try: mypath = os.path.abspath(sys.argv[0]) print '%-30s %s' % ('running tests from', mypath) global BZRPATH if len(sys.argv) > 1: BZRPATH = sys.argv[1] else: BZRPATH = os.path.join(os.path.split(mypath)[0], 'bzr') print '%-30s %s' % ('against bzr', BZRPATH) print '%-30s %s' % ('in directory', os.getcwd()) print print backtick([BZRPATH, 'version']) runcmd(['mkdir', TESTDIR]) cd(TESTDIR) test_root = os.getcwd() progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("internal tests") runcmd("bzr selftest") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) runcmd("bzr diff --message foo", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == 'unknown:\n test.txt\n' out = backtick("bzr status --all") assert out == "unknown:\n test.txt\n" out = backtick("bzr status test.txt --all") assert out == "unknown:\n test.txt\n" f = file('test2.txt', 'wt') f.write('goodbye cruel world...\n') f.close() out = backtick("bzr status test.txt") assert out == "unknown:\n test.txt\n" out = backtick("bzr status") assert out == ("unknown:\n" " test.txt\n" " test2.txt\n") os.unlink('test2.txt') progress("command aliases") out = backtick("bzr st --all") assert out == ("unknown:\n" " test.txt\n") out = backtick("bzr stat") assert out == ("unknown:\n" " test.txt\n") progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == ("added:\n" " test.txt\n") progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == "sub2/hello.txt\n" assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == 'sub1/sub2/hello.txt\n' assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') assert backtick('bzr relpath sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') runcmd('bzr log') runcmd('bzr log -v') cd('..') cd('..') progress('ignore patterns') mkdir('ignorebranch') cd('ignorebranch') runcmd('bzr init') assert backtick('bzr unknowns') == '' file('foo.tmp', 'wt').write('tmp files are ignored') assert backtick('bzr unknowns') == '' file('foo.c', 'wt').write('int main() {}') assert backtick('bzr unknowns') == 'foo.c\n' runcmd('bzr add foo.c') assert backtick('bzr unknowns') == '' # 'ignore' works when creating the .bzignore file file('foo.blah', 'wt').write('blah') assert backtick('bzr unknowns') == 'foo.blah\n' runcmd('bzr ignore *.blah') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\n' # 'ignore' works when then .bzrignore file already exists file('garh', 'wt').write('garh') assert backtick('bzr unknowns') == 'garh\n' runcmd('bzr ignore garh') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\ngarh\n' progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :500 committer Martin Pool 1116211474 +1000 data 49 - fix return value from run_bzr reported by mpe from :499 M 644 inline bzrlib/commands.py data 35666 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date from bzrlib import merge def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0], lock_mode='r') file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.', lock_mode='r') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, specific_files=file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from branch import find_branch b = find_branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=False, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): merge.merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :501 committer Martin Pool 1116211567 +1000 data 35 - fix verboseness in commit command from :500 M 644 inline bzrlib/commit.py data 8113 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def commit(branch, message, timestamp=None, timezone=None, committer=None, verbose=True, specific_files=None, rev_id=None): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. specific_files If true, commit only those files. """ import os, time, tempfile from inventory import Inventory from osutils import isdir, isfile, sha_string, quotefn, \ local_time_offset, username, kind_marker, is_inside_any from branch import gen_file_id from errors import BzrError from revision import Revision from trace import mutter, note branch._need_writelock() ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_tree = branch.working_tree() work_inv = work_tree.inventory inv = Inventory() basis = branch.basis_tree() basis_inv = basis.inventory missing_ids = [] if verbose: note('looking for changes...') for path, entry in work_inv.iter_entries(): ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = branch.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if specific_files and not is_inside_any(specific_files, path): if basis_inv.has_id(file_id): # carry over with previous state inv.add(basis_inv[file_id].copy()) else: # omit this from committed inventory pass continue if not work_tree.has_id(file_id): if verbose: print('deleted %s%s' % (path, kind_marker(entry.kind))) mutter(" file is missing, removing from inventory") missing_ids.append(file_id) continue inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: raise BzrError("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): raise BzrError("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): raise BzrError("%s is entered as file but is not a file" % quotefn(p)) new_sha1 = work_tree.get_file_sha1(file_id) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and old_ie.text_sha1 == new_sha1): ## assert content == basis.get_file(file_id).read() entry.text_id = old_ie.text_id entry.text_sha1 = new_sha1 entry.text_size = old_ie.text_size mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) entry.text_id = gen_file_id(entry.name) branch.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: note('added %s' % path) elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): note('modified %s' % path) else: note('renamed %s' % path) for file_id in missing_ids: # Any files that have been deleted are now removed from the # working inventory. Files that were not selected for commit # are left as they were in the working inventory and ommitted # from the revision inventory. # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itbranch. if work_inv.has_id(file_id): del work_inv[file_id] if rev_id is None: rev_id = _gen_revision_id(time.time()) inv_id = rev_id inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) branch.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) branch._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = branch.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) branch.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (branch.revno() + 1)) branch.append_revision(rev_id) if verbose: note("commited r%d" % branch.revno()) def _gen_revision_id(when): """Return new revision-id.""" from binascii import hexlify from osutils import rand_bytes, compact_date, user_email s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s commit refs/heads/master mark :502 committer Martin Pool 1116211656 +1000 data 4 todo from :501 M 644 inline TODO data 10312 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * ``bzr log DIR`` should give changes to any files within DIR. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. * Maybe also store directories in the statcache so that we can quickly identify that they still exist. Medium things ------------- * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Merge Aaron's merge code. * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. We should be able to just get the id for the selected files, look up their location and diff just those files. No need to traverse the entire inventories. * ``bzr status DIR`` or ``bzr diff DIR`` should report on all changes under that directory. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. - Selected-file commit - Impossible selected-file commit: adding things in non-versioned directories, crossing renames, etc. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Should be a signature at the top of the cache file. * Paranoid mode where we never trust SHA-1 matches. * Don't commit if there are no changes unless forced. * --dry-run mode for commit? * Generally, be a bit more verbose unless --silent is specified. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :503 committer Martin Pool 1116211678 +1000 data 35 - fix verboseness in commit command from :502 M 644 inline bzrlib/commit.py data 8116 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def commit(branch, message, timestamp=None, timezone=None, committer=None, verbose=True, specific_files=None, rev_id=None): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. specific_files If true, commit only those files. """ import os, time, tempfile from inventory import Inventory from osutils import isdir, isfile, sha_string, quotefn, \ local_time_offset, username, kind_marker, is_inside_any from branch import gen_file_id from errors import BzrError from revision import Revision from trace import mutter, note branch._need_writelock() ## TODO: Show branch names # TODO: Don't commit if there are no changes, unless forced? # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_tree = branch.working_tree() work_inv = work_tree.inventory inv = Inventory() basis = branch.basis_tree() basis_inv = basis.inventory missing_ids = [] if verbose: note('looking for changes...') for path, entry in work_inv.iter_entries(): ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = branch.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if specific_files and not is_inside_any(specific_files, path): if basis_inv.has_id(file_id): # carry over with previous state inv.add(basis_inv[file_id].copy()) else: # omit this from committed inventory pass continue if not work_tree.has_id(file_id): if verbose: print('deleted %s%s' % (path, kind_marker(entry.kind))) mutter(" file is missing, removing from inventory") missing_ids.append(file_id) continue inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: raise BzrError("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): raise BzrError("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): raise BzrError("%s is entered as file but is not a file" % quotefn(p)) new_sha1 = work_tree.get_file_sha1(file_id) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and old_ie.text_sha1 == new_sha1): ## assert content == basis.get_file(file_id).read() entry.text_id = old_ie.text_id entry.text_sha1 = new_sha1 entry.text_size = old_ie.text_size mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) entry.text_id = gen_file_id(entry.name) branch.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: print('added %s' % path) elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): print('modified %s' % path) else: print('renamed %s' % path) for file_id in missing_ids: # Any files that have been deleted are now removed from the # working inventory. Files that were not selected for commit # are left as they were in the working inventory and ommitted # from the revision inventory. # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itbranch. if work_inv.has_id(file_id): del work_inv[file_id] if rev_id is None: rev_id = _gen_revision_id(time.time()) inv_id = rev_id inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) branch.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) branch._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = branch.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) branch.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (branch.revno() + 1)) branch.append_revision(rev_id) if verbose: note("commited r%d" % branch.revno()) def _gen_revision_id(when): """Return new revision-id.""" from binascii import hexlify from osutils import rand_bytes, compact_date, user_email s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s commit refs/heads/master mark :504 committer Martin Pool 1116211796 +1000 data 3 doc from :503 M 644 inline bzrlib/commit.py data 8339 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def commit(branch, message, timestamp=None, timezone=None, committer=None, verbose=True, specific_files=None, rev_id=None): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. specific_files If true, commit only those files. rev_id If set, use this as the new revision id. Useful for test or import commands that need to tightly control what revisions are assigned. If you duplicate a revision id that exists elsewhere it is your own fault. If null (default), a time/random revision id is generated. """ import os, time, tempfile from inventory import Inventory from osutils import isdir, isfile, sha_string, quotefn, \ local_time_offset, username, kind_marker, is_inside_any from branch import gen_file_id from errors import BzrError from revision import Revision from trace import mutter, note branch._need_writelock() # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_tree = branch.working_tree() work_inv = work_tree.inventory inv = Inventory() basis = branch.basis_tree() basis_inv = basis.inventory missing_ids = [] if verbose: note('looking for changes...') for path, entry in work_inv.iter_entries(): ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = branch.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if specific_files and not is_inside_any(specific_files, path): if basis_inv.has_id(file_id): # carry over with previous state inv.add(basis_inv[file_id].copy()) else: # omit this from committed inventory pass continue if not work_tree.has_id(file_id): if verbose: print('deleted %s%s' % (path, kind_marker(entry.kind))) mutter(" file is missing, removing from inventory") missing_ids.append(file_id) continue inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: raise BzrError("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): raise BzrError("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): raise BzrError("%s is entered as file but is not a file" % quotefn(p)) new_sha1 = work_tree.get_file_sha1(file_id) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and old_ie.text_sha1 == new_sha1): ## assert content == basis.get_file(file_id).read() entry.text_id = old_ie.text_id entry.text_sha1 = new_sha1 entry.text_size = old_ie.text_size mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) entry.text_id = gen_file_id(entry.name) branch.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: print('added %s' % path) elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): print('modified %s' % path) else: print('renamed %s' % path) for file_id in missing_ids: # Any files that have been deleted are now removed from the # working inventory. Files that were not selected for commit # are left as they were in the working inventory and ommitted # from the revision inventory. # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itbranch. if work_inv.has_id(file_id): del work_inv[file_id] if rev_id is None: rev_id = _gen_revision_id(time.time()) inv_id = rev_id inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) branch.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) branch._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = branch.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) branch.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (branch.revno() + 1)) branch.append_revision(rev_id) if verbose: note("commited r%d" % branch.revno()) def _gen_revision_id(when): """Return new revision-id.""" from binascii import hexlify from osutils import rand_bytes, compact_date, user_email s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s commit refs/heads/master mark :505 committer Martin Pool 1116211833 +1000 data 30 - commit is verbose by default from :504 M 644 inline bzrlib/commands.py data 35665 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date from bzrlib import merge def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0], lock_mode='r') file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.', lock_mode='r') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, specific_files=file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from branch import find_branch b = find_branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir, find_root=False)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): merge.merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :506 committer Martin Pool 1116214727 +1000 data 4 todo from :505 M 644 inline TODO data 10461 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * ``bzr log DIR`` should give changes to any files within DIR. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. * Maybe also store directories in the statcache so that we can quickly identify that they still exist. Medium things ------------- * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Merge Aaron's merge code. * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. We should be able to just get the id for the selected files, look up their location and diff just those files. No need to traverse the entire inventories. * ``bzr status DIR`` or ``bzr diff DIR`` should report on all changes under that directory. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. - Selected-file commit - Impossible selected-file commit: adding things in non-versioned directories, crossing renames, etc. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Should be a signature at the top of the cache file. * Paranoid mode where we never trust SHA-1 matches. * Don't commit if there are no changes unless forced. * --dry-run mode for commit? * Generally, be a bit more verbose unless --silent is specified. * Tests need to run under Python2.4 to have the new subprocess utilities, but there should be a way to run bzr itself under Python2.3. Can we? Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :507 committer Martin Pool 1116216241 +1000 data 93 - new -p option for testbzr to use a different version of python to run the bzr under test from :506 M 644 inline testbzr data 10291 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. usage: testbzr [-p PYTHON] [BZR] By default this tests the copy of bzr found in the same directory as testbzr, or the first one found on the $PATH. A copy of bzr may be given on the command line to override this, for example when applying a new test suite to an old copy of bzr or vice versa. testbzr normally invokes bzr using the same version of python as it would normally use to run -- that is, the system default python, unless that is older than 2.3. The -p option allows specification of a different Python interpreter, such as when testing that bzr still works on python2.3. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" OVERRIDE_PYTHON = None LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = BZRPATH if OVERRIDE_PYTHON: cmd.insert(0, OVERRIDE_PYTHON) logfile.write('$ %r\n' % cmd) return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open(LOGFILENAME, 'wt', buffering=1) try: from getopt import getopt opts, args = getopt(sys.argv[1:], 'p:') for option, value in opts: if option == '-p': OVERRIDE_PYTHON = value mypath = os.path.abspath(sys.argv[0]) print '%-30s %s' % ('running tests from', mypath) global BZRPATH if args: BZRPATH = args[1] else: BZRPATH = os.path.join(os.path.split(mypath)[0], 'bzr') print '%-30s %s' % ('against bzr', BZRPATH) print '%-30s %s' % ('in directory', os.getcwd()) print '%-30s %s' % ('with python', (OVERRIDE_PYTHON or '(default)')) print print backtick([BZRPATH, 'version']) runcmd(['mkdir', TESTDIR]) cd(TESTDIR) test_root = os.getcwd() progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("internal tests") runcmd("bzr selftest") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) runcmd("bzr diff --message foo", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == 'unknown:\n test.txt\n' out = backtick("bzr status --all") assert out == "unknown:\n test.txt\n" out = backtick("bzr status test.txt --all") assert out == "unknown:\n test.txt\n" f = file('test2.txt', 'wt') f.write('goodbye cruel world...\n') f.close() out = backtick("bzr status test.txt") assert out == "unknown:\n test.txt\n" out = backtick("bzr status") assert out == ("unknown:\n" " test.txt\n" " test2.txt\n") os.unlink('test2.txt') progress("command aliases") out = backtick("bzr st --all") assert out == ("unknown:\n" " test.txt\n") out = backtick("bzr stat") assert out == ("unknown:\n" " test.txt\n") progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == ("added:\n" " test.txt\n") progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == "sub2/hello.txt\n" assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == 'sub1/sub2/hello.txt\n' assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') assert backtick('bzr relpath sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') runcmd('bzr log') runcmd('bzr log -v') cd('..') cd('..') progress('ignore patterns') mkdir('ignorebranch') cd('ignorebranch') runcmd('bzr init') assert backtick('bzr unknowns') == '' file('foo.tmp', 'wt').write('tmp files are ignored') assert backtick('bzr unknowns') == '' file('foo.c', 'wt').write('int main() {}') assert backtick('bzr unknowns') == 'foo.c\n' runcmd('bzr add foo.c') assert backtick('bzr unknowns') == '' # 'ignore' works when creating the .bzignore file file('foo.blah', 'wt').write('blah') assert backtick('bzr unknowns') == 'foo.blah\n' runcmd('bzr ignore *.blah') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\n' # 'ignore' works when then .bzrignore file already exists file('garh', 'wt').write('garh') assert backtick('bzr unknowns') == 'garh\n' runcmd('bzr ignore garh') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\ngarh\n' progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :508 committer Martin Pool 1116216611 +1000 data 93 - new -p option for testbzr to use a different version of python to run the bzr under test from :507 M 644 inline NEWS data 7413 bzr-0.0.5 NOT RELEASED YET CHANGES: * ``bzr`` with no command now shows help rather than giving an error. Suggested by Michael Ellerman. * ``bzr status`` output format changed, because svn-style output doesn't really match the model of bzr. Now files are grouped by status and can be shown with their IDs. ``bzr status --all`` shows all versioned files and unknown files but not ignored files. ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New ``bzr ignore PATTERN`` command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. File may be in a branch other than the working directory. * ``bzr log`` and ``bzr root`` can be given an http URL instead of a filename. * Commands can now be defined by external programs or scripts in a directory on $BZRPATH. * New "stat cache" avoids reading the contents of files if they haven't changed since the previous time. * If the Python interpreter is too old, try to find a better one or give an error. Based on a patch from Fredrik Lundh. * New optional parameter ``bzr info [BRANCH]``. * New form ``bzr commit SELECTED`` to commit only selected files. BUG FIXES: * Fixed diff format so that added and removed files will be handled properly by patch. Fix from Lalo Martins. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. * testbzr requires python2.4, but can be used to test bzr running under a different version. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. * New developer commands ``added``, ``modified``. PORTABILITY: * Cope on Windows on python2.3 by using the weaker random seed. 2.4 is now only recommended. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline TODO data 10312 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * ``bzr log DIR`` should give changes to any files within DIR. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. * Maybe also store directories in the statcache so that we can quickly identify that they still exist. Medium things ------------- * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Merge Aaron's merge code. * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. We should be able to just get the id for the selected files, look up their location and diff just those files. No need to traverse the entire inventories. * ``bzr status DIR`` or ``bzr diff DIR`` should report on all changes under that directory. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. - Selected-file commit - Impossible selected-file commit: adding things in non-versioned directories, crossing renames, etc. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Should be a signature at the top of the cache file. * Paranoid mode where we never trust SHA-1 matches. * Don't commit if there are no changes unless forced. * --dry-run mode for commit? * Generally, be a bit more verbose unless --silent is specified. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :509 committer Martin Pool 1116217941 +1000 data 159 clean up stat cache code: - smarter UTF-8 and quopri encoding of file names - check paths are not duplicated in cache - check lines are well-formed - more docs from :508 M 644 inline bzrlib/statcache.py data 7780 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import stat, os, sha, time from binascii import b2a_qp, a2b_qp from trace import mutter from errors import BzrError, BzrCheckError """File stat cache to speed up tree comparisons. This module basically gives a quick way to find the SHA-1 and related information of a file in the working directory, without actually reading and hashing the whole file. Implementation ============== Users of this module should not need to know about how this is implemented, and in particular should not depend on the particular data which is stored or its format. This is done by maintaining a cache indexed by a file fingerprint of (path, size, mtime, ctime, ino, dev) pointing to the SHA-1. If the fingerprint has changed, we assume the file content has not changed either and the SHA-1 is therefore the same. If any of the fingerprint fields have changed then the file content *may* have changed, or it may not have. We need to reread the file contents to make sure, but this is not visible to the user or higher-level code (except as a delay of course). The mtime and ctime are stored with nanosecond fields, but not all filesystems give this level of precision. There is therefore a possible race: the file might be modified twice within a second without changing the size or mtime, and a SHA-1 cached from the first version would be wrong. We handle this by not recording a cached hash for any files which were modified in the current second and that therefore have the chance to change again before the second is up. The only known hole in this design is if the system clock jumps backwards crossing invocations of bzr. Please don't do that; use ntp to gradually adjust your clock or don't use bzr over the step. At the moment this is stored in a simple textfile; it might be nice to use a tdb instead. The cache is represented as a map from file_id to a tuple of (file_id, sha1, path, size, mtime, ctime, ino, dev). The SHA-1 is stored in memory as a hexdigest. File names are written out as the quoted-printable encoding of their UTF-8 representation. """ # order of fields returned by fingerprint() FP_SIZE = 0 FP_MTIME = 1 FP_CTIME = 2 FP_INO = 3 FP_DEV = 4 # order of fields in the statcache file and in the in-memory map SC_FILE_ID = 0 SC_SHA1 = 1 SC_PATH = 2 SC_SIZE = 3 SC_MTIME = 4 SC_CTIME = 5 SC_INO = 6 SC_DEV = 7 def fingerprint(abspath): try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) def _write_cache(basedir, entry_iter, dangerfiles): from atomicfile import AtomicFile cachefn = os.path.join(basedir, '.bzr', 'stat-cache') outf = AtomicFile(cachefn, 'wb') try: for entry in entry_iter: if len(entry) != 8: raise ValueError("invalid statcache entry tuple %r" % entry) if entry[SC_FILE_ID] in dangerfiles: continue # changed too recently outf.write(entry[0]) # file id outf.write(' ') outf.write(entry[1]) # hex sha1 outf.write(' ') outf.write(b2a_qp(entry[2].encode('utf-8'), True)) # name for nf in entry[3:]: outf.write(' %d' % nf) outf.write('\n') outf.commit() finally: if not outf.closed: outf.abort() def load_cache(basedir): from sets import Set cache = {} seen_paths = Set() try: cachefn = os.path.join(basedir, '.bzr', 'stat-cache') cachefile = open(cachefn, 'r') except IOError: return cache for l in cachefile: f = l.split(' ') file_id = f[0] if file_id in cache: raise BzrError("duplicated file_id in cache: {%s}" % file_id) path = a2b_qp(f[2]).decode('utf-8') if path in seen_paths: raise BzrCheckError("duplicated path in cache: %r" % path) seen_paths.add(path) entry = (file_id, f[1], path) + tuple([long(x) for x in f[3:]]) if len(entry) != 8: raise ValueError("invalid statcache entry tuple %r" % entry) cache[file_id] = entry return cache def _files_from_inventory(inv): for path, ie in inv.iter_entries(): if ie.kind != 'file': continue yield ie.file_id, path def update_cache(basedir, inv, flush=False): """Update and return the cache for the branch. The returned cache may contain entries that have not been written to disk for files recently touched. flush -- discard any previous cache and recalculate from scratch. """ # TODO: It's supposed to be faster to stat the files in order by inum. # We don't directly know the inum of the files of course but we do # know where they were last sighted, so we can sort by that. assert isinstance(flush, bool) if flush: cache = {} else: cache = load_cache(basedir) return _update_cache_from_list(basedir, cache, _files_from_inventory(inv)) def _update_cache_from_list(basedir, cache, to_update): """Update and return the cache for given files. cache -- Previously cached values to be validated. to_update -- Sequence of (file_id, path) pairs to check. """ from sets import Set stat_cnt = missing_cnt = hardcheck = change_cnt = 0 # files that have been recently touched and can't be # committed to a persistent cache yet. dangerfiles = Set() now = int(time.time()) ## mutter('update statcache under %r' % basedir) for file_id, path in to_update: abspath = os.path.join(basedir, path) fp = fingerprint(abspath) stat_cnt += 1 cacheentry = cache.get(file_id) if fp == None: # not here if cacheentry: del cache[file_id] change_cnt += 1 missing_cnt += 1 continue if (fp[FP_MTIME] >= now) or (fp[FP_CTIME] >= now): dangerfiles.add(file_id) if cacheentry and (cacheentry[3:] == fp): continue # all stat fields unchanged hardcheck += 1 dig = sha.new(file(abspath, 'rb').read()).hexdigest() # We update the cache even if the digest has not changed from # last time we looked, so that the fingerprint fields will # match in future. cacheentry = (file_id, dig, path) + fp cache[file_id] = cacheentry change_cnt += 1 mutter('statcache: statted %d files, read %d files, %d changed, %d dangerous, ' '%d in cache' % (stat_cnt, hardcheck, change_cnt, len(dangerfiles), len(cache))) if change_cnt: mutter('updating on-disk statcache') _write_cache(basedir, cache.itervalues(), dangerfiles) return cache commit refs/heads/master mark :510 committer Martin Pool 1116221211 +1000 data 48 - fix add of files when standing in subdirectory from :509 M 644 inline bzrlib/add.py data 3018 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, sys import bzrlib from osutils import quotefn, appendpath from errors import bailout from trace import mutter, note def smart_add(file_list, verbose=False, recurse=True): """Add files to version, optionall recursing into directories. This is designed more towards DWIM for humans than API simplicity. For the specific behaviour see the help for cmd_add(). """ assert file_list user_list = file_list[:] assert not isinstance(file_list, basestring) b = bzrlib.branch.Branch(file_list[0], find_root=True) inv = b.read_working_inventory() tree = b.working_tree() count = 0 for f in file_list: rf = b.relpath(f) af = b.abspath(rf) kind = bzrlib.osutils.file_kind(af) if kind != 'file' and kind != 'directory': if f not in user_list: print "Skipping %s (can't add file of kind '%s')" % (f, kind) continue bailout("can't add file of kind %r" % kind) bzrlib.mutter("smart add of %r, abs=%r" % (f, af)) if bzrlib.branch.is_control_file(af): bailout("cannot add control file %r" % af) versioned = (inv.path2id(rf) != None) if rf == '': mutter("branch root doesn't need to be added") elif versioned: mutter("%r is already versioned" % f) else: file_id = bzrlib.branch.gen_file_id(rf) inv.add_path(rf, kind=kind, file_id=file_id) bzrlib.mutter("added %r kind %r file_id={%s}" % (rf, kind, file_id)) count += 1 if verbose: bzrlib.textui.show_status('A', kind, quotefn(f)) if kind == 'directory' and recurse: for subf in os.listdir(af): subp = appendpath(rf, subf) if subf == bzrlib.BZRDIR: mutter("skip control directory %r" % subp) elif tree.is_ignored(subp): mutter("skip ignored sub-file %r" % subp) else: mutter("queue to add sub-file %r" % subp) file_list.append(b.abspath(subp)) if count > 0: if verbose: note('added %d' % count) b._write_inventory(inv) commit refs/heads/master mark :511 committer Martin Pool 1116221290 +1000 data 79 - add tests for files and directories with spaces in name (currently failing) from :510 M 644 inline testbzr data 10565 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. usage: testbzr [-p PYTHON] [BZR] By default this tests the copy of bzr found in the same directory as testbzr, or the first one found on the $PATH. A copy of bzr may be given on the command line to override this, for example when applying a new test suite to an old copy of bzr or vice versa. testbzr normally invokes bzr using the same version of python as it would normally use to run -- that is, the system default python, unless that is older than 2.3. The -p option allows specification of a different Python interpreter, such as when testing that bzr still works on python2.3. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" OVERRIDE_PYTHON = None LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = BZRPATH if OVERRIDE_PYTHON: cmd.insert(0, OVERRIDE_PYTHON) logfile.write('$ %r\n' % cmd) return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) logfile = open(LOGFILENAME, 'wt', buffering=1) try: from getopt import getopt opts, args = getopt(sys.argv[1:], 'p:') for option, value in opts: if option == '-p': OVERRIDE_PYTHON = value mypath = os.path.abspath(sys.argv[0]) print '%-30s %s' % ('running tests from', mypath) global BZRPATH if args: BZRPATH = args[1] else: BZRPATH = os.path.join(os.path.split(mypath)[0], 'bzr') print '%-30s %s' % ('against bzr', BZRPATH) print '%-30s %s' % ('in directory', os.getcwd()) print '%-30s %s' % ('with python', (OVERRIDE_PYTHON or '(default)')) print print backtick([BZRPATH, 'version']) runcmd(['mkdir', TESTDIR]) cd(TESTDIR) test_root = os.getcwd() progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("internal tests") runcmd("bzr selftest") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) runcmd("bzr diff --message foo", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == 'unknown:\n test.txt\n' out = backtick("bzr status --all") assert out == "unknown:\n test.txt\n" out = backtick("bzr status test.txt --all") assert out == "unknown:\n test.txt\n" f = file('test2.txt', 'wt') f.write('goodbye cruel world...\n') f.close() out = backtick("bzr status test.txt") assert out == "unknown:\n test.txt\n" out = backtick("bzr status") assert out == ("unknown:\n" " test.txt\n" " test2.txt\n") os.unlink('test2.txt') progress("command aliases") out = backtick("bzr st --all") assert out == ("unknown:\n" " test.txt\n") out = backtick("bzr stat") assert out == ("unknown:\n" " test.txt\n") progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == ("added:\n" " test.txt\n") progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == "sub2/hello.txt\n" assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == 'sub1/sub2/hello.txt\n' assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') assert backtick('bzr relpath sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') runcmd('bzr log') runcmd('bzr log -v') progress("file with spaces in name") mkdir('sub directory') file('sub directory/file with spaces ', 'wt').write('see how this works\n') runcmd('bzr add .') runcmd('bzr diff') runcmd('bzr commit -m add-spaces') runcmd('bzr check') cd('..') cd('..') progress('ignore patterns') mkdir('ignorebranch') cd('ignorebranch') runcmd('bzr init') assert backtick('bzr unknowns') == '' file('foo.tmp', 'wt').write('tmp files are ignored') assert backtick('bzr unknowns') == '' file('foo.c', 'wt').write('int main() {}') assert backtick('bzr unknowns') == 'foo.c\n' runcmd('bzr add foo.c') assert backtick('bzr unknowns') == '' # 'ignore' works when creating the .bzignore file file('foo.blah', 'wt').write('blah') assert backtick('bzr unknowns') == 'foo.blah\n' runcmd('bzr ignore *.blah') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\n' # 'ignore' works when then .bzrignore file already exists file('garh', 'wt').write('garh') assert backtick('bzr unknowns') == 'garh\n' runcmd('bzr ignore garh') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\ngarh\n' progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) sys.exit(1) commit refs/heads/master mark :512 committer Martin Pool 1116221389 +1000 data 49 - bzr check can be run from a branch subdirectory from :511 M 644 inline bzrlib/commands.py data 35648 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date from bzrlib import merge def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0], lock_mode='r') file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.', lock_mode='r') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, specific_files=file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from branch import find_branch b = find_branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.tests, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): merge.merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :513 committer Martin Pool 1116221544 +1000 data 40 - show some log output if the tests fail from :512 M 644 inline testbzr data 10711 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. usage: testbzr [-p PYTHON] [BZR] By default this tests the copy of bzr found in the same directory as testbzr, or the first one found on the $PATH. A copy of bzr may be given on the command line to override this, for example when applying a new test suite to an old copy of bzr or vice versa. testbzr normally invokes bzr using the same version of python as it would normally use to run -- that is, the system default python, unless that is older than 2.3. The -p option allows specification of a different Python interpreter, such as when testing that bzr still works on python2.3. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" OVERRIDE_PYTHON = None LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = BZRPATH if OVERRIDE_PYTHON: cmd.insert(0, OVERRIDE_PYTHON) logfile.write('$ %r\n' % cmd) return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) start_dir = os.getcwd() logfile = open(LOGFILENAME, 'wt', buffering=1) try: from getopt import getopt opts, args = getopt(sys.argv[1:], 'p:') for option, value in opts: if option == '-p': OVERRIDE_PYTHON = value mypath = os.path.abspath(sys.argv[0]) print '%-30s %s' % ('running tests from', mypath) global BZRPATH if args: BZRPATH = args[1] else: BZRPATH = os.path.join(os.path.split(mypath)[0], 'bzr') print '%-30s %s' % ('against bzr', BZRPATH) print '%-30s %s' % ('in directory', os.getcwd()) print '%-30s %s' % ('with python', (OVERRIDE_PYTHON or '(default)')) print print backtick([BZRPATH, 'version']) runcmd(['mkdir', TESTDIR]) cd(TESTDIR) test_root = os.getcwd() progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("internal tests") runcmd("bzr selftest") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) runcmd("bzr diff --message foo", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == 'unknown:\n test.txt\n' out = backtick("bzr status --all") assert out == "unknown:\n test.txt\n" out = backtick("bzr status test.txt --all") assert out == "unknown:\n test.txt\n" f = file('test2.txt', 'wt') f.write('goodbye cruel world...\n') f.close() out = backtick("bzr status test.txt") assert out == "unknown:\n test.txt\n" out = backtick("bzr status") assert out == ("unknown:\n" " test.txt\n" " test2.txt\n") os.unlink('test2.txt') progress("command aliases") out = backtick("bzr st --all") assert out == ("unknown:\n" " test.txt\n") out = backtick("bzr stat") assert out == ("unknown:\n" " test.txt\n") progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == ("added:\n" " test.txt\n") progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == "sub2/hello.txt\n" assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == 'sub1/sub2/hello.txt\n' assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') assert backtick('bzr relpath sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') runcmd('bzr log') runcmd('bzr log -v') progress("file with spaces in name") mkdir('sub directory') file('sub directory/file with spaces ', 'wt').write('see how this works\n') runcmd('bzr add .') runcmd('bzr diff') runcmd('bzr commit -m add-spaces') runcmd('bzr check') cd('..') cd('..') progress('ignore patterns') mkdir('ignorebranch') cd('ignorebranch') runcmd('bzr init') assert backtick('bzr unknowns') == '' file('foo.tmp', 'wt').write('tmp files are ignored') assert backtick('bzr unknowns') == '' file('foo.c', 'wt').write('int main() {}') assert backtick('bzr unknowns') == 'foo.c\n' runcmd('bzr add foo.c') assert backtick('bzr unknowns') == '' # 'ignore' works when creating the .bzignore file file('foo.blah', 'wt').write('blah') assert backtick('bzr unknowns') == 'foo.blah\n' runcmd('bzr ignore *.blah') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\n' # 'ignore' works when then .bzrignore file already exists file('garh', 'wt').write('garh') assert backtick('bzr unknowns') == 'garh\n' runcmd('bzr ignore garh') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\ngarh\n' progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) logfile.close() sys.stdout.writelines(file(os.path.join(start_dir, LOGFILENAME), 'rt').readlines()[-50:]) sys.exit(1) commit refs/heads/master mark :514 committer Martin Pool 1116230980 +1000 data 55 - remove old internal tests in favour of blackbox tests from :513 D bzrlib/tests.py M 644 inline bzrlib/commands.py data 35634 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date from bzrlib import merge def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0], lock_mode='r') file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.', lock_mode='r') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, specific_files=file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from branch import find_branch b = find_branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures else: print class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): merge.merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :515 committer Martin Pool 1116231101 +1000 data 56 - bzr selftest: return shell false (1) if any tests fail from :514 M 644 inline bzrlib/commands.py data 35676 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date from bzrlib import merge def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0], lock_mode='r') file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.', lock_mode='r') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, specific_files=file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from branch import find_branch b = find_branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file.""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures return 1 else: print return 0 class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): merge.merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :516 committer Martin Pool 1116290039 +1000 data 20 - fix doc index link from :515 M 644 inline doc/index.txt data 5523 Bazaar-NG ********* .. These documents are formatted as ReStructuredText. You can .. .. convert them to HTML, PDF, etc using the ``python-docutils`` .. .. package. .. *Bazaar-NG* (``bzr``) is a project of `Canonical Ltd`__ to develop an open source distributed version control system that is powerful, friendly, and scalable. The project is at an early stage of development. __ http://canonical.com/ **Note:** These documents are in a very preliminary state, and so may be internally or externally inconsistent or redundant. Comments are still very welcome. Please send them to . For more information, see the homepage at http://bazaar-ng.org/ User documentation ------------------ * `Project overview/introduction `__ * `Command reference `__ -- intended to be user documentation, and gives the best overview at the moment of what the system will feel like to use. Fairly complete. Requirements and general design ------------------------------- * `Various purposes of a VCS `__ -- taking snapshots and helping with merges is not the whole story. * `Requirements `__ * `Costs `__ of various factors: time, disk, network, etc. * `Deadly sins `__ that gcc maintainers suggest we avoid. * `Overview of the whole design `__ and miscellaneous small design points. * `File formats `__ * `Random observations `__ that don't fit anywhere else yet. Design of particular features ----------------------------- * `Automatic generation of ChangeLogs `__ * `Cherry picking `__ -- merge just selected non-contiguous changes from a branch. * `Common changeset format `__ for interchange format between VCS. * `Compression `__ of file text for more efficient storage. * `Config specs `__ assemble a tree from several places. * `Conflicts `_ that can occur during merge-like operations. * `Ignored files `__ * `Recovering from interrupted operations `__ * `Inventory command `__ * `Branch joins `__ represent that all the changes from one branch are integrated into another. * `Kill a version `__ to fix a broken commit or wrong message, or to remove confidential information from the history. * `Hash collisions `__ and weaknesses, and the security implications thereof. * `Layers `__ within the design * `Library interface `__ for Python. * `Merge `__ * `Mirroring `__ * `Optional edit command `__: sometimes people want to make the working copy read-only, or not present at all. * `Partial commits `__ * `Patch pools `__ to efficiently store related branches. * `Revfiles `__ store the text history of files. * `Revfiles storing annotations `__ * `Revision syntax `__ -- ``hello.c@12``, etc. * `Roll-up commits `__ -- a single revision incorporates the changes from several others. * `Scalability `__ * `Security `__ * `Shared branches `__ maintained by more than one person * `Supportability `__ -- how to handle any bugs or problems in the field. * `Place tags on revisions for easy reference `__ * `Detecting unchanged files `__ * `Merging previously-unrelated branches `__ * `Usability principles `__ (very small at the moment) * ``__ * ``__ * ``__ Modelling/controlling flow of patches. * ``__ -- Discussion of using YAML_ as a storage or transmission format. .. _YAML: http://www.yaml.org/ Comparisons to other systems ---------------------------- * `Taxonomy `__: basic questions a VCS must answer. * `Bitkeeper `__, the proprietary system used by some kernel developers. * `Aegis `__, a tool focussed on enforcing process and workflow. * `Codeville `__ has an intruiging but scarcely-documented merge algorithm. * `CVSNT `__, with more Windows support and some merge enhancements. * `OpenCM `__, another hash-based tool with a good whitepaper. * `PRCS `__, a non-distributed inventory-based tool. * `GNU Arch `__, with many pros and cons. * `Darcs `__, a merge-focussed tool with good usability. * `Quilt `__ -- Andrew Morton's patch scripts, popular with kernel maintainers. * `Monotone `__, Graydon Hoare's hash-based distributed system. * `SVK `__ -- distributed operation stacked on Subversion. * `Sun Teamware `__ Project management and organization ----------------------------------- * `Notes on how to get a VCS adopted `__ * `Thanks `__ to various people * `Extra commands `__ for internal/developer/debugger use. * `Choice of Python as a development language `__ commit refs/heads/master mark :517 committer Martin Pool 1116305969 +1000 data 9 - cleanup from :516 M 644 inline bzrlib/commit.py data 8361 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def commit(branch, message, timestamp=None, timezone=None, committer=None, verbose=True, specific_files=None, rev_id=None): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. specific_files If true, commit only those files. rev_id If set, use this as the new revision id. Useful for test or import commands that need to tightly control what revisions are assigned. If you duplicate a revision id that exists elsewhere it is your own fault. If null (default), a time/random revision id is generated. """ import os, time, tempfile from inventory import Inventory from osutils import isdir, isfile, sha_string, quotefn, \ local_time_offset, username, kind_marker, is_inside_any from branch import gen_file_id from errors import BzrError from revision import Revision from trace import mutter, note branch._need_writelock() # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_tree = branch.working_tree() work_inv = work_tree.inventory inv = Inventory() basis = branch.basis_tree() basis_inv = basis.inventory missing_ids = [] if verbose: note('looking for changes...') for path, entry in work_inv.iter_entries(): ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = branch.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if specific_files and not is_inside_any(specific_files, path): if basis_inv.has_id(file_id): # carry over with previous state inv.add(basis_inv[file_id].copy()) else: # omit this from committed inventory pass continue if not work_tree.has_id(file_id): if verbose: print('deleted %s%s' % (path, kind_marker(entry.kind))) mutter(" file is missing, removing from inventory") missing_ids.append(file_id) continue inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: raise BzrError("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): raise BzrError("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): raise BzrError("%s is entered as file but is not a file" % quotefn(p)) new_sha1 = work_tree.get_file_sha1(file_id) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and old_ie.text_sha1 == new_sha1): ## assert content == basis.get_file(file_id).read() entry.text_id = old_ie.text_id entry.text_sha1 = new_sha1 entry.text_size = old_ie.text_size mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: content = file(p, 'rb').read() entry.text_sha1 = sha_string(content) entry.text_size = len(content) entry.text_id = gen_file_id(entry.name) branch.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: print('added %s' % path) elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): print('modified %s' % path) else: print('renamed %s' % path) for file_id in missing_ids: # Any files that have been deleted are now removed from the # working inventory. Files that were not selected for commit # are left as they were in the working inventory and ommitted # from the revision inventory. # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itbranch. if work_inv.has_id(file_id): del work_inv[file_id] if rev_id is None: rev_id = _gen_revision_id(time.time()) inv_id = rev_id inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) branch.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) branch._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = branch.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) branch.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (branch.revno() + 1)) branch.append_revision(rev_id) if verbose: note("commited r%d" % branch.revno()) def _gen_revision_id(when): """Return new revision-id.""" from binascii import hexlify from osutils import rand_bytes, compact_date, user_email s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s commit refs/heads/master mark :518 committer Martin Pool 1116312691 +1000 data 3 doc from :517 M 644 inline bzrlib/commit.py data 8488 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def commit(branch, message, timestamp=None, timezone=None, committer=None, verbose=True, specific_files=None, rev_id=None): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. specific_files If true, commit only those files. rev_id If set, use this as the new revision id. Useful for test or import commands that need to tightly control what revisions are assigned. If you duplicate a revision id that exists elsewhere it is your own fault. If null (default), a time/random revision id is generated. """ import os, time, tempfile from inventory import Inventory from osutils import isdir, isfile, sha_string, quotefn, \ local_time_offset, username, kind_marker, is_inside_any from branch import gen_file_id from errors import BzrError from revision import Revision from trace import mutter, note branch._need_writelock() # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_tree = branch.working_tree() work_inv = work_tree.inventory inv = Inventory() basis = branch.basis_tree() basis_inv = basis.inventory missing_ids = [] if verbose: note('looking for changes...') for path, entry in work_inv.iter_entries(): ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = branch.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if specific_files and not is_inside_any(specific_files, path): if basis_inv.has_id(file_id): # carry over with previous state inv.add(basis_inv[file_id].copy()) else: # omit this from committed inventory pass continue if not work_tree.has_id(file_id): if verbose: print('deleted %s%s' % (path, kind_marker(entry.kind))) mutter(" file is missing, removing from inventory") missing_ids.append(file_id) continue inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: raise BzrError("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): raise BzrError("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): raise BzrError("%s is entered as file but is not a file" % quotefn(p)) new_sha1 = work_tree.get_file_sha1(file_id) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and old_ie.text_sha1 == new_sha1): ## assert content == basis.get_file(file_id).read() entry.text_id = old_ie.text_id entry.text_sha1 = new_sha1 entry.text_size = old_ie.text_size mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: content = file(p, 'rb').read() # calculate the sha again, just in case the file contents # changed since we updated the cache entry.text_sha1 = sha_string(content) entry.text_size = len(content) entry.text_id = gen_file_id(entry.name) branch.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: print('added %s' % path) elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): print('modified %s' % path) else: print('renamed %s' % path) for file_id in missing_ids: # Any files that have been deleted are now removed from the # working inventory. Files that were not selected for commit # are left as they were in the working inventory and ommitted # from the revision inventory. # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itbranch. if work_inv.has_id(file_id): del work_inv[file_id] if rev_id is None: rev_id = _gen_revision_id(time.time()) inv_id = rev_id inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) branch.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) branch._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = branch.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) branch.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (branch.revno() + 1)) branch.append_revision(rev_id) if verbose: note("commited r%d" % branch.revno()) def _gen_revision_id(when): """Return new revision-id.""" from binascii import hexlify from osutils import rand_bytes, compact_date, user_email s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s commit refs/heads/master mark :519 committer Martin Pool 1116312823 +1000 data 38 - todo: discussion of pre-commit tests from :518 M 644 inline TODO data 11110 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * ``bzr log DIR`` should give changes to any files within DIR. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. * Maybe also store directories in the statcache so that we can quickly identify that they still exist. Medium things ------------- * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Merge Aaron's merge code. * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. We should be able to just get the id for the selected files, look up their location and diff just those files. No need to traverse the entire inventories. * ``bzr status DIR`` or ``bzr diff DIR`` should report on all changes under that directory. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. - Selected-file commit - Impossible selected-file commit: adding things in non-versioned directories, crossing renames, etc. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Should be a signature at the top of the cache file. * Paranoid mode where we never trust SHA-1 matches. * Don't commit if there are no changes unless forced. * --dry-run mode for commit? * Generally, be a bit more verbose unless --silent is specified. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. Possibly this should be done by splitting the commit function into several parts (under a single interface). It is already rather large. Decomposition: - find tree modifications and prepare in-memory inventory - export that inventory to a temporary directory - run the test in that temporary directory - if that succeeded, continue to actually finish the commit What should be done with the text of modified files while this is underway? I don't think we want to count on holding them in memory and we can't trust the working files to stay in one place so I suppose we need to move them into the text store, or otherwise into a temporary directory. If the commit does not actually complete, we would rather the content was not left behind in the stores. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :520 committer Martin Pool 1116312976 +1000 data 33 - add space for un-merged patches from :519 commit refs/heads/master mark :521 committer Martin Pool 1116313307 +1000 data 35 - Add patch to give symlink support from :520 M 644 inline patches/symlink-support.patch data 20803 Return-Path: X-Original-To: mbp@sourcefrog.net Delivered-To: mbp@ozlabs.org X-Greylist: delayed 1826 seconds by postgrey-1.21 at ozlabs; Sun, 15 May 2005 06:59:11 EST Received: from upstroke.tntech.dk (cpe.atm2-0-1041078.0x503eaf62.odnxx4.customer.tele.dk [80.62.175.98]) by ozlabs.org (Postfix) with ESMTP id B968E679EA for ; Sun, 15 May 2005 06:59:11 +1000 (EST) Received: by upstroke.tntech.dk (Postfix, from userid 1001) id 63F83542FF; Sat, 14 May 2005 22:28:37 +0200 (CEST) To: Martin Pool Subject: [PATCH] symlink support patch From: Erik Toubro Nielsen Date: Sat, 14 May 2005 22:28:37 +0200 Message-ID: <86u0l57dsa.fsf@upstroke.tntech.dk> User-Agent: Gnus/5.1006 (Gnus v5.10.6) XEmacs/21.4 (Security Through Obscurity, linux) MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" X-Spam-Checker-Version: SpamAssassin 3.0.3 (2005-04-27) on ozlabs.org X-Spam-Level: X-Spam-Status: No, score=-1.4 required=3.2 tests=BAYES_00,NO_MORE_FUNN, RCVD_IN_BLARS_RBL autolearn=no version=3.0.3 Content-Length: 19688 Lines: 604 --=-=-= I'm not sending this to the list as it is pretty large. Let me know if its usefull and if I should rework anything. Erik Overview: (Bugfix: in class TreeDelta I've moved kind= one level up, since kind is also used in the else part) Since both the InventoryEntry and stat cache is changed, perhaps the branch format number should be increased? Added test cases for symlinks to testbzr Cannot use realpath since it expands path/L to path/LinkTarget Cannot use exists, use new osutils.lexists I'm overloading the text_modified to signify that a link target is changed. Perhaps text_modified should be renamed content_modified? InventoryEntry has a new member "symlink_target", The stat cache entry has been extended to contain the symlink target and the st_mode. I try to ignore an old format cache file. --=-=-= Content-Type: text/x-patch Content-Disposition: inline; filename=symlinksupport.patch *** modified file 'bzrlib/add.py' --- bzrlib/add.py +++ bzrlib/add.py @@ -38,7 +38,7 @@ for f in file_list: kind = bzrlib.osutils.file_kind(f) - if kind != 'file' and kind != 'directory': + if kind != 'file' and kind != 'directory' and kind != 'symlink': if f not in user_list: print "Skipping %s (can't add file of kind '%s')" % (f, kind) continue @@ -56,7 +56,7 @@ kind = bzrlib.osutils.file_kind(f) - if kind != 'file' and kind != 'directory': + if kind != 'file' and kind != 'directory' and kind != 'symlink': bailout("can't add file '%s' of kind %r" % (f, kind)) versioned = (inv.path2id(rf) != None) *** modified file 'bzrlib/branch.py' --- bzrlib/branch.py +++ bzrlib/branch.py @@ -58,11 +58,9 @@ run into the root.""" if f == None: f = os.getcwd() - elif hasattr(os.path, 'realpath'): - f = os.path.realpath(f) else: - f = os.path.abspath(f) - if not os.path.exists(f): + f = bzrlib.osutils.normalizepath(f) + if not bzrlib.osutils.lexists(f): raise BzrError('%r does not exist' % f) @@ -189,7 +187,7 @@ """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" - rp = os.path.realpath(path) + rp = bzrlib.osutils.normalizepath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) @@ -531,7 +529,9 @@ file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) - if not os.path.exists(p): + # it should be enough to use os.lexists instead of exists + # but lexists in an 2.4 function + if not bzrlib.osutils.lexists(p): mutter(" file is missing, removing from inventory") if verbose: show_status('D', entry.kind, quotefn(path)) @@ -554,6 +554,10 @@ if entry.kind == 'directory': if not isdir(p): bailout("%s is entered as directory but not a directory" % quotefn(p)) + elif entry.kind == 'symlink': + if not os.path.islink(p): + bailout("%s is entered as symbolic link but is not a symbolic link" % quotefn(p)) + entry.read_symlink_target(p) elif entry.kind == 'file': if not isfile(p): bailout("%s is entered as file but is not a file" % quotefn(p)) *** modified file 'bzrlib/diff.py' --- bzrlib/diff.py +++ bzrlib/diff.py @@ -66,6 +66,11 @@ print >>to_file, "\\ No newline at end of file" print >>to_file +def _diff_symlink(old_tree, new_tree, file_id): + t1 = old_tree.get_symlink_target(file_id) + t2 = new_tree.get_symlink_target(file_id) + print '*** *** target changed %r => %r' % (t1, t2) + def show_diff(b, revision, specific_files): import sys @@ -112,12 +117,15 @@ for old_path, new_path, file_id, kind, text_modified in delta.renamed: print '*** renamed %s %r => %r' % (kind, old_path, new_path) - if text_modified: + if kind == 'file' and text_modified: _diff_one(old_tree.get_file(file_id).readlines(), new_tree.get_file(file_id).readlines(), sys.stdout, fromfile=old_label + old_path, tofile=new_label + new_path) + + elif kind == 'symlink' and text_modified: + _diff_symlink(old_tree, new_tree, file_id) for path, file_id, kind in delta.modified: print '*** modified %s %r' % (kind, path) @@ -128,6 +136,8 @@ fromfile=old_label + path, tofile=new_label + path) + elif kind == 'symlink': + _diff_symlink(old_tree, new_tree, file_id) class TreeDelta: @@ -149,7 +159,9 @@ Each id is listed only once. Files that are both modified and renamed are listed only in - renamed, with the text_modified flag true. + renamed, with the text_modified flag true. The text_modified + applies either to the the content of the file or the target of the + symbolic link, depending of the kind of file. The lists are normally sorted when the delta is created. """ @@ -224,8 +236,8 @@ specific_files = ImmutableSet(specific_files) for file_id in old_tree: + kind = old_inv.get_file_kind(file_id) if file_id in new_tree: - kind = old_inv.get_file_kind(file_id) assert kind == new_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ @@ -246,6 +258,14 @@ old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) + elif kind == 'symlink': + t1 = old_tree.get_symlink_target(file_id) + t2 = new_tree.get_symlink_target(file_id) + if t1 != t2: + mutter(" symlink target changed") + text_modified = True + else: + text_modified = False else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False *** modified file 'bzrlib/inventory.py' --- bzrlib/inventory.py +++ bzrlib/inventory.py @@ -125,14 +125,22 @@ self.kind = kind self.text_id = text_id self.parent_id = parent_id + self.symlink_target = None if kind == 'directory': self.children = {} elif kind == 'file': pass + elif kind == 'symlink': + pass else: raise BzrError("unhandled entry kind %r" % kind) - + def read_symlink_target(self, path): + if self.kind == 'symlink': + try: + self.symlink_target = os.readlink(path) + except OSError,e: + raise BzrError("os.readlink error, %s" % e) def sorted_children(self): l = self.children.items() @@ -145,6 +153,7 @@ self.parent_id, text_id=self.text_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size + other.symlink_target = self.symlink_target return other @@ -168,7 +177,7 @@ if self.text_size != None: e.set('text_size', '%d' % self.text_size) - for f in ['text_id', 'text_sha1']: + for f in ['text_id', 'text_sha1', 'symlink_target']: v = getattr(self, f) if v != None: e.set(f, v) @@ -198,6 +207,7 @@ self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') + self.symlink_target = elt.get('symlink_target') ## mutter("read inventoryentry: %r" % (elt.attrib)) *** modified file 'bzrlib/osutils.py' --- bzrlib/osutils.py +++ bzrlib/osutils.py @@ -58,7 +58,30 @@ else: raise BzrError("can't handle file kind with mode %o of %r" % (mode, f)) - +def lexists(f): + try: + if hasattr(os, 'lstat'): + os.lstat(f) + else: + os.stat(f) + return True + except OSError,e: + if e.errno == errno.ENOENT: + return False; + else: + raise BzrError("lstat/stat of (%r): %r" % (f, e)) + +def normalizepath(f): + if hasattr(os.path, 'realpath'): + F = os.path.realpath + else: + F = os.path.abspath + [p,e] = os.path.split(f) + if e == "" or e == "." or e == "..": + return F(f) + else: + return os.path.join(F(p), e) + def isdir(f): """True if f is an accessible directory.""" *** modified file 'bzrlib/statcache.py' --- bzrlib/statcache.py +++ bzrlib/statcache.py @@ -53,7 +53,7 @@ to use a tdb instead. The cache is represented as a map from file_id to a tuple of (file_id, -sha1, path, size, mtime, ctime, ino, dev). +sha1, path, symlink target, size, mtime, ctime, ino, dev, mode). """ @@ -62,11 +62,13 @@ FP_CTIME = 2 FP_INO = 3 FP_DEV = 4 - +FP_ST_MODE=5 SC_FILE_ID = 0 SC_SHA1 = 1 - +SC_SYMLINK_TARGET = 3 + +CACHE_ENTRY_SIZE = 10 def fingerprint(abspath): try: @@ -79,7 +81,7 @@ return None return (fs.st_size, fs.st_mtime, - fs.st_ctime, fs.st_ino, fs.st_dev) + fs.st_ctime, fs.st_ino, fs.st_dev, fs.st_mode) def _write_cache(basedir, entry_iter, dangerfiles): @@ -93,7 +95,9 @@ continue outf.write(entry[0] + ' ' + entry[1] + ' ') outf.write(b2a_qp(entry[2], True)) - outf.write(' %d %d %d %d %d\n' % entry[3:]) + outf.write(' ') + outf.write(b2a_qp(entry[3], True)) # symlink_target + outf.write(' %d %d %d %d %d %d\n' % entry[4:]) outf.commit() finally: @@ -114,10 +118,13 @@ for l in cachefile: f = l.split(' ') + if len(f) != CACHE_ENTRY_SIZE: + mutter("cache is in old format, must recreate it") + return {} file_id = f[0] if file_id in cache: raise BzrError("duplicated file_id in cache: {%s}" % file_id) - cache[file_id] = (f[0], f[1], a2b_qp(f[2])) + tuple([long(x) for x in f[3:]]) + cache[file_id] = (f[0], f[1], a2b_qp(f[2]), a2b_qp(f[3])) + tuple([long(x) for x in f[4:]]) return cache @@ -125,7 +132,7 @@ def _files_from_inventory(inv): for path, ie in inv.iter_entries(): - if ie.kind != 'file': + if ie.kind != 'file' and ie.kind != 'symlink': continue yield ie.file_id, path @@ -190,17 +197,24 @@ if (fp[FP_MTIME] >= now) or (fp[FP_CTIME] >= now): dangerfiles.add(file_id) - if cacheentry and (cacheentry[3:] == fp): + if cacheentry and (cacheentry[4:] == fp): continue # all stat fields unchanged hardcheck += 1 - dig = sha.new(file(abspath, 'rb').read()).hexdigest() - + mode = fp[FP_ST_MODE] + if stat.S_ISREG(mode): + link_target = '-' # can be anything, but must be non-empty + dig = sha.new(file(abspath, 'rb').read()).hexdigest() + elif stat.S_ISLNK(mode): + link_target = os.readlink(abspath) + dig = sha.new(link_target).hexdigest() + else: + raise BzrError("file %r: unknown file stat mode: %o"%(abspath,mode)) if cacheentry == None or dig != cacheentry[1]: # if there was no previous entry for this file, or if the # SHA has changed, then update the cache - cacheentry = (file_id, dig, path) + fp + cacheentry = (file_id, dig, path, link_target) + fp cache[file_id] = cacheentry change_cnt += 1 *** modified file 'bzrlib/tree.py' --- bzrlib/tree.py +++ bzrlib/tree.py @@ -125,6 +125,11 @@ os.mkdir(fullpath) elif kind == 'file': pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb')) + elif kind == 'symlink': + try: + os.symlink(ie.symlink_target, fullpath) + except OSError,e: + bailout("Failed to create symlink %r -> %r, error: %s" % (fullpath, ie.symlink_target, e)) else: bailout("don't know how to export {%s} of kind %r" % (ie.file_id, kind)) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) @@ -167,6 +172,9 @@ for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id + def get_symlink_target(self, file_id): + ie = self._inventory[file_id] + return ie.symlink_target; class EmptyTree(Tree): def __init__(self): @@ -179,7 +187,8 @@ if False: # just to make it a generator yield None - + def get_symlink_target(self, file_id): + return None ###################################################################### # diff @@ -245,3 +254,7 @@ if old_name != new_name: yield (old_name, new_name) + + def get_symlink_target(self, file_id): + ie = self._inventory[file_id] + return ie.symlink_target *** modified file 'bzrlib/workingtree.py' --- bzrlib/workingtree.py +++ bzrlib/workingtree.py @@ -53,7 +53,7 @@ ie = inv[file_id] if ie.kind == 'file': if ((file_id in self._statcache) - or (os.path.exists(self.abspath(inv.id2path(file_id))))): + or (bzrlib.osutils.lexists(self.abspath(inv.id2path(file_id))))): yield file_id @@ -66,7 +66,7 @@ return os.path.join(self.basedir, filename) def has_filename(self, filename): - return os.path.exists(self.abspath(filename)) + return bzrlib.osutils.lexists(self.abspath(filename)) def get_file(self, file_id): return self.get_file_byname(self.id2path(file_id)) @@ -86,7 +86,7 @@ self._update_statcache() if file_id in self._statcache: return True - return os.path.exists(self.abspath(self.id2path(file_id))) + return bzrlib.osutils.lexists(self.abspath(self.id2path(file_id))) __contains__ = has_id @@ -108,6 +108,12 @@ return self._statcache[file_id][statcache.SC_SHA1] + def get_symlink_target(self, file_id): + import statcache + self._update_statcache() + target = self._statcache[file_id][statcache.SC_SYMLINK_TARGET] + return target + def file_class(self, filename): if self.path2id(filename): return 'V' *** modified file 'testbzr' --- testbzr +++ testbzr @@ -1,4 +1,4 @@ -#! /usr/bin/python +#! /usr/bin/env python # Copyright (C) 2005 Canonical Ltd @@ -113,6 +113,17 @@ logfile.write(' at %s:%d\n' % stack[:2]) +def has_symlinks(): + import os; + if hasattr(os, 'symlink'): + return True + else: + return False + +def listdir_sorted(dir): + L = os.listdir(dir) + L.sort() + return L # prepare an empty scratch directory if os.path.exists(TESTDIR): @@ -320,8 +331,105 @@ runcmd('bzr ignore *.blah') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rt').read() == '*.blah\n' - - + cd("..") + + if has_symlinks(): + progress("symlinks") + mkdir('symlinks') + cd('symlinks') + runcmd('bzr init') + os.symlink("NOWHERE1", "link1") + runcmd('bzr add link1') + assert backtick('bzr unknowns') == '' + runcmd(['bzr', 'commit', '-m', '1: added symlink link1']) + + mkdir('d1') + runcmd('bzr add d1') + assert backtick('bzr unknowns') == '' + os.symlink("NOWHERE2", "d1/link2") + assert backtick('bzr unknowns') == 'd1/link2\n' + # is d1/link2 found when adding d1 + runcmd('bzr add d1') + assert backtick('bzr unknowns') == '' + os.symlink("NOWHERE3", "d1/link3") + assert backtick('bzr unknowns') == 'd1/link3\n' + runcmd(['bzr', 'commit', '-m', '2: added dir, symlink']) + + runcmd('bzr rename d1 d2') + runcmd('bzr move d2/link2 .') + runcmd('bzr move link1 d2') + assert os.readlink("./link2") == "NOWHERE2" + assert os.readlink("d2/link1") == "NOWHERE1" + runcmd('bzr add d2/link3') + runcmd('bzr diff') + runcmd(['bzr', 'commit', '-m', '3: rename of dir, move symlinks, add link3']) + + os.unlink("link2") + os.symlink("TARGET 2", "link2") + os.unlink("d2/link1") + os.symlink("TARGET 1", "d2/link1") + runcmd('bzr diff') + assert backtick("bzr relpath d2/link1") == "d2/link1\n" + runcmd(['bzr', 'commit', '-m', '4: retarget of two links']) + + runcmd('bzr remove d2/link1') + assert backtick('bzr unknowns') == 'd2/link1\n' + runcmd(['bzr', 'commit', '-m', '5: remove d2/link1']) + + os.mkdir("d1") + runcmd('bzr add d1') + runcmd('bzr rename d2/link3 d1/link3new') + assert backtick('bzr unknowns') == 'd2/link1\n' + runcmd(['bzr', 'commit', '-m', '6: remove d2/link1, move/rename link3']) + + runcmd(['bzr', 'check']) + + runcmd(['bzr', 'export', '-r', '1', 'exp1.tmp']) + cd("exp1.tmp") + assert listdir_sorted(".") == [ "link1" ] + assert os.readlink("link1") == "NOWHERE1" + cd("..") + + runcmd(['bzr', 'export', '-r', '2', 'exp2.tmp']) + cd("exp2.tmp") + assert listdir_sorted(".") == [ "d1", "link1" ] + cd("..") + + runcmd(['bzr', 'export', '-r', '3', 'exp3.tmp']) + cd("exp3.tmp") + assert listdir_sorted(".") == [ "d2", "link2" ] + assert listdir_sorted("d2") == [ "link1", "link3" ] + assert os.readlink("d2/link1") == "NOWHERE1" + assert os.readlink("link2") == "NOWHERE2" + cd("..") + + runcmd(['bzr', 'export', '-r', '4', 'exp4.tmp']) + cd("exp4.tmp") + assert listdir_sorted(".") == [ "d2", "link2" ] + assert os.readlink("d2/link1") == "TARGET 1" + assert os.readlink("link2") == "TARGET 2" + assert listdir_sorted("d2") == [ "link1", "link3" ] + cd("..") + + runcmd(['bzr', 'export', '-r', '5', 'exp5.tmp']) + cd("exp5.tmp") + assert listdir_sorted(".") == [ "d2", "link2" ] + assert os.path.islink("link2") + assert listdir_sorted("d2")== [ "link3" ] + cd("..") + + runcmd(['bzr', 'export', '-r', '6', 'exp6.tmp']) + cd("exp6.tmp") + assert listdir_sorted(".") == [ "d1", "d2", "link2" ] + assert listdir_sorted("d1") == [ "link3new" ] + assert listdir_sorted("d2") == [] + assert os.readlink("d1/link3new") == "NOWHERE3" + cd("..") + + cd("..") + else: + progress("skipping symlink tests") + progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' --=-=-=-- commit refs/heads/master mark :522 committer Martin Pool 1116314086 +1000 data 4 todo from :521 M 644 inline TODO data 11281 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * ``bzr log DIR`` should give changes to any files within DIR. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. * Maybe also store directories in the statcache so that we can quickly identify that they still exist. Medium things ------------- * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Merge Aaron's merge code. * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. We should be able to just get the id for the selected files, look up their location and diff just those files. No need to traverse the entire inventories. * ``bzr status DIR`` or ``bzr diff DIR`` should report on all changes under that directory. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. - Selected-file commit - Impossible selected-file commit: adding things in non-versioned directories, crossing renames, etc. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Should be a signature at the top of the cache file. * Paranoid mode where we never trust SHA-1 matches. * Don't commit if there are no changes unless forced. * --dry-run mode for commit? (Or maybe just run with check-command=false?) * Generally, be a bit more verbose unless --silent is specified. * Function that finds all changes to files under a given directory; perhaps log should use this if a directory is given. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. Possibly this should be done by splitting the commit function into several parts (under a single interface). It is already rather large. Decomposition: - find tree modifications and prepare in-memory inventory - export that inventory to a temporary directory - run the test in that temporary directory - if that succeeded, continue to actually finish the commit What should be done with the text of modified files while this is underway? I don't think we want to count on holding them in memory and we can't trust the working files to stay in one place so I suppose we need to move them into the text store, or otherwise into a temporary directory. If the commit does not actually complete, we would rather the content was not left behind in the stores. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` M 644 inline bzrlib/log.py data 4927 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def find_touching_revisions(branch, file_id): """Yield a description of revisions which affect the file_id. Each returned element is (revno, revision_id, description) This is the list of revisions where the file is either added, modified, renamed or deleted. TODO: Perhaps some way to limit this to only particular revisions, or to traverse a non-mainline set of revisions? """ last_ie = None last_path = None revno = 1 for revision_id in branch.revision_history(): this_inv = branch.get_revision_inventory(revision_id) if file_id in this_inv: this_ie = this_inv[file_id] this_path = this_inv.id2path(file_id) else: this_ie = this_path = None # now we know how it was last time, and how it is in this revision. # are those two states effectively the same or not? if not this_ie and not last_ie: # not present in either pass elif this_ie and not last_ie: yield revno, revision_id, "added " + this_path elif not this_ie and last_ie: # deleted here yield revno, revision_id, "deleted " + last_path elif this_path != last_path: yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path)) elif (this_ie.text_size != last_ie.text_size or this_ie.text_sha1 != last_ie.text_sha1): yield revno, revision_id, "modified " + this_path last_ie = this_ie last_path = this_path revno += 1 def show_log(branch, filename=None, show_timezone='original', verbose=False, show_ids=False, to_file=None): """Write out human-readable log of commits to this branch. filename If true, list only the commits affecting the specified file, rather than all commits. show_timezone 'original' (committer's timezone), 'utc' (universal time), or 'local' (local user's timezone) verbose If true show added/changed/deleted/renamed files. show_ids If true, show revision and file ids. to_file File to send log to; by default stdout. """ from osutils import format_date from errors import BzrCheckError from diff import compare_trees from textui import show_status if to_file == None: import sys to_file = sys.stdout if filename: file_id = branch.read_working_inventory().path2id(filename) def which_revs(): for revno, revid, why in find_touching_revisions(branch, file_id): yield revno, revid else: def which_revs(): for i, revid in enumerate(branch.revision_history()): yield i+1, revid branch._need_readlock() precursor = None if verbose: from tree import EmptyTree prev_tree = EmptyTree() for revno, revision_id in which_revs(): print >>to_file, '-' * 60 print >>to_file, 'revno:', revno rev = branch.get_revision(revision_id) if show_ids: print >>to_file, 'revision-id:', revision_id print >>to_file, 'committer:', rev.committer print >>to_file, 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) if revision_id != rev.revision_id: raise BzrCheckError("retrieved wrong revision: %r" % (revision_id, rev.revision_id)) print >>to_file, 'message:' if not rev.message: print >>to_file, ' (no message)' else: for l in rev.message.split('\n'): print >>to_file, ' ' + l # Don't show a list of changed files if we were asked about # one specific file. if verbose: this_tree = branch.revision_tree(revision_id) delta = compare_trees(prev_tree, this_tree, want_unchanged=False) delta.show(to_file, show_ids) prev_tree = this_tree precursor = revision_id commit refs/heads/master mark :523 committer Martin Pool 1116314160 +1000 data 3 doc from :522 M 644 inline bzrlib/commands.py data 35731 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date from bzrlib import merge def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0], lock_mode='r') file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.', lock_mode='r') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, specific_files=file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Perhaps show most-recent first with an option for last. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from branch import find_branch b = find_branch((filename or '.'), lock_mode='r') if filename: filename = b.relpath(filename) bzrlib.show_log(b, filename, show_timezone=timezone, verbose=verbose, show_ids=show_ids) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures return 1 else: print return 0 class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): merge.merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :524 committer Martin Pool 1116314324 +1000 data 40 - add version marker at top of statcache from :523 M 644 inline bzrlib/statcache.py data 8025 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import stat, os, sha, time from binascii import b2a_qp, a2b_qp from trace import mutter from errors import BzrError, BzrCheckError """File stat cache to speed up tree comparisons. This module basically gives a quick way to find the SHA-1 and related information of a file in the working directory, without actually reading and hashing the whole file. Implementation ============== Users of this module should not need to know about how this is implemented, and in particular should not depend on the particular data which is stored or its format. This is done by maintaining a cache indexed by a file fingerprint of (path, size, mtime, ctime, ino, dev) pointing to the SHA-1. If the fingerprint has changed, we assume the file content has not changed either and the SHA-1 is therefore the same. If any of the fingerprint fields have changed then the file content *may* have changed, or it may not have. We need to reread the file contents to make sure, but this is not visible to the user or higher-level code (except as a delay of course). The mtime and ctime are stored with nanosecond fields, but not all filesystems give this level of precision. There is therefore a possible race: the file might be modified twice within a second without changing the size or mtime, and a SHA-1 cached from the first version would be wrong. We handle this by not recording a cached hash for any files which were modified in the current second and that therefore have the chance to change again before the second is up. The only known hole in this design is if the system clock jumps backwards crossing invocations of bzr. Please don't do that; use ntp to gradually adjust your clock or don't use bzr over the step. At the moment this is stored in a simple textfile; it might be nice to use a tdb instead. The cache is represented as a map from file_id to a tuple of (file_id, sha1, path, size, mtime, ctime, ino, dev). The SHA-1 is stored in memory as a hexdigest. File names are written out as the quoted-printable encoding of their UTF-8 representation. """ # order of fields returned by fingerprint() FP_SIZE = 0 FP_MTIME = 1 FP_CTIME = 2 FP_INO = 3 FP_DEV = 4 # order of fields in the statcache file and in the in-memory map SC_FILE_ID = 0 SC_SHA1 = 1 SC_PATH = 2 SC_SIZE = 3 SC_MTIME = 4 SC_CTIME = 5 SC_INO = 6 SC_DEV = 7 CACHE_HEADER = "### bzr statcache v2" def fingerprint(abspath): try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) def _write_cache(basedir, entry_iter, dangerfiles): from atomicfile import AtomicFile cachefn = os.path.join(basedir, '.bzr', 'stat-cache') outf = AtomicFile(cachefn, 'wb') outf.write(CACHE_HEADER + '\n') try: for entry in entry_iter: if len(entry) != 8: raise ValueError("invalid statcache entry tuple %r" % entry) if entry[SC_FILE_ID] in dangerfiles: continue # changed too recently outf.write(entry[0]) # file id outf.write(' ') outf.write(entry[1]) # hex sha1 outf.write(' ') outf.write(b2a_qp(entry[2].encode('utf-8'), True)) # name for nf in entry[3:]: outf.write(' %d' % nf) outf.write('\n') outf.commit() finally: if not outf.closed: outf.abort() def load_cache(basedir): from sets import Set cache = {} seen_paths = Set() try: cachefn = os.path.join(basedir, '.bzr', 'stat-cache') cachefile = open(cachefn, 'rb') except IOError: return cache line1 = cachefile.readline().rstrip('\r\n') if line1 != CACHE_HEADER: mutter('cache header marker not found at top of %s' % cachefn) return cache for l in cachefile: f = l.split(' ') file_id = f[0] if file_id in cache: raise BzrError("duplicated file_id in cache: {%s}" % file_id) path = a2b_qp(f[2]).decode('utf-8') if path in seen_paths: raise BzrCheckError("duplicated path in cache: %r" % path) seen_paths.add(path) entry = (file_id, f[1], path) + tuple([long(x) for x in f[3:]]) if len(entry) != 8: raise ValueError("invalid statcache entry tuple %r" % entry) cache[file_id] = entry return cache def _files_from_inventory(inv): for path, ie in inv.iter_entries(): if ie.kind != 'file': continue yield ie.file_id, path def update_cache(basedir, inv, flush=False): """Update and return the cache for the branch. The returned cache may contain entries that have not been written to disk for files recently touched. flush -- discard any previous cache and recalculate from scratch. """ # TODO: It's supposed to be faster to stat the files in order by inum. # We don't directly know the inum of the files of course but we do # know where they were last sighted, so we can sort by that. assert isinstance(flush, bool) if flush: cache = {} else: cache = load_cache(basedir) return _update_cache_from_list(basedir, cache, _files_from_inventory(inv)) def _update_cache_from_list(basedir, cache, to_update): """Update and return the cache for given files. cache -- Previously cached values to be validated. to_update -- Sequence of (file_id, path) pairs to check. """ from sets import Set stat_cnt = missing_cnt = hardcheck = change_cnt = 0 # files that have been recently touched and can't be # committed to a persistent cache yet. dangerfiles = Set() now = int(time.time()) ## mutter('update statcache under %r' % basedir) for file_id, path in to_update: abspath = os.path.join(basedir, path) fp = fingerprint(abspath) stat_cnt += 1 cacheentry = cache.get(file_id) if fp == None: # not here if cacheentry: del cache[file_id] change_cnt += 1 missing_cnt += 1 continue if (fp[FP_MTIME] >= now) or (fp[FP_CTIME] >= now): dangerfiles.add(file_id) if cacheentry and (cacheentry[3:] == fp): continue # all stat fields unchanged hardcheck += 1 dig = sha.new(file(abspath, 'rb').read()).hexdigest() # We update the cache even if the digest has not changed from # last time we looked, so that the fingerprint fields will # match in future. cacheentry = (file_id, dig, path) + fp cache[file_id] = cacheentry change_cnt += 1 mutter('statcache: statted %d files, read %d files, %d changed, %d dangerous, ' '%d in cache' % (stat_cnt, hardcheck, change_cnt, len(dangerfiles), len(cache))) if change_cnt: mutter('updating on-disk statcache') _write_cache(basedir, cache.itervalues(), dangerfiles) return cache commit refs/heads/master mark :525 committer Martin Pool 1116316311 +1000 data 27 - export bzrlib.find_branch from :524 M 644 inline bzrlib/__init__.py data 1747 # (C) 2005 Canonical Development Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr library""" from inventory import Inventory, InventoryEntry from branch import Branch, ScratchBranch, find_branch from osutils import format_date from tree import Tree from diff import compare_trees from trace import mutter, warning, open_tracefile from log import show_log import add BZRDIR = ".bzr" DEFAULT_IGNORE = ['.bzr.log', '*~', '#*#', '*$', '.#*', '.*.swp', '.*.tmp', '*.tmp', '*.bak', '*.BAK', '*.orig', '*.o', '*.obj', '*.a', '*.py[oc]', '*.so', '*.exe', '*.elc', '{arch}', 'CVS', 'CVS.adm', '.svn', '_darcs', 'SCCS', 'RCS', '*,v', 'BitKeeper', 'TAGS', '.make.state', '.sconsign', '.tmp*', '.del-*'] IGNORE_FILENAME = ".bzrignore" import locale user_encoding = locale.getpreferredencoding() __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __version__ = '0.0.5pre' commit refs/heads/master mark :526 committer Martin Pool 1116316336 +1000 data 44 - use ValueError for bad internal parameters from :525 M 644 inline bzrlib/branch.py data 26578 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. base Base directory of the branch. """ _lockmode = None def __init__(self, base, init=False, find_root=True, lock_mode='w'): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.lock(lock_mode) self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def lock(self, mode='w'): """Lock the on-disk branch, excluding other processes.""" try: import fcntl, errno if mode == 'w': lm = fcntl.LOCK_EX om = os.O_WRONLY | os.O_CREAT elif mode == 'r': lm = fcntl.LOCK_SH om = os.O_RDONLY else: raise BzrError("invalid locking mode %r" % mode) try: lockfile = os.open(self.controlfilename('branch-lock'), om) except OSError, e: if e.errno == errno.ENOENT: # might not exist on branches from <0.0.4 self.controlfile('branch-lock', 'w').close() lockfile = os.open(self.controlfilename('branch-lock'), om) else: raise e fcntl.lockf(lockfile, lm) def unlock(): fcntl.lockf(lockfile, fcntl.LOCK_UN) os.close(lockfile) self._lockmode = None self.unlock = unlock self._lockmode = mode except ImportError: warning("please write a locking method for platform %r" % sys.platform) def unlock(): self._lockmode = None self.unlock = unlock self._lockmode = mode def _need_readlock(self): if self._lockmode not in ['r', 'w']: raise BzrError('need read lock on branch, only have %r' % self._lockmode) def _need_writelock(self): if self._lockmode not in ['w']: raise BzrError('need write lock on branch, only have %r' % self._lockmode) def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" self._need_readlock() before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self._need_writelock() ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ self._need_writelock() # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" self._need_readlock() tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability self._need_writelock() if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" self._need_readlock() r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" self._need_readlock() i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" self._need_readlock() if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self._need_readlock() return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def commit(self, *args, **kw): """Deprecated""" from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" self._need_readlock() if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self._need_writelock() tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self._need_writelock() ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :527 committer Martin Pool 1116316794 +1000 data 144 - refactor log command - log runs from most-recent to least now (reverse from previously) - log -v and log on specific file temporarily broken from :526 M 644 inline NEWS data 7512 bzr-0.0.5 NOT RELEASED YET CHANGES: * ``bzr`` with no command now shows help rather than giving an error. Suggested by Michael Ellerman. * ``bzr status`` output format changed, because svn-style output doesn't really match the model of bzr. Now files are grouped by status and can be shown with their IDs. ``bzr status --all`` shows all versioned files and unknown files but not ignored files. * ``bzr log`` runs from most-recent to least-recent, the reverse of the previous order. ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New ``bzr ignore PATTERN`` command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. File may be in a branch other than the working directory. * ``bzr log`` and ``bzr root`` can be given an http URL instead of a filename. * Commands can now be defined by external programs or scripts in a directory on $BZRPATH. * New "stat cache" avoids reading the contents of files if they haven't changed since the previous time. * If the Python interpreter is too old, try to find a better one or give an error. Based on a patch from Fredrik Lundh. * New optional parameter ``bzr info [BRANCH]``. * New form ``bzr commit SELECTED`` to commit only selected files. BUG FIXES: * Fixed diff format so that added and removed files will be handled properly by patch. Fix from Lalo Martins. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. * testbzr requires python2.4, but can be used to test bzr running under a different version. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. * New developer commands ``added``, ``modified``. PORTABILITY: * Cope on Windows on python2.3 by using the weaker random seed. 2.4 is now only recommended. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 35875 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date from bzrlib import merge def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0], lock_mode='r') file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.', lock_mode='r') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, specific_files=file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Option to show in forward order. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from bzrlib import show_log, find_branch if filename: b = find_branch(filename, lock_mode='r') fp = b.relpath(filename) file_id = b.read_working_inventory().path2id(fp) else: b = find_branch('.', lock_mode='r') file_id = None show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=sys.stdout) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store, bzrlib.tests bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures return 1 else: print return 0 class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): merge.merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/log.py data 5874 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Code to show logs of changes. Various flavors of log can be produced: * for one file, or the whole tree, and (not done yet) for files in a given directory * in "verbose" mode with a description of what changed from one version to the next * with file-ids and revision-ids shown * from last to first or (not anymore) from first to last; the default is "reversed" because it shows the likely most relevant and interesting information first * (not yet) in XML format """ def find_touching_revisions(branch, file_id): """Yield a description of revisions which affect the file_id. Each returned element is (revno, revision_id, description) This is the list of revisions where the file is either added, modified, renamed or deleted. TODO: Perhaps some way to limit this to only particular revisions, or to traverse a non-mainline set of revisions? """ last_ie = None last_path = None revno = 1 for revision_id in branch.revision_history(): this_inv = branch.get_revision_inventory(revision_id) if file_id in this_inv: this_ie = this_inv[file_id] this_path = this_inv.id2path(file_id) else: this_ie = this_path = None # now we know how it was last time, and how it is in this revision. # are those two states effectively the same or not? if not this_ie and not last_ie: # not present in either pass elif this_ie and not last_ie: yield revno, revision_id, "added " + this_path elif not this_ie and last_ie: # deleted here yield revno, revision_id, "deleted " + last_path elif this_path != last_path: yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path)) elif (this_ie.text_size != last_ie.text_size or this_ie.text_sha1 != last_ie.text_sha1): yield revno, revision_id, "modified " + this_path last_ie = this_ie last_path = this_path revno += 1 def show_log(branch, specific_fileid=None, show_timezone='original', verbose=False, show_ids=False, to_file=None, direction='reverse'): """Write out human-readable log of commits to this branch. specific_fileid If true, list only the commits affecting the specified file, rather than all commits. show_timezone 'original' (committer's timezone), 'utc' (universal time), or 'local' (local user's timezone) verbose If true show added/changed/deleted/renamed files. show_ids If true, show revision and file ids. to_file File to send log to; by default stdout. direction 'reverse' (default) is latest to earliest; 'forward' is earliest to latest. """ from osutils import format_date from errors import BzrCheckError from diff import compare_trees from textui import show_status if to_file == None: import sys to_file = sys.stdout if specific_fileid or verbose: raise NotImplementedError('sorry, option not implemented at the moment') which_revs = branch.enum_history(direction) for revno, revision_id in which_revs: # TODO: if filename given, check if it's changed; if not # changed, skip this one # TODO: if verbose, get a list of changes; if we're running # forward then the delta is as compared to the previous # version, otherwise as compared to the *next* version to be # enumerated; in both cases must treat 0 specially as the # empty tree. rev = branch.get_revision(revision_id) delta = None show_one_log(revno, rev, delta, show_ids, to_file, show_timezone) def junk(): precursor = None if verbose: from tree import EmptyTree prev_tree = EmptyTree() for revno, revision_id in which_revs: precursor = revision_id if revision_id != rev.revision_id: raise BzrCheckError("retrieved wrong revision: %r" % (revision_id, rev.revision_id)) if verbose: this_tree = branch.revision_tree(revision_id) delta = compare_trees(prev_tree, this_tree, want_unchanged=False) prev_tree = this_tree else: delta = None def show_one_log(revno, rev, delta, show_ids, to_file, show_timezone): from osutils import format_date print >>to_file, '-' * 60 print >>to_file, 'revno:', revno if show_ids: print >>to_file, 'revision-id:', rev.revision_id print >>to_file, 'committer:', rev.committer print >>to_file, 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) print >>to_file, 'message:' if not rev.message: print >>to_file, ' (no message)' else: for l in rev.message.split('\n'): print >>to_file, ' ' + l if delta != None: delta.show(to_file, show_ids) commit refs/heads/master mark :528 committer Martin Pool 1116316832 +1000 data 55 - fix bzr selftest trying to import non-existant module from :527 M 644 inline bzrlib/commands.py data 35861 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date from bzrlib import merge def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0], lock_mode='r') file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.', lock_mode='r') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, specific_files=file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Option to show in forward order. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from bzrlib import show_log, find_branch if filename: b = find_branch(filename, lock_mode='r') fp = b.relpath(filename) file_id = b.read_working_inventory().path2id(fp) else: b = find_branch('.', lock_mode='r') file_id = None show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=sys.stdout) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures return 1 else: print return 0 class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): merge.merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :529 committer Martin Pool 1116317268 +1000 data 4 todo from :528 M 644 inline bzrlib/branch.py data 26691 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. base Base directory of the branch. """ _lockmode = None def __init__(self, base, init=False, find_root=True, lock_mode='w'): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.lock(lock_mode) self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def lock(self, mode='w'): """Lock the on-disk branch, excluding other processes.""" try: import fcntl, errno if mode == 'w': lm = fcntl.LOCK_EX om = os.O_WRONLY | os.O_CREAT elif mode == 'r': lm = fcntl.LOCK_SH om = os.O_RDONLY else: raise BzrError("invalid locking mode %r" % mode) try: lockfile = os.open(self.controlfilename('branch-lock'), om) except OSError, e: if e.errno == errno.ENOENT: # might not exist on branches from <0.0.4 self.controlfile('branch-lock', 'w').close() lockfile = os.open(self.controlfilename('branch-lock'), om) else: raise e fcntl.lockf(lockfile, lm) def unlock(): fcntl.lockf(lockfile, fcntl.LOCK_UN) os.close(lockfile) self._lockmode = None self.unlock = unlock self._lockmode = mode except ImportError: warning("please write a locking method for platform %r" % sys.platform) def unlock(): self._lockmode = None self.unlock = unlock self._lockmode = mode def _need_readlock(self): if self._lockmode not in ['r', 'w']: raise BzrError('need read lock on branch, only have %r' % self._lockmode) def _need_writelock(self): if self._lockmode not in ['w']: raise BzrError('need write lock on branch, only have %r' % self._lockmode) def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" self._need_readlock() before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self._need_writelock() ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ self._need_writelock() # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" self._need_readlock() tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability self._need_writelock() if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" self._need_readlock() r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" self._need_readlock() i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" self._need_readlock() if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self._need_readlock() return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def commit(self, *args, **kw): """Deprecated""" from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. self._need_readlock() if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self._need_writelock() tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self._need_writelock() ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] name = name.lstrip('.') s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :530 committer Martin Pool 1116317880 +1000 data 48 - put back verbose log support for reversed logs from :529 M 644 inline bzrlib/log.py data 6912 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Code to show logs of changes. Various flavors of log can be produced: * for one file, or the whole tree, and (not done yet) for files in a given directory * in "verbose" mode with a description of what changed from one version to the next * with file-ids and revision-ids shown * from last to first or (not anymore) from first to last; the default is "reversed" because it shows the likely most relevant and interesting information first * (not yet) in XML format """ def find_touching_revisions(branch, file_id): """Yield a description of revisions which affect the file_id. Each returned element is (revno, revision_id, description) This is the list of revisions where the file is either added, modified, renamed or deleted. TODO: Perhaps some way to limit this to only particular revisions, or to traverse a non-mainline set of revisions? """ last_ie = None last_path = None revno = 1 for revision_id in branch.revision_history(): this_inv = branch.get_revision_inventory(revision_id) if file_id in this_inv: this_ie = this_inv[file_id] this_path = this_inv.id2path(file_id) else: this_ie = this_path = None # now we know how it was last time, and how it is in this revision. # are those two states effectively the same or not? if not this_ie and not last_ie: # not present in either pass elif this_ie and not last_ie: yield revno, revision_id, "added " + this_path elif not this_ie and last_ie: # deleted here yield revno, revision_id, "deleted " + last_path elif this_path != last_path: yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path)) elif (this_ie.text_size != last_ie.text_size or this_ie.text_sha1 != last_ie.text_sha1): yield revno, revision_id, "modified " + this_path last_ie = this_ie last_path = this_path revno += 1 def show_log(branch, specific_fileid=None, show_timezone='original', verbose=False, show_ids=False, to_file=None, direction='reverse'): """Write out human-readable log of commits to this branch. specific_fileid If true, list only the commits affecting the specified file, rather than all commits. show_timezone 'original' (committer's timezone), 'utc' (universal time), or 'local' (local user's timezone) verbose If true show added/changed/deleted/renamed files. show_ids If true, show revision and file ids. to_file File to send log to; by default stdout. direction 'reverse' (default) is latest to earliest; 'forward' is earliest to latest. """ from osutils import format_date from errors import BzrCheckError from textui import show_status if to_file == None: import sys to_file = sys.stdout if specific_fileid: raise NotImplementedError('sorry, option not implemented at the moment') which_revs = branch.enum_history(direction) if not verbose: # no actual deltas generated with_deltas = deltas_for_log_dummy(branch, which_revs) elif direction == 'reverse': with_deltas = deltas_for_log_reverse(branch, which_revs) else: raise NotImplementedError("sorry, verbose forward logs not done yet") for revno, rev, delta in with_deltas: # TODO: if filename given, check if it's changed; if not # changed, skip this one show_one_log(revno, rev, delta, show_ids, to_file, show_timezone) def deltas_for_log_dummy(branch, which_revs): for revno, revision_id in which_revs: yield revno, branch.get_revision(revision_id), None def deltas_for_log_reverse(branch, which_revs): """Compute deltas for display in reverse log. Given a sequence of (revno, revision_id) pairs, return (revno, rev, delta). The delta is from the given revision to the next one in the sequence, which makes sense if the log is being displayed from newest to oldest. """ from tree import EmptyTree from diff import compare_trees last_revno = last_revision_id = last_tree = None for revno, revision_id in which_revs: this_tree = branch.revision_tree(revision_id) this_revision = branch.get_revision(revision_id) if last_revno: yield last_revno, last_revision, compare_trees(this_tree, last_tree, False) last_revno = revno last_revision = this_revision last_tree = this_tree if last_revno: this_tree = EmptyTree() yield last_revno, last_revision, compare_trees(this_tree, last_tree, False) def junk(): precursor = None if verbose: from tree import EmptyTree prev_tree = EmptyTree() for revno, revision_id in which_revs: precursor = revision_id if revision_id != rev.revision_id: raise BzrCheckError("retrieved wrong revision: %r" % (revision_id, rev.revision_id)) if verbose: this_tree = branch.revision_tree(revision_id) delta = compare_trees(prev_tree, this_tree, want_unchanged=False) prev_tree = this_tree else: delta = None def show_one_log(revno, rev, delta, show_ids, to_file, show_timezone): from osutils import format_date print >>to_file, '-' * 60 print >>to_file, 'revno:', revno if show_ids: print >>to_file, 'revision-id:', rev.revision_id print >>to_file, 'committer:', rev.committer print >>to_file, 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) print >>to_file, 'message:' if not rev.message: print >>to_file, ' (no message)' else: for l in rev.message.split('\n'): print >>to_file, ' ' + l if delta != None: delta.show(to_file, show_ids) commit refs/heads/master mark :531 committer Martin Pool 1116318031 +1000 data 39 - new utility TreeDelta.touches_file_id from :530 M 644 inline bzrlib/diff.py data 10011 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set, ImmutableSet from trace import mutter from errors import BzrError def _diff_one(oldlines, newlines, to_file, **kw): import difflib # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') to_file.writelines(ud) if nonl: print >>to_file, "\\ No newline at end of file" print >>to_file def show_diff(b, revision, specific_files): import sys if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. delta = compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=specific_files) for path, file_id, kind in delta.removed: print '*** removed %s %r' % (kind, path) if kind == 'file': _diff_one(old_tree.get_file(file_id).readlines(), [], sys.stdout, fromfile=old_label + path, tofile=DEVNULL) for path, file_id, kind in delta.added: print '*** added %s %r' % (kind, path) if kind == 'file': _diff_one([], new_tree.get_file(file_id).readlines(), sys.stdout, fromfile=DEVNULL, tofile=new_label + path) for old_path, new_path, file_id, kind, text_modified in delta.renamed: print '*** renamed %s %r => %r' % (kind, old_path, new_path) if text_modified: _diff_one(old_tree.get_file(file_id).readlines(), new_tree.get_file(file_id).readlines(), sys.stdout, fromfile=old_label + old_path, tofile=new_label + new_path) for path, file_id, kind in delta.modified: print '*** modified %s %r' % (kind, path) if kind == 'file': _diff_one(old_tree.get_file(file_id).readlines(), new_tree.get_file(file_id).readlines(), sys.stdout, fromfile=old_label + path, tofile=new_label + path) class TreeDelta: """Describes changes from one tree to another. Contains four lists: added (path, id, kind) removed (path, id, kind) renamed (oldpath, newpath, id, kind, text_modified) modified (path, id, kind) unchanged (path, id, kind) Each id is listed only once. Files that are both modified and renamed are listed only in renamed, with the text_modified flag true. The lists are normally sorted when the delta is created. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] self.unchanged = [] def touches_file_id(self, file_id): """Return True if file_id is modified by this delta.""" for l in self.added, self.removed, self.modified: for v in l: if v[1] == file_id: return True for v in self.renamed: if v[2] == file_id: return True return False def show(self, to_file, show_ids=False, show_unchanged=False): def show_list(files): for path, fid, kind in files: if kind == 'directory': path += '/' elif kind == 'symlink': path += '@' if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if self.removed: print >>to_file, 'removed:' show_list(self.removed) if self.added: print >>to_file, 'added:' show_list(self.added) if self.renamed: print >>to_file, 'renamed:' for oldpath, newpath, fid, kind, text_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if self.modified: print >>to_file, 'modified:' show_list(self.modified) if show_unchanged and self.unchanged: print >>to_file, 'unchanged:' show_list(self.unchanged) def compare_trees(old_tree, new_tree, want_unchanged, specific_files=None): """Describe changes from one tree to another. Returns a TreeDelta with details of added, modified, renamed, and deleted entries. The root entry is specifically exempt. This only considers versioned files. want_unchanged If true, also list files unchanged from one version to the next. specific_files If true, only check for changes to specified names or files within them. """ from osutils import is_inside_any old_inv = old_tree.inventory new_inv = new_tree.inventory delta = TreeDelta() mutter('start compare_trees') # TODO: match for specific files can be rather smarter by finding # the IDs of those files up front and then considering only that. for file_id in old_tree: if file_id in new_tree: kind = old_inv.get_file_kind(file_id) assert kind == new_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ 'invalid file kind %r' % kind if kind == 'root_directory': continue old_path = old_inv.id2path(file_id) new_path = new_inv.id2path(file_id) if specific_files: if (not is_inside_any(specific_files, old_path) and not is_inside_any(specific_files, new_path)): continue if kind == 'file': old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False # TODO: Can possibly avoid calculating path strings if the # two files are unchanged and their names and parents are # the same and the parents are unchanged all the way up. # May not be worthwhile. if old_path != new_path: delta.renamed.append((old_path, new_path, file_id, kind, text_modified)) elif text_modified: delta.modified.append((new_path, file_id, kind)) elif want_unchanged: delta.unchanged.append((new_path, file_id, kind)) else: old_path = old_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, old_path): continue delta.removed.append((old_path, file_id, kind)) mutter('start looking for new files') for file_id in new_inv: if file_id in old_inv: continue new_path = new_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, new_path): continue kind = new_inv.get_file_kind(file_id) delta.added.append((new_path, file_id, kind)) delta.removed.sort() delta.added.sort() delta.renamed.sort() delta.modified.sort() delta.unchanged.sort() return delta commit refs/heads/master mark :532 committer Martin Pool 1116318208 +1000 data 44 - put back support for log on specific files from :531 M 644 inline bzrlib/log.py data 6999 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Code to show logs of changes. Various flavors of log can be produced: * for one file, or the whole tree, and (not done yet) for files in a given directory * in "verbose" mode with a description of what changed from one version to the next * with file-ids and revision-ids shown * from last to first or (not anymore) from first to last; the default is "reversed" because it shows the likely most relevant and interesting information first * (not yet) in XML format """ def find_touching_revisions(branch, file_id): """Yield a description of revisions which affect the file_id. Each returned element is (revno, revision_id, description) This is the list of revisions where the file is either added, modified, renamed or deleted. TODO: Perhaps some way to limit this to only particular revisions, or to traverse a non-mainline set of revisions? """ last_ie = None last_path = None revno = 1 for revision_id in branch.revision_history(): this_inv = branch.get_revision_inventory(revision_id) if file_id in this_inv: this_ie = this_inv[file_id] this_path = this_inv.id2path(file_id) else: this_ie = this_path = None # now we know how it was last time, and how it is in this revision. # are those two states effectively the same or not? if not this_ie and not last_ie: # not present in either pass elif this_ie and not last_ie: yield revno, revision_id, "added " + this_path elif not this_ie and last_ie: # deleted here yield revno, revision_id, "deleted " + last_path elif this_path != last_path: yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path)) elif (this_ie.text_size != last_ie.text_size or this_ie.text_sha1 != last_ie.text_sha1): yield revno, revision_id, "modified " + this_path last_ie = this_ie last_path = this_path revno += 1 def show_log(branch, specific_fileid=None, show_timezone='original', verbose=False, show_ids=False, to_file=None, direction='reverse'): """Write out human-readable log of commits to this branch. specific_fileid If true, list only the commits affecting the specified file, rather than all commits. show_timezone 'original' (committer's timezone), 'utc' (universal time), or 'local' (local user's timezone) verbose If true show added/changed/deleted/renamed files. show_ids If true, show revision and file ids. to_file File to send log to; by default stdout. direction 'reverse' (default) is latest to earliest; 'forward' is earliest to latest. """ from osutils import format_date from errors import BzrCheckError from textui import show_status if to_file == None: import sys to_file = sys.stdout which_revs = branch.enum_history(direction) if not (verbose or specific_fileid): # no need to know what changed between revisions with_deltas = deltas_for_log_dummy(branch, which_revs) elif direction == 'reverse': with_deltas = deltas_for_log_reverse(branch, which_revs) else: raise NotImplementedError("sorry, verbose forward logs not done yet") for revno, rev, delta in with_deltas: if specific_fileid: if not delta.touches_file_id(specific_fileid): continue if not verbose: # although we calculated it, throw it away without display delta = None show_one_log(revno, rev, delta, show_ids, to_file, show_timezone) def deltas_for_log_dummy(branch, which_revs): for revno, revision_id in which_revs: yield revno, branch.get_revision(revision_id), None def deltas_for_log_reverse(branch, which_revs): """Compute deltas for display in reverse log. Given a sequence of (revno, revision_id) pairs, return (revno, rev, delta). The delta is from the given revision to the next one in the sequence, which makes sense if the log is being displayed from newest to oldest. """ from tree import EmptyTree from diff import compare_trees last_revno = last_revision_id = last_tree = None for revno, revision_id in which_revs: this_tree = branch.revision_tree(revision_id) this_revision = branch.get_revision(revision_id) if last_revno: yield last_revno, last_revision, compare_trees(this_tree, last_tree, False) last_revno = revno last_revision = this_revision last_tree = this_tree if last_revno: this_tree = EmptyTree() yield last_revno, last_revision, compare_trees(this_tree, last_tree, False) def junk(): precursor = None if verbose: from tree import EmptyTree prev_tree = EmptyTree() for revno, revision_id in which_revs: precursor = revision_id if revision_id != rev.revision_id: raise BzrCheckError("retrieved wrong revision: %r" % (revision_id, rev.revision_id)) if verbose: this_tree = branch.revision_tree(revision_id) delta = compare_trees(prev_tree, this_tree, want_unchanged=False) prev_tree = this_tree else: delta = None def show_one_log(revno, rev, delta, show_ids, to_file, show_timezone): from osutils import format_date print >>to_file, '-' * 60 print >>to_file, 'revno:', revno if show_ids: print >>to_file, 'revision-id:', rev.revision_id print >>to_file, 'committer:', rev.committer print >>to_file, 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) print >>to_file, 'message:' if not rev.message: print >>to_file, ' (no message)' else: for l in rev.message.split('\n'): print >>to_file, ' ' + l if delta != None: delta.show(to_file, show_ids) commit refs/heads/master mark :533 committer Martin Pool 1116318423 +1000 data 59 - fix up asking for the log for the root of a remote branch from :532 M 644 inline bzrlib/commands.py data 35958 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path from sets import Set import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date from bzrlib import merge def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0], lock_mode='r') file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.', lock_mode='r') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, specific_files=file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Option to show in forward order. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from bzrlib import show_log, find_branch if filename: b = find_branch(filename, lock_mode='r') fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.', lock_mode='r') file_id = None show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=sys.stdout) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures return 1 else: print return 0 class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): merge.merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/log.py data 7084 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Code to show logs of changes. Various flavors of log can be produced: * for one file, or the whole tree, and (not done yet) for files in a given directory * in "verbose" mode with a description of what changed from one version to the next * with file-ids and revision-ids shown * from last to first or (not anymore) from first to last; the default is "reversed" because it shows the likely most relevant and interesting information first * (not yet) in XML format """ def find_touching_revisions(branch, file_id): """Yield a description of revisions which affect the file_id. Each returned element is (revno, revision_id, description) This is the list of revisions where the file is either added, modified, renamed or deleted. TODO: Perhaps some way to limit this to only particular revisions, or to traverse a non-mainline set of revisions? """ last_ie = None last_path = None revno = 1 for revision_id in branch.revision_history(): this_inv = branch.get_revision_inventory(revision_id) if file_id in this_inv: this_ie = this_inv[file_id] this_path = this_inv.id2path(file_id) else: this_ie = this_path = None # now we know how it was last time, and how it is in this revision. # are those two states effectively the same or not? if not this_ie and not last_ie: # not present in either pass elif this_ie and not last_ie: yield revno, revision_id, "added " + this_path elif not this_ie and last_ie: # deleted here yield revno, revision_id, "deleted " + last_path elif this_path != last_path: yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path)) elif (this_ie.text_size != last_ie.text_size or this_ie.text_sha1 != last_ie.text_sha1): yield revno, revision_id, "modified " + this_path last_ie = this_ie last_path = this_path revno += 1 def show_log(branch, specific_fileid=None, show_timezone='original', verbose=False, show_ids=False, to_file=None, direction='reverse'): """Write out human-readable log of commits to this branch. specific_fileid If true, list only the commits affecting the specified file, rather than all commits. show_timezone 'original' (committer's timezone), 'utc' (universal time), or 'local' (local user's timezone) verbose If true show added/changed/deleted/renamed files. show_ids If true, show revision and file ids. to_file File to send log to; by default stdout. direction 'reverse' (default) is latest to earliest; 'forward' is earliest to latest. """ from osutils import format_date from errors import BzrCheckError from textui import show_status if specific_fileid: mutter('get log for file_id %r' % specific_fileid) if to_file == None: import sys to_file = sys.stdout which_revs = branch.enum_history(direction) if not (verbose or specific_fileid): # no need to know what changed between revisions with_deltas = deltas_for_log_dummy(branch, which_revs) elif direction == 'reverse': with_deltas = deltas_for_log_reverse(branch, which_revs) else: raise NotImplementedError("sorry, verbose forward logs not done yet") for revno, rev, delta in with_deltas: if specific_fileid: if not delta.touches_file_id(specific_fileid): continue if not verbose: # although we calculated it, throw it away without display delta = None show_one_log(revno, rev, delta, show_ids, to_file, show_timezone) def deltas_for_log_dummy(branch, which_revs): for revno, revision_id in which_revs: yield revno, branch.get_revision(revision_id), None def deltas_for_log_reverse(branch, which_revs): """Compute deltas for display in reverse log. Given a sequence of (revno, revision_id) pairs, return (revno, rev, delta). The delta is from the given revision to the next one in the sequence, which makes sense if the log is being displayed from newest to oldest. """ from tree import EmptyTree from diff import compare_trees last_revno = last_revision_id = last_tree = None for revno, revision_id in which_revs: this_tree = branch.revision_tree(revision_id) this_revision = branch.get_revision(revision_id) if last_revno: yield last_revno, last_revision, compare_trees(this_tree, last_tree, False) last_revno = revno last_revision = this_revision last_tree = this_tree if last_revno: this_tree = EmptyTree() yield last_revno, last_revision, compare_trees(this_tree, last_tree, False) def junk(): precursor = None if verbose: from tree import EmptyTree prev_tree = EmptyTree() for revno, revision_id in which_revs: precursor = revision_id if revision_id != rev.revision_id: raise BzrCheckError("retrieved wrong revision: %r" % (revision_id, rev.revision_id)) if verbose: this_tree = branch.revision_tree(revision_id) delta = compare_trees(prev_tree, this_tree, want_unchanged=False) prev_tree = this_tree else: delta = None def show_one_log(revno, rev, delta, show_ids, to_file, show_timezone): from osutils import format_date print >>to_file, '-' * 60 print >>to_file, 'revno:', revno if show_ids: print >>to_file, 'revision-id:', rev.revision_id print >>to_file, 'committer:', rev.committer print >>to_file, 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) print >>to_file, 'message:' if not rev.message: print >>to_file, ' (no message)' else: for l in rev.message.split('\n'): print >>to_file, ' ' + l if delta != None: delta.show(to_file, show_ids) commit refs/heads/master mark :534 committer Martin Pool 1116318878 +1000 data 133 - new RemoteStore class - use this for inventory_store and text_store in RemoteBranch - log -v now works (slowly) on remote branches from :533 M 644 inline bzrlib/remotebranch.py data 6902 #! /usr/bin/env python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Proxy object for access to remote branches. At the moment remote branches are only for HTTP and only for read access. """ import gzip from sets import Set from cStringIO import StringIO import urllib2 from errors import BzrError, BzrCheckError from branch import Branch, BZR_BRANCH_FORMAT from trace import mutter # velocitynet.com.au transparently proxies connections and thereby # breaks keep-alive -- sucks! ENABLE_URLGRABBER = True if ENABLE_URLGRABBER: import urlgrabber import urlgrabber.keepalive urlgrabber.keepalive.DEBUG = 0 def get_url(path, compressed=False): try: url = path if compressed: url += '.gz' mutter("grab url %s" % url) url_f = urlgrabber.urlopen(url, keepalive=1, close_connection=0) if not compressed: return url_f else: return gzip.GzipFile(fileobj=StringIO(url_f.read())) except urllib2.URLError, e: raise BzrError("remote fetch failed: %r: %s" % (url, e)) else: def get_url(url, compressed=False): import urllib2 if compressed: url += '.gz' mutter("get_url %s" % url) url_f = urllib2.urlopen(url) if compressed: return gzip.GzipFile(fileobj=StringIO(url_f.read())) else: return url_f def _find_remote_root(url): """Return the prefix URL that corresponds to the branch root.""" orig_url = url while True: try: ff = get_url(url + '/.bzr/branch-format') fmt = ff.read() ff.close() fmt = fmt.rstrip('\r\n') if fmt != BZR_BRANCH_FORMAT.rstrip('\r\n'): raise BzrError("sorry, branch format %r not supported at url %s" % (fmt, url)) return url except urllib2.URLError: pass try: idx = url.rindex('/') except ValueError: raise BzrError('no branch root found for URL %s' % orig_url) url = url[:idx] class RemoteBranch(Branch): def __init__(self, baseurl, find_root=True, lock_mode='r'): """Create new proxy for a remote branch.""" if lock_mode not in ('', 'r'): raise BzrError('lock mode %r is not supported for remote branches' % lock_mode) if find_root: self.baseurl = _find_remote_root(baseurl) else: self.baseurl = baseurl self._check_format() self.inventory_store = RemoteStore(baseurl + '/.bzr/inventory-store/') self.text_store = RemoteStore(baseurl + '/.bzr/text-store/') def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.baseurl) __repr__ = __str__ def controlfile(self, filename, mode): if mode not in ('rb', 'rt', 'r'): raise BzrError("file mode %r not supported for remote branches" % mode) return get_url(self.baseurl + '/.bzr/' + filename, False) def _need_readlock(self): # remote branch always safe for read pass def _need_writelock(self): raise BzrError("cannot get write lock on HTTP remote branch") def relpath(self, path): if not path.startswith(self.baseurl): raise BzrError('path %r is not under base URL %r' % (path, self.baseurl)) pl = len(self.baseurl) return path[pl:].lstrip('/') def get_revision(self, revision_id): from revision import Revision revf = get_url(self.baseurl + '/.bzr/revision-store/' + revision_id, True) r = Revision.read_xml(revf) if r.revision_id != revision_id: raise BzrCheckError('revision stored as {%s} actually contains {%s}' % (revision_id, r.revision_id)) return r class RemoteStore: def __init__(self, baseurl): self._baseurl = baseurl def _path(self, name): if '/' in name: raise ValueError('invalid store id', name) return self._baseurl + '/' + name def __getitem__(self, fileid): p = self._path(fileid) return get_url(p, compressed=True) def simple_walk(): """For experimental purposes, traverse many parts of a remote branch""" from revision import Revision from branch import Branch from inventory import Inventory got_invs = Set() got_texts = Set() print 'read history' history = get_url('/.bzr/revision-history').readlines() num_revs = len(history) for i, rev_id in enumerate(history): rev_id = rev_id.rstrip() print 'read revision %d/%d' % (i, num_revs) # python gzip needs a seekable file (!!) but the HTTP response # isn't, so we need to buffer it rev_f = get_url('/.bzr/revision-store/%s' % rev_id, compressed=True) rev = Revision.read_xml(rev_f) print rev.message inv_id = rev.inventory_id if inv_id not in got_invs: print 'get inventory %s' % inv_id inv_f = get_url('/.bzr/inventory-store/%s' % inv_id, compressed=True) inv = Inventory.read_xml(inv_f) print '%4d inventory entries' % len(inv) for path, ie in inv.iter_entries(): text_id = ie.text_id if text_id == None: continue if text_id in got_texts: continue print ' fetch %s text {%s}' % (path, text_id) text_f = get_url('/.bzr/text-store/%s' % text_id, compressed=True) got_texts.add(text_id) got_invs.add(inv_id) print '----' def try_me(): BASE_URL = 'http://bazaar-ng.org/bzr/bzr.dev/' b = RemoteBranch(BASE_URL) ## print '\n'.join(b.revision_history()) from log import show_log show_log(b) if __name__ == '__main__': try_me() commit refs/heads/master mark :535 committer Martin Pool 1116319175 +1000 data 79 - try to eliminate wierd characters from file names when they're first added from :534 M 644 inline bzrlib/branch.py data 26895 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from sets import Set import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. base Base directory of the branch. """ _lockmode = None def __init__(self, base, init=False, find_root=True, lock_mode='w'): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.lock(lock_mode) self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def lock(self, mode='w'): """Lock the on-disk branch, excluding other processes.""" try: import fcntl, errno if mode == 'w': lm = fcntl.LOCK_EX om = os.O_WRONLY | os.O_CREAT elif mode == 'r': lm = fcntl.LOCK_SH om = os.O_RDONLY else: raise BzrError("invalid locking mode %r" % mode) try: lockfile = os.open(self.controlfilename('branch-lock'), om) except OSError, e: if e.errno == errno.ENOENT: # might not exist on branches from <0.0.4 self.controlfile('branch-lock', 'w').close() lockfile = os.open(self.controlfilename('branch-lock'), om) else: raise e fcntl.lockf(lockfile, lm) def unlock(): fcntl.lockf(lockfile, fcntl.LOCK_UN) os.close(lockfile) self._lockmode = None self.unlock = unlock self._lockmode = mode except ImportError: warning("please write a locking method for platform %r" % sys.platform) def unlock(): self._lockmode = None self.unlock = unlock self._lockmode = mode def _need_readlock(self): if self._lockmode not in ['r', 'w']: raise BzrError('need read lock on branch, only have %r' % self._lockmode) def _need_writelock(self): if self._lockmode not in ['w']: raise BzrError('need write lock on branch, only have %r' % self._lockmode) def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" self._need_readlock() before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self._need_writelock() ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ self._need_writelock() # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" self._need_readlock() tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability self._need_writelock() if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" self._need_readlock() r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" self._need_readlock() i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" self._need_readlock() if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self._need_readlock() return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def commit(self, *args, **kw): """Deprecated""" from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. self._need_readlock() if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self._need_writelock() tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self._need_writelock() ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = Set(inv.get_idpath(to_dir_id)) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :536 committer Martin Pool 1116319222 +1000 data 3 doc from :535 M 644 inline NEWS data 7601 bzr-0.0.5 NOT RELEASED YET CHANGES: * ``bzr`` with no command now shows help rather than giving an error. Suggested by Michael Ellerman. * ``bzr status`` output format changed, because svn-style output doesn't really match the model of bzr. Now files are grouped by status and can be shown with their IDs. ``bzr status --all`` shows all versioned files and unknown files but not ignored files. * ``bzr log`` runs from most-recent to least-recent, the reverse of the previous order. ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New ``bzr ignore PATTERN`` command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. File may be in a branch other than the working directory. * ``bzr log`` and ``bzr root`` can be given an http URL instead of a filename. * Commands can now be defined by external programs or scripts in a directory on $BZRPATH. * New "stat cache" avoids reading the contents of files if they haven't changed since the previous time. * If the Python interpreter is too old, try to find a better one or give an error. Based on a patch from Fredrik Lundh. * New optional parameter ``bzr info [BRANCH]``. * New form ``bzr commit SELECTED`` to commit only selected files. BUG FIXES: * Fixed diff format so that added and removed files will be handled properly by patch. Fix from Lalo Martins. * Various fixes for files whose names contain spaces or other metacharacters. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. * testbzr requires python2.4, but can be used to test bzr running under a different version. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. * New developer commands ``added``, ``modified``. PORTABILITY: * Cope on Windows on python2.3 by using the weaker random seed. 2.4 is now only recommended. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. commit refs/heads/master mark :537 committer Martin Pool 1116320711 +1000 data 176 - file-ids are stored as quoted-printable in the stat cache, so as to better handle any wierd values that may be present. - more sanity checks on records read from stat cache from :536 M 644 inline bzrlib/statcache.py data 8378 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import stat, os, sha, time from binascii import b2a_qp, a2b_qp from trace import mutter from errors import BzrError, BzrCheckError """File stat cache to speed up tree comparisons. This module basically gives a quick way to find the SHA-1 and related information of a file in the working directory, without actually reading and hashing the whole file. Implementation ============== Users of this module should not need to know about how this is implemented, and in particular should not depend on the particular data which is stored or its format. This is done by maintaining a cache indexed by a file fingerprint of (path, size, mtime, ctime, ino, dev) pointing to the SHA-1. If the fingerprint has changed, we assume the file content has not changed either and the SHA-1 is therefore the same. If any of the fingerprint fields have changed then the file content *may* have changed, or it may not have. We need to reread the file contents to make sure, but this is not visible to the user or higher-level code (except as a delay of course). The mtime and ctime are stored with nanosecond fields, but not all filesystems give this level of precision. There is therefore a possible race: the file might be modified twice within a second without changing the size or mtime, and a SHA-1 cached from the first version would be wrong. We handle this by not recording a cached hash for any files which were modified in the current second and that therefore have the chance to change again before the second is up. The only known hole in this design is if the system clock jumps backwards crossing invocations of bzr. Please don't do that; use ntp to gradually adjust your clock or don't use bzr over the step. At the moment this is stored in a simple textfile; it might be nice to use a tdb instead. The cache is represented as a map from file_id to a tuple of (file_id, sha1, path, size, mtime, ctime, ino, dev). The SHA-1 is stored in memory as a hexdigest. File names and file-ids are written out as the quoted-printable encoding of their UTF-8 representation. (file-ids shouldn't contain wierd characters, but it might happen.) """ # order of fields returned by fingerprint() FP_SIZE = 0 FP_MTIME = 1 FP_CTIME = 2 FP_INO = 3 FP_DEV = 4 # order of fields in the statcache file and in the in-memory map SC_FILE_ID = 0 SC_SHA1 = 1 SC_PATH = 2 SC_SIZE = 3 SC_MTIME = 4 SC_CTIME = 5 SC_INO = 6 SC_DEV = 7 CACHE_HEADER = "### bzr statcache v2" def fingerprint(abspath): try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) def _write_cache(basedir, entry_iter, dangerfiles): from atomicfile import AtomicFile cachefn = os.path.join(basedir, '.bzr', 'stat-cache') outf = AtomicFile(cachefn, 'wb') outf.write(CACHE_HEADER + '\n') try: for entry in entry_iter: if len(entry) != 8: raise ValueError("invalid statcache entry tuple %r" % entry) if entry[SC_FILE_ID] in dangerfiles: continue # changed too recently outf.write(b2a_qp(entry[0].encode('utf-8'))) # file id outf.write(' ') outf.write(entry[1]) # hex sha1 outf.write(' ') outf.write(b2a_qp(entry[2].encode('utf-8'), True)) # name for nf in entry[3:]: outf.write(' %d' % nf) outf.write('\n') outf.commit() finally: if not outf.closed: outf.abort() def load_cache(basedir): from sets import Set import re cache = {} seen_paths = Set() sha_re = re.compile(r'[a-f0-9]{40}') try: cachefn = os.path.join(basedir, '.bzr', 'stat-cache') cachefile = open(cachefn, 'rb') except IOError: return cache line1 = cachefile.readline().rstrip('\r\n') if line1 != CACHE_HEADER: mutter('cache header marker not found at top of %s' % cachefn) return cache for l in cachefile: f = l.split(' ') file_id = a2b_qp(f[0]).decode('utf-8') if file_id in cache: raise BzrCheckError("duplicated file_id in cache: {%s}" % file_id) text_sha = f[1] if len(text_sha) != 40 or not sha_re.match(text_sha): raise BzrCheckError("invalid file SHA-1 in cache: %r" % text_sha) path = a2b_qp(f[2]).decode('utf-8') if path in seen_paths: raise BzrCheckError("duplicated path in cache: %r" % path) seen_paths.add(path) entry = (file_id, text_sha, path) + tuple([long(x) for x in f[3:]]) if len(entry) != 8: raise ValueError("invalid statcache entry tuple %r" % entry) cache[file_id] = entry return cache def _files_from_inventory(inv): for path, ie in inv.iter_entries(): if ie.kind != 'file': continue yield ie.file_id, path def update_cache(basedir, inv, flush=False): """Update and return the cache for the branch. The returned cache may contain entries that have not been written to disk for files recently touched. flush -- discard any previous cache and recalculate from scratch. """ # TODO: It's supposed to be faster to stat the files in order by inum. # We don't directly know the inum of the files of course but we do # know where they were last sighted, so we can sort by that. assert isinstance(flush, bool) if flush: cache = {} else: cache = load_cache(basedir) return _update_cache_from_list(basedir, cache, _files_from_inventory(inv)) def _update_cache_from_list(basedir, cache, to_update): """Update and return the cache for given files. cache -- Previously cached values to be validated. to_update -- Sequence of (file_id, path) pairs to check. """ from sets import Set stat_cnt = missing_cnt = hardcheck = change_cnt = 0 # files that have been recently touched and can't be # committed to a persistent cache yet. dangerfiles = Set() now = int(time.time()) ## mutter('update statcache under %r' % basedir) for file_id, path in to_update: abspath = os.path.join(basedir, path) fp = fingerprint(abspath) stat_cnt += 1 cacheentry = cache.get(file_id) if fp == None: # not here if cacheentry: del cache[file_id] change_cnt += 1 missing_cnt += 1 continue if (fp[FP_MTIME] >= now) or (fp[FP_CTIME] >= now): dangerfiles.add(file_id) if cacheentry and (cacheentry[3:] == fp): continue # all stat fields unchanged hardcheck += 1 dig = sha.new(file(abspath, 'rb').read()).hexdigest() # We update the cache even if the digest has not changed from # last time we looked, so that the fingerprint fields will # match in future. cacheentry = (file_id, dig, path) + fp cache[file_id] = cacheentry change_cnt += 1 mutter('statcache: statted %d files, read %d files, %d changed, %d dangerous, ' '%d in cache' % (stat_cnt, hardcheck, change_cnt, len(dangerfiles), len(cache))) if change_cnt: mutter('updating on-disk statcache') _write_cache(basedir, cache.itervalues(), dangerfiles) return cache commit refs/heads/master mark :538 committer Martin Pool 1116418102 +1000 data 12 - fix import from :537 M 644 inline bzrlib/log.py data 7109 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Code to show logs of changes. Various flavors of log can be produced: * for one file, or the whole tree, and (not done yet) for files in a given directory * in "verbose" mode with a description of what changed from one version to the next * with file-ids and revision-ids shown * from last to first or (not anymore) from first to last; the default is "reversed" because it shows the likely most relevant and interesting information first * (not yet) in XML format """ from trace import mutter def find_touching_revisions(branch, file_id): """Yield a description of revisions which affect the file_id. Each returned element is (revno, revision_id, description) This is the list of revisions where the file is either added, modified, renamed or deleted. TODO: Perhaps some way to limit this to only particular revisions, or to traverse a non-mainline set of revisions? """ last_ie = None last_path = None revno = 1 for revision_id in branch.revision_history(): this_inv = branch.get_revision_inventory(revision_id) if file_id in this_inv: this_ie = this_inv[file_id] this_path = this_inv.id2path(file_id) else: this_ie = this_path = None # now we know how it was last time, and how it is in this revision. # are those two states effectively the same or not? if not this_ie and not last_ie: # not present in either pass elif this_ie and not last_ie: yield revno, revision_id, "added " + this_path elif not this_ie and last_ie: # deleted here yield revno, revision_id, "deleted " + last_path elif this_path != last_path: yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path)) elif (this_ie.text_size != last_ie.text_size or this_ie.text_sha1 != last_ie.text_sha1): yield revno, revision_id, "modified " + this_path last_ie = this_ie last_path = this_path revno += 1 def show_log(branch, specific_fileid=None, show_timezone='original', verbose=False, show_ids=False, to_file=None, direction='reverse'): """Write out human-readable log of commits to this branch. specific_fileid If true, list only the commits affecting the specified file, rather than all commits. show_timezone 'original' (committer's timezone), 'utc' (universal time), or 'local' (local user's timezone) verbose If true show added/changed/deleted/renamed files. show_ids If true, show revision and file ids. to_file File to send log to; by default stdout. direction 'reverse' (default) is latest to earliest; 'forward' is earliest to latest. """ from osutils import format_date from errors import BzrCheckError from textui import show_status if specific_fileid: mutter('get log for file_id %r' % specific_fileid) if to_file == None: import sys to_file = sys.stdout which_revs = branch.enum_history(direction) if not (verbose or specific_fileid): # no need to know what changed between revisions with_deltas = deltas_for_log_dummy(branch, which_revs) elif direction == 'reverse': with_deltas = deltas_for_log_reverse(branch, which_revs) else: raise NotImplementedError("sorry, verbose forward logs not done yet") for revno, rev, delta in with_deltas: if specific_fileid: if not delta.touches_file_id(specific_fileid): continue if not verbose: # although we calculated it, throw it away without display delta = None show_one_log(revno, rev, delta, show_ids, to_file, show_timezone) def deltas_for_log_dummy(branch, which_revs): for revno, revision_id in which_revs: yield revno, branch.get_revision(revision_id), None def deltas_for_log_reverse(branch, which_revs): """Compute deltas for display in reverse log. Given a sequence of (revno, revision_id) pairs, return (revno, rev, delta). The delta is from the given revision to the next one in the sequence, which makes sense if the log is being displayed from newest to oldest. """ from tree import EmptyTree from diff import compare_trees last_revno = last_revision_id = last_tree = None for revno, revision_id in which_revs: this_tree = branch.revision_tree(revision_id) this_revision = branch.get_revision(revision_id) if last_revno: yield last_revno, last_revision, compare_trees(this_tree, last_tree, False) last_revno = revno last_revision = this_revision last_tree = this_tree if last_revno: this_tree = EmptyTree() yield last_revno, last_revision, compare_trees(this_tree, last_tree, False) def junk(): precursor = None if verbose: from tree import EmptyTree prev_tree = EmptyTree() for revno, revision_id in which_revs: precursor = revision_id if revision_id != rev.revision_id: raise BzrCheckError("retrieved wrong revision: %r" % (revision_id, rev.revision_id)) if verbose: this_tree = branch.revision_tree(revision_id) delta = compare_trees(prev_tree, this_tree, want_unchanged=False) prev_tree = this_tree else: delta = None def show_one_log(revno, rev, delta, show_ids, to_file, show_timezone): from osutils import format_date print >>to_file, '-' * 60 print >>to_file, 'revno:', revno if show_ids: print >>to_file, 'revision-id:', rev.revision_id print >>to_file, 'committer:', rev.committer print >>to_file, 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) print >>to_file, 'message:' if not rev.message: print >>to_file, ' (no message)' else: for l in rev.message.split('\n'): print >>to_file, ' ' + l if delta != None: delta.show(to_file, show_ids) commit refs/heads/master mark :539 committer Martin Pool 1116464293 +1000 data 64 - urlgrabber fix for parsing python version strings from aaron from :538 M 644 inline urlgrabber/keepalive.py data 20326 # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the # Free Software Foundation, Inc., # 59 Temple Place, Suite 330, # Boston, MA 02111-1307 USA # This file is part of urlgrabber, a high-level cross-protocol url-grabber # Copyright 2002-2004 Michael D. Stenner, Ryan Tomayko """An HTTP handler for urllib2 that supports HTTP 1.1 and keepalive. >>> import urllib2 >>> from keepalive import HTTPHandler >>> keepalive_handler = HTTPHandler() >>> opener = urllib2.build_opener(keepalive_handler) >>> urllib2.install_opener(opener) >>> >>> fo = urllib2.urlopen('http://www.python.org') If a connection to a given host is requested, and all of the existing connections are still in use, another connection will be opened. If the handler tries to use an existing connection but it fails in some way, it will be closed and removed from the pool. To remove the handler, simply re-run build_opener with no arguments, and install that opener. You can explicitly close connections by using the close_connection() method of the returned file-like object (described below) or you can use the handler methods: close_connection(host) close_all() open_connections() NOTE: using the close_connection and close_all methods of the handler should be done with care when using multiple threads. * there is nothing that prevents another thread from creating new connections immediately after connections are closed * no checks are done to prevent in-use connections from being closed >>> keepalive_handler.close_all() EXTRA ATTRIBUTES AND METHODS Upon a status of 200, the object returned has a few additional attributes and methods, which should not be used if you want to remain consistent with the normal urllib2-returned objects: close_connection() - close the connection to the host readlines() - you know, readlines() status - the return status (ie 404) reason - english translation of status (ie 'File not found') If you want the best of both worlds, use this inside an AttributeError-catching try: >>> try: status = fo.status >>> except AttributeError: status = None Unfortunately, these are ONLY there if status == 200, so it's not easy to distinguish between non-200 responses. The reason is that urllib2 tries to do clever things with error codes 301, 302, 401, and 407, and it wraps the object upon return. For python versions earlier than 2.4, you can avoid this fancy error handling by setting the module-level global HANDLE_ERRORS to zero. You see, prior to 2.4, it's the HTTP Handler's job to determine what to handle specially, and what to just pass up. HANDLE_ERRORS == 0 means "pass everything up". In python 2.4, however, this job no longer belongs to the HTTP Handler and is now done by a NEW handler, HTTPErrorProcessor. Here's the bottom line: python version < 2.4 HANDLE_ERRORS == 1 (default) pass up 200, treat the rest as errors HANDLE_ERRORS == 0 pass everything up, error processing is left to the calling code python version >= 2.4 HANDLE_ERRORS == 1 pass up 200, treat the rest as errors HANDLE_ERRORS == 0 (default) pass everything up, let the other handlers (specifically, HTTPErrorProcessor) decide what to do In practice, setting the variable either way makes little difference in python 2.4, so for the most consistent behavior across versions, you probably just want to use the defaults, which will give you exceptions on errors. """ # $Id: keepalive.py,v 1.9 2005/02/14 21:55:07 mstenner Exp $ import urllib2 import httplib import socket import thread DEBUG = 0 def DBPRINT(*args): print ' '.join(args) import sys if hasattr(sys, 'version_info'): _python_version = sys.version_info else: _python_version = map(int, sys.version.split()[0].split('.')) if _python_version < [2, 4]: HANDLE_ERRORS = 1 else: HANDLE_ERRORS = 0 class ConnectionManager: """ The connection manager must be able to: * keep track of all existing """ def __init__(self): self._lock = thread.allocate_lock() self._hostmap = {} # map hosts to a list of connections self._connmap = {} # map connections to host self._readymap = {} # map connection to ready state def add(self, host, connection, ready): self._lock.acquire() try: if not self._hostmap.has_key(host): self._hostmap[host] = [] self._hostmap[host].append(connection) self._connmap[connection] = host self._readymap[connection] = ready finally: self._lock.release() def remove(self, connection): self._lock.acquire() try: try: host = self._connmap[connection] except KeyError: pass else: del self._connmap[connection] del self._readymap[connection] self._hostmap[host].remove(connection) if not self._hostmap[host]: del self._hostmap[host] finally: self._lock.release() def set_ready(self, connection, ready): try: self._readymap[connection] = ready except KeyError: pass def get_ready_conn(self, host): conn = None self._lock.acquire() try: if self._hostmap.has_key(host): for c in self._hostmap[host]: if self._readymap[c]: self._readymap[c] = 0 conn = c break finally: self._lock.release() return conn def get_all(self, host=None): if host: return list(self._hostmap.get(host, [])) else: return dict(self._hostmap) class HTTPHandler(urllib2.HTTPHandler): def __init__(self): self._cm = ConnectionManager() #### Connection Management def open_connections(self): """return a list of connected hosts and the number of connections to each. [('foo.com:80', 2), ('bar.org', 1)]""" return [(host, len(li)) for (host, li) in self._cm.get_all().items()] def close_connection(self, host): """close connection(s) to host is the host:port spec, as in 'www.cnn.com:8080' as passed in. no error occurs if there is no connection to that host.""" for h in self._cm.get_all(host): self._cm.remove(h) h.close() def close_all(self): """close all open connections""" for host, conns in self._cm.get_all().items(): for h in conns: self._cm.remove(h) h.close() def _request_closed(self, request, host, connection): """tells us that this request is now closed and the the connection is ready for another request""" self._cm.set_ready(connection, 1) def _remove_connection(self, host, connection, close=0): if close: connection.close() self._cm.remove(connection) #### Transaction Execution def http_open(self, req): return self.do_open(HTTPConnection, req) def do_open(self, http_class, req): host = req.get_host() if not host: raise urllib2.URLError('no host given') try: h = self._cm.get_ready_conn(host) while h: r = self._reuse_connection(h, req, host) # if this response is non-None, then it worked and we're # done. Break out, skipping the else block. if r: break # connection is bad - possibly closed by server # discard it and ask for the next free connection h.close() self._cm.remove(h) h = self._cm.get_ready_conn(host) else: # no (working) free connections were found. Create a new one. h = http_class(host) if DEBUG: DBPRINT("creating new connection to %s (%d)" % \ (host, id(h))) self._cm.add(host, h, 0) self._start_transaction(h, req) r = h.getresponse() except (socket.error, httplib.HTTPException), err: raise urllib2.URLError(err) # if not a persistent connection, don't try to reuse it if r.will_close: self._cm.remove(h) if DEBUG: DBPRINT("STATUS: %s, %s" % (r.status, r.reason)) r._handler = self r._host = host r._url = req.get_full_url() r._connection = h r.code = r.status if r.status == 200 or not HANDLE_ERRORS: return r else: return self.parent.error('http', req, r, r.status, r.reason, r.msg) def _reuse_connection(self, h, req, host): """start the transaction with a re-used connection return a response object (r) upon success or None on failure. This DOES not close or remove bad connections in cases where it returns. However, if an unexpected exception occurs, it will close and remove the connection before re-raising. """ try: self._start_transaction(h, req) r = h.getresponse() # note: just because we got something back doesn't mean it # worked. We'll check the version below, too. except (socket.error, httplib.HTTPException): r = None except: # adding this block just in case we've missed # something we will still raise the exception, but # lets try and close the connection and remove it # first. We previously got into a nasty loop # where an exception was uncaught, and so the # connection stayed open. On the next try, the # same exception was raised, etc. The tradeoff is # that it's now possible this call will raise # a DIFFERENT exception if DEBUG: DBPRINT("unexpected exception - " \ "closing connection to %s (%d)" % (host, id(h))) self._cm.remove(h) h.close() raise if r is None or r.version == 9: # httplib falls back to assuming HTTP 0.9 if it gets a # bad header back. This is most likely to happen if # the socket has been closed by the server since we # last used the connection. if DEBUG: DBPRINT("failed to re-use connection to %s (%d)" \ % (host, id(h))) r = None else: if DEBUG: DBPRINT("re-using connection to %s (%d)" % (host, id(h))) return r def _start_transaction(self, h, req): try: if req.has_data(): data = req.get_data() h.putrequest('POST', req.get_selector()) if not req.headers.has_key('Content-type'): h.putheader('Content-type', 'application/x-www-form-urlencoded') if not req.headers.has_key('Content-length'): h.putheader('Content-length', '%d' % len(data)) else: h.putrequest('GET', req.get_selector()) except (socket.error, httplib.HTTPException), err: raise urllib2.URLError(err) for args in self.parent.addheaders: h.putheader(*args) for k, v in req.headers.items(): h.putheader(k, v) h.endheaders() if req.has_data(): h.send(data) class HTTPResponse(httplib.HTTPResponse): # we need to subclass HTTPResponse in order to # 1) add readline() and readlines() methods # 2) add close_connection() methods # 3) add info() and geturl() methods # in order to add readline(), read must be modified to deal with a # buffer. example: readline must read a buffer and then spit back # one line at a time. The only real alternative is to read one # BYTE at a time (ick). Once something has been read, it can't be # put back (ok, maybe it can, but that's even uglier than this), # so if you THEN do a normal read, you must first take stuff from # the buffer. # the read method wraps the original to accomodate buffering, # although read() never adds to the buffer. # Both readline and readlines have been stolen with almost no # modification from socket.py def __init__(self, sock, debuglevel=0, strict=0, method=None): if method: # the httplib in python 2.3 uses the method arg httplib.HTTPResponse.__init__(self, sock, debuglevel, method) else: # 2.2 doesn't httplib.HTTPResponse.__init__(self, sock, debuglevel) self.fileno = sock.fileno self.code = None self._rbuf = '' self._rbufsize = 8096 self._handler = None # inserted by the handler later self._host = None # (same) self._url = None # (same) self._connection = None # (same) _raw_read = httplib.HTTPResponse.read def close(self): if self.fp: self.fp.close() self.fp = None if self._handler: self._handler._request_closed(self, self._host, self._connection) def close_connection(self): self._handler._remove_connection(self._host, self._connection, close=1) self.close() def info(self): return self.msg def geturl(self): return self._url def read(self, amt=None): # the _rbuf test is only in this first if for speed. It's not # logically necessary if self._rbuf and not amt is None: L = len(self._rbuf) if amt > L: amt -= L else: s = self._rbuf[:amt] self._rbuf = self._rbuf[amt:] return s s = self._rbuf + self._raw_read(amt) self._rbuf = '' return s def readline(self, limit=-1): data = "" i = self._rbuf.find('\n') while i < 0 and not (0 < limit <= len(self._rbuf)): new = self._raw_read(self._rbufsize) if not new: break i = new.find('\n') if i >= 0: i = i + len(self._rbuf) self._rbuf = self._rbuf + new if i < 0: i = len(self._rbuf) else: i = i+1 if 0 <= limit < len(self._rbuf): i = limit data, self._rbuf = self._rbuf[:i], self._rbuf[i:] return data def readlines(self, sizehint = 0): total = 0 list = [] while 1: line = self.readline() if not line: break list.append(line) total += len(line) if sizehint and total >= sizehint: break return list class HTTPConnection(httplib.HTTPConnection): # use the modified response class response_class = HTTPResponse ######################################################################### ##### TEST FUNCTIONS ######################################################################### def error_handler(url): global HANDLE_ERRORS orig = HANDLE_ERRORS keepalive_handler = HTTPHandler() opener = urllib2.build_opener(keepalive_handler) urllib2.install_opener(opener) pos = {0: 'off', 1: 'on'} for i in (0, 1): print " fancy error handling %s (HANDLE_ERRORS = %i)" % (pos[i], i) HANDLE_ERRORS = i try: fo = urllib2.urlopen(url) foo = fo.read() fo.close() try: status, reason = fo.status, fo.reason except AttributeError: status, reason = None, None except IOError, e: print " EXCEPTION: %s" % e raise else: print " status = %s, reason = %s" % (status, reason) HANDLE_ERRORS = orig hosts = keepalive_handler.open_connections() print "open connections:", hosts keepalive_handler.close_all() def continuity(url): import md5 format = '%25s: %s' # first fetch the file with the normal http handler opener = urllib2.build_opener() urllib2.install_opener(opener) fo = urllib2.urlopen(url) foo = fo.read() fo.close() m = md5.new(foo) print format % ('normal urllib', m.hexdigest()) # now install the keepalive handler and try again opener = urllib2.build_opener(HTTPHandler()) urllib2.install_opener(opener) fo = urllib2.urlopen(url) foo = fo.read() fo.close() m = md5.new(foo) print format % ('keepalive read', m.hexdigest()) fo = urllib2.urlopen(url) foo = '' while 1: f = fo.readline() if f: foo = foo + f else: break fo.close() m = md5.new(foo) print format % ('keepalive readline', m.hexdigest()) def comp(N, url): print ' making %i connections to:\n %s' % (N, url) sys.stdout.write(' first using the normal urllib handlers') # first use normal opener opener = urllib2.build_opener() urllib2.install_opener(opener) t1 = fetch(N, url) print ' TIME: %.3f s' % t1 sys.stdout.write(' now using the keepalive handler ') # now install the keepalive handler and try again opener = urllib2.build_opener(HTTPHandler()) urllib2.install_opener(opener) t2 = fetch(N, url) print ' TIME: %.3f s' % t2 print ' improvement factor: %.2f' % (t1/t2, ) def fetch(N, url, delay=0): lens = [] starttime = time.time() for i in range(N): if delay and i > 0: time.sleep(delay) fo = urllib2.urlopen(url) foo = fo.read() fo.close() lens.append(len(foo)) diff = time.time() - starttime j = 0 for i in lens[1:]: j = j + 1 if not i == lens[0]: print "WARNING: inconsistent length on read %i: %i" % (j, i) return diff def test_timeout(url): global DEBUG, DBPRINT dbp = DBPRINT def DBPRINT(*args): print ' ' + ' '.join(args) DEBUG=1 print " fetching the file to establish a connection" fo = urllib2.urlopen(url) data1 = fo.read() fo.close() i = 20 print " waiting %i seconds for the server to close the connection" % i while i > 0: sys.stdout.write('\r %2i' % i) sys.stdout.flush() time.sleep(1) i -= 1 sys.stderr.write('\r') print " fetching the file a second time" fo = urllib2.urlopen(url) data2 = fo.read() fo.close() if data1 == data2: print ' data are identical' else: print ' ERROR: DATA DIFFER' DEBUG=0 DBPRINT = dbp def test(url, N=10): print "checking error hander (do this on a non-200)" try: error_handler(url) except IOError, e: print "exiting - exception will prevent further tests" sys.exit() print print "performing continuity test (making sure stuff isn't corrupted)" continuity(url) print print "performing speed comparison" comp(N, url) print print "performing dropped-connection check" test_timeout(url) if __name__ == '__main__': import time import sys try: N = int(sys.argv[1]) url = sys.argv[2] except: print "%s " % sys.argv[0] else: test(url, N) commit refs/heads/master mark :540 committer Martin Pool 1116484641 +1000 data 183 - use builtin set object in python2.4 (suggested by Mario Pernici) - clean up some uses of set() that can simply be lists or dicts - remove some imports from the bzrlib namespace from :539 M 644 inline bzrlib/__init__.py data 2108 # (C) 2005 Canonical Development Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr library""" from inventory import Inventory, InventoryEntry from branch import Branch, ScratchBranch, find_branch from osutils import format_date from tree import Tree from diff import compare_trees from trace import mutter, warning, open_tracefile from log import show_log import add BZRDIR = ".bzr" DEFAULT_IGNORE = ['.bzr.log', '*~', '#*#', '*$', '.#*', '.*.swp', '.*.tmp', '*.tmp', '*.bak', '*.BAK', '*.orig', '*.o', '*.obj', '*.a', '*.py[oc]', '*.so', '*.exe', '*.elc', '{arch}', 'CVS', 'CVS.adm', '.svn', '_darcs', 'SCCS', 'RCS', '*,v', 'BitKeeper', 'TAGS', '.make.state', '.sconsign', '.tmp*', '.del-*'] IGNORE_FILENAME = ".bzrignore" import locale user_encoding = locale.getpreferredencoding() del locale __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __version__ = '0.0.5pre' # in python2.4 there is a 'set' builtin type; in 2.3 we need to use # the slower pure-python 'sets' module. import sys if sys.version_info < (2, 4): import sets set = sets.Set frozenset = sets.ImmutableSet del sets else: import __builtin__ set = __builtin__.set frozenset = __builtin__.frozenset del __builtin__ M 644 inline bzrlib/branch.py data 26868 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch: """Branch holding a history of revisions. base Base directory of the branch. """ _lockmode = None def __init__(self, base, init=False, find_root=True, lock_mode='w'): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.lock(lock_mode) self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def lock(self, mode='w'): """Lock the on-disk branch, excluding other processes.""" try: import fcntl, errno if mode == 'w': lm = fcntl.LOCK_EX om = os.O_WRONLY | os.O_CREAT elif mode == 'r': lm = fcntl.LOCK_SH om = os.O_RDONLY else: raise BzrError("invalid locking mode %r" % mode) try: lockfile = os.open(self.controlfilename('branch-lock'), om) except OSError, e: if e.errno == errno.ENOENT: # might not exist on branches from <0.0.4 self.controlfile('branch-lock', 'w').close() lockfile = os.open(self.controlfilename('branch-lock'), om) else: raise e fcntl.lockf(lockfile, lm) def unlock(): fcntl.lockf(lockfile, fcntl.LOCK_UN) os.close(lockfile) self._lockmode = None self.unlock = unlock self._lockmode = mode except ImportError: warning("please write a locking method for platform %r" % sys.platform) def unlock(): self._lockmode = None self.unlock = unlock self._lockmode = mode def _need_readlock(self): if self._lockmode not in ['r', 'w']: raise BzrError('need read lock on branch, only have %r' % self._lockmode) def _need_writelock(self): if self._lockmode not in ['w']: raise BzrError('need write lock on branch, only have %r' % self._lockmode) def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" self._need_readlock() before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self._need_writelock() ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ self._need_writelock() # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" self._need_readlock() tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability self._need_writelock() if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" self._need_readlock() r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" self._need_readlock() i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" self._need_readlock() if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self._need_readlock() return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def commit(self, *args, **kw): """Deprecated""" from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. self._need_readlock() if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self._need_writelock() tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self._need_writelock() ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/check.py data 4304 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ###################################################################### # consistency checks import sys from trace import mutter from errors import bailout import osutils def check(branch, progress=True): from bzrlib import set out = sys.stdout # TODO: factor out if not (hasattr(out, 'isatty') and out.isatty()): progress=False if progress: def p(m): mutter('checking ' + m) out.write('\rchecking: %-50.50s' % m) out.flush() else: def p(m): mutter('checking ' + m) p('history of %r' % branch.base) last_ptr = None checked_revs = set() history = branch.revision_history() revno = 0 revcount = len(history) checked_texts = {} for rid in history: revno += 1 p('revision %d/%d' % (revno, revcount)) mutter(' revision {%s}' % rid) rev = branch.get_revision(rid) if rev.revision_id != rid: bailout('wrong internal revision id in revision {%s}' % rid) if rev.precursor != last_ptr: bailout('mismatched precursor in revision {%s}' % rid) last_ptr = rid if rid in checked_revs: bailout('repeated revision {%s}' % rid) checked_revs.add(rid) ## TODO: Check all the required fields are present on the revision. inv = branch.get_inventory(rev.inventory_id) seen_ids = set() seen_names = set() p('revision %d/%d file ids' % (revno, revcount)) for file_id in inv: if file_id in seen_ids: bailout('duplicated file_id {%s} in inventory for revision {%s}' % (file_id, rid)) seen_ids.add(file_id) i = 0 len_inv = len(inv) for file_id in inv: i += 1 if (i % 100) == 0: p('revision %d/%d file text %d/%d' % (revno, revcount, i, len_inv)) ie = inv[file_id] if ie.parent_id != None: if ie.parent_id not in seen_ids: bailout('missing parent {%s} in inventory for revision {%s}' % (ie.parent_id, rid)) if ie.kind == 'file': if ie.text_id in checked_texts: fp = checked_texts[ie.text_id] else: if not ie.text_id in branch.text_store: bailout('text {%s} not in text_store' % ie.text_id) tf = branch.text_store[ie.text_id] fp = osutils.fingerprint_file(tf) checked_texts[ie.text_id] = fp if ie.text_size != fp['size']: bailout('text {%s} wrong size' % ie.text_id) if ie.text_sha1 != fp['sha1']: bailout('text {%s} wrong sha1' % ie.text_id) elif ie.kind == 'directory': if ie.text_sha1 != None or ie.text_size != None or ie.text_id != None: bailout('directory {%s} has text in revision {%s}' % (file_id, rid)) p('revision %d/%d file paths' % (revno, revcount)) for path, ie in inv.iter_entries(): if path in seen_names: bailout('duplicated path %r in inventory for revision {%s}' % (path, revid)) seen_names.add(path) p('done') if progress: print print 'checked %d revisions, %d file texts' % (revcount, len(checked_texts)) M 644 inline bzrlib/commands.py data 35937 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date from bzrlib import merge def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0], lock_mode='r') file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.', lock_mode='r') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, specific_files=file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. TODO: Option to show in forward order. """ takes_args = ['filename?'] takes_options = ['timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False): from bzrlib import show_log, find_branch if filename: b = find_branch(filename, lock_mode='r') fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.', lock_mode='r') file_id = None show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=sys.stdout) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures return 1 else: print return 0 class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): merge.merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/diff.py data 9975 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from trace import mutter from errors import BzrError def _diff_one(oldlines, newlines, to_file, **kw): import difflib # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') to_file.writelines(ud) if nonl: print >>to_file, "\\ No newline at end of file" print >>to_file def show_diff(b, revision, specific_files): import sys if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. delta = compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=specific_files) for path, file_id, kind in delta.removed: print '*** removed %s %r' % (kind, path) if kind == 'file': _diff_one(old_tree.get_file(file_id).readlines(), [], sys.stdout, fromfile=old_label + path, tofile=DEVNULL) for path, file_id, kind in delta.added: print '*** added %s %r' % (kind, path) if kind == 'file': _diff_one([], new_tree.get_file(file_id).readlines(), sys.stdout, fromfile=DEVNULL, tofile=new_label + path) for old_path, new_path, file_id, kind, text_modified in delta.renamed: print '*** renamed %s %r => %r' % (kind, old_path, new_path) if text_modified: _diff_one(old_tree.get_file(file_id).readlines(), new_tree.get_file(file_id).readlines(), sys.stdout, fromfile=old_label + old_path, tofile=new_label + new_path) for path, file_id, kind in delta.modified: print '*** modified %s %r' % (kind, path) if kind == 'file': _diff_one(old_tree.get_file(file_id).readlines(), new_tree.get_file(file_id).readlines(), sys.stdout, fromfile=old_label + path, tofile=new_label + path) class TreeDelta: """Describes changes from one tree to another. Contains four lists: added (path, id, kind) removed (path, id, kind) renamed (oldpath, newpath, id, kind, text_modified) modified (path, id, kind) unchanged (path, id, kind) Each id is listed only once. Files that are both modified and renamed are listed only in renamed, with the text_modified flag true. The lists are normally sorted when the delta is created. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] self.unchanged = [] def touches_file_id(self, file_id): """Return True if file_id is modified by this delta.""" for l in self.added, self.removed, self.modified: for v in l: if v[1] == file_id: return True for v in self.renamed: if v[2] == file_id: return True return False def show(self, to_file, show_ids=False, show_unchanged=False): def show_list(files): for path, fid, kind in files: if kind == 'directory': path += '/' elif kind == 'symlink': path += '@' if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if self.removed: print >>to_file, 'removed:' show_list(self.removed) if self.added: print >>to_file, 'added:' show_list(self.added) if self.renamed: print >>to_file, 'renamed:' for oldpath, newpath, fid, kind, text_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if self.modified: print >>to_file, 'modified:' show_list(self.modified) if show_unchanged and self.unchanged: print >>to_file, 'unchanged:' show_list(self.unchanged) def compare_trees(old_tree, new_tree, want_unchanged, specific_files=None): """Describe changes from one tree to another. Returns a TreeDelta with details of added, modified, renamed, and deleted entries. The root entry is specifically exempt. This only considers versioned files. want_unchanged If true, also list files unchanged from one version to the next. specific_files If true, only check for changes to specified names or files within them. """ from osutils import is_inside_any old_inv = old_tree.inventory new_inv = new_tree.inventory delta = TreeDelta() mutter('start compare_trees') # TODO: match for specific files can be rather smarter by finding # the IDs of those files up front and then considering only that. for file_id in old_tree: if file_id in new_tree: kind = old_inv.get_file_kind(file_id) assert kind == new_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ 'invalid file kind %r' % kind if kind == 'root_directory': continue old_path = old_inv.id2path(file_id) new_path = new_inv.id2path(file_id) if specific_files: if (not is_inside_any(specific_files, old_path) and not is_inside_any(specific_files, new_path)): continue if kind == 'file': old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False # TODO: Can possibly avoid calculating path strings if the # two files are unchanged and their names and parents are # the same and the parents are unchanged all the way up. # May not be worthwhile. if old_path != new_path: delta.renamed.append((old_path, new_path, file_id, kind, text_modified)) elif text_modified: delta.modified.append((new_path, file_id, kind)) elif want_unchanged: delta.unchanged.append((new_path, file_id, kind)) else: old_path = old_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, old_path): continue delta.removed.append((old_path, file_id, kind)) mutter('start looking for new files') for file_id in new_inv: if file_id in old_inv: continue new_path = new_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, new_path): continue kind = new_inv.get_file_kind(file_id) delta.added.append((new_path, file_id, kind)) delta.removed.sort() delta.added.sort() delta.renamed.sort() delta.modified.sort() delta.unchanged.sort() return delta M 644 inline bzrlib/info.py data 3500 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import time from osutils import format_date def _countiter(it): # surely there's a builtin for this? i = 0 for j in it: i += 1 return i def show_info(b): import diff print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl != None: return pl else: return 's' count_version_dirs = 0 basis = b.basis_tree() working = b.working_tree() work_inv = working.inventory delta = diff.compare_trees(basis, working, want_unchanged=True) print print 'in the working tree:' print ' %8s unchanged' % len(delta.unchanged) print ' %8d modified' % len(delta.modified) print ' %8d added' % len(delta.added) print ' %8d removed' % len(delta.removed) print ' %8d renamed' % len(delta.renamed) ignore_cnt = unknown_cnt = 0 for path in working.extras(): if working.is_ignored(path): ignore_cnt += 1 else: unknown_cnt += 1 print ' %8d unknown' % unknown_cnt print ' %8d ignored' % ignore_cnt dir_cnt = 0 for file_id in work_inv: if work_inv.get_file_kind(file_id) == 'directory': dir_cnt += 1 print ' %8d versioned %s' \ % (dir_cnt, plural(dir_cnt, 'subdirectory', 'subdirectories')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %8d revision%s' % (revno, plural(revno)) committers = {} for rev in history: committers[b.get_revision(rev).committer)] = True print ' %8d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %8d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) print print 'text store:' c, t = b.text_store.total_size() print ' %8d file texts' % c print ' %8d kB' % (t/1024) print print 'revision store:' c, t = b.revision_store.total_size() print ' %8d revisions' % c print ' %8d kB' % (t/1024) print print 'inventory store:' c, t = b.inventory_store.total_size() print ' %8d inventories' % c print ' %8d kB' % (t/1024) M 644 inline bzrlib/inventory.py data 18815 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # This should really be an id randomly assigned when the tree is # created, but it's not for now. ROOT_ID = "TREE_ROOT" import sys, os.path, types, re try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from xml import XMLMixin from errors import bailout, BzrError, BzrCheckError import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') 'TREE_ROOT' >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID)) >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123')) >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' TODO: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ # TODO: split InventoryEntry into subclasses for files, # directories, etc etc. text_sha1 = None text_size = None def __init__(self, file_id, name, kind, parent_id, text_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID) >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID) Traceback (most recent call last): BzrCheckError: InventoryEntry name 'src/hello.c' is invalid """ if '/' in name or '\\' in name: raise BzrCheckError('InventoryEntry name %r is invalid' % name) self.file_id = file_id self.name = name self.kind = kind self.text_id = text_id self.parent_id = parent_id if kind == 'directory': self.children = {} elif kind == 'file': pass else: raise BzrError("unhandled entry kind %r" % kind) def sorted_children(self): l = self.children.items() l.sort() return l def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.parent_id, text_id=self.text_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size # note that children are *not* copied; they're pulled across when # others are added return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size != None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1']: v = getattr(self, f) if v != None: e.set(f, v) # to be conservative, we don't externalize the root pointers # for now, leaving them as null in the xml form. in a future # version it will be implied by nested elements. if self.parent_id != ROOT_ID: assert isinstance(self.parent_id, basestring) e.set('parent_id', self.parent_id) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' ## original format inventories don't have a parent_id for ## nodes in the root directory, but it's cleaner to use one ## internally. parent_id = elt.get('parent_id') if parent_id == None: parent_id = ROOT_ID self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __cmp__(self, other): if self is other: return 0 if not isinstance(other, InventoryEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.name, other.name) \ or cmp(self.text_sha1, other.text_sha1) \ or cmp(self.text_size, other.text_size) \ or cmp(self.text_id, other.text_id) \ or cmp(self.parent_id, other.parent_id) \ or cmp(self.kind, other.kind) class RootEntry(InventoryEntry): def __init__(self, file_id): self.file_id = file_id self.children = {} self.kind = 'root_directory' self.parent_id = None self.name = '' def __cmp__(self, other): if self is other: return 0 if not isinstance(other, RootEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.children, other.children) class Inventory(XMLMixin): """Inventory of versioned files in a tree. This describes which file_id is present at each point in the tree, and possibly the SHA-1 or other information about the file. Entries can be looked up either by path or by file_id. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted, other than through the Inventory API. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID)) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. The inventory is created with a default root directory, with an id of None. """ self.root = RootEntry(ROOT_ID) self._byid = {self.root.file_id: self.root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, from_dir=None): """Return (path, entry) pairs, in order by name.""" if from_dir == None: assert self.root from_dir = self.root elif isinstance(from_dir, basestring): from_dir = self._byid[from_dir] kids = from_dir.children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(from_dir=ie.file_id): yield os.path.join(name, cn), cie def directories(self): """Return (path, entry) pairs for all directories. """ def descend(parent_ie): parent_name = parent_ie.name yield parent_name, parent_ie # directory children in sorted order dn = [] for ie in parent_ie.children.itervalues(): if ie.kind == 'directory': dn.append((ie.name, ie)) dn.sort() for name, child_ie in dn: for sub_name, sub_ie in descend(child_ie): yield appendpath(parent_name, sub_name), sub_ie for name, ie in descend(self.root): yield name, ie def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID)) >>> inv['123123'].name 'hello.c' """ try: return self._byid[file_id] except KeyError: if file_id == None: raise BzrError("can't look up file_id None") else: raise BzrError("file_id {%s} not in inventory" % file_id) def get_file_kind(self, file_id): return self._byid[file_id].kind def get_child(self, parent_id, filename): return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self._byid: bailout("inventory already contains entry with id {%s}" % entry.file_id) try: parent = self._byid[entry.parent_id] except KeyError: bailout("parent_id {%s} not in inventory" % entry.parent_id) if parent.children.has_key(entry.name): bailout("%s is already versioned" % appendpath(self.id2path(parent.file_id), entry.name)) self._byid[entry.file_id] = entry parent.children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: bailout("cannot re-add root of inventory") if file_id == None: file_id = bzrlib.branch.gen_file_id(relpath) parent_id = self.path2id(parts[:-1]) assert parent_id != None ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def id_set(self): from bzrlib import frozenset return frozenset(self._byid) def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID)) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __cmp__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 True """ if self is other: return 0 if not isinstance(other, Inventory): return NotImplemented if self.id_set() ^ other.id_set(): return 1 for file_id in self._byid: c = cmp(self[file_id], other[file_id]) if c: return c return 0 def get_idpath(self, file_id): """Return a list of file_ids for the path to an entry. The list contains one element for each directory followed by the id of the file itself. So the length of the returned list is equal to the depth of the file in the tree, counting the root directory as depth 1. """ p = [] while file_id != None: try: ie = self._byid[file_id] except KeyError: bailout("file_id {%s} not found in inventory" % file_id) p.insert(0, ie.file_id) file_id = ie.parent_id return p def id2path(self, file_id): """Return as a list the path to file_id.""" # get all names, skipping root p = [self[fid].name for fid in self.get_idpath(file_id)[1:]] return os.sep.join(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. Returns None iff the path is not found. """ if isinstance(name, types.StringTypes): name = splitpath(name) mutter("lookup path %r" % name) parent = self.root for f in name: try: cie = parent.children[f] assert cie.name == f assert cie.parent_id == parent.file_id parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): return self._byid.has_key(file_id) def rename(self, file_id, new_parent_id, new_name): """Move a file within the inventory. This can change either the name, or the parent, or both. This does not move the working file.""" if not is_valid_name(new_name): bailout("not an acceptable filename: %r" % new_name) new_parent = self._byid[new_parent_id] if new_name in new_parent.children: bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id))) new_parent_idpath = self.get_idpath(new_parent_id) if file_id in new_parent_idpath: bailout("cannot move directory %r into a subdirectory of itself, %r" % (self.id2path(file_id), self.id2path(new_parent_id))) file_ie = self._byid[file_id] old_parent = self._byid[file_ie.parent_id] # TODO: Don't leave things messed up if this fails del old_parent.children[file_ie.name] new_parent.children[new_name] = file_ie file_ie.name = new_name file_ie.parent_id = new_parent_id _NAME_RE = re.compile(r'^[^/\\]+$') def is_valid_name(name): return bool(_NAME_RE.match(name)) M 644 inline bzrlib/remotebranch.py data 6908 #! /usr/bin/env python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Proxy object for access to remote branches. At the moment remote branches are only for HTTP and only for read access. """ import gzip from cStringIO import StringIO import urllib2 from errors import BzrError, BzrCheckError from branch import Branch, BZR_BRANCH_FORMAT from trace import mutter # velocitynet.com.au transparently proxies connections and thereby # breaks keep-alive -- sucks! ENABLE_URLGRABBER = True if ENABLE_URLGRABBER: import urlgrabber import urlgrabber.keepalive urlgrabber.keepalive.DEBUG = 0 def get_url(path, compressed=False): try: url = path if compressed: url += '.gz' mutter("grab url %s" % url) url_f = urlgrabber.urlopen(url, keepalive=1, close_connection=0) if not compressed: return url_f else: return gzip.GzipFile(fileobj=StringIO(url_f.read())) except urllib2.URLError, e: raise BzrError("remote fetch failed: %r: %s" % (url, e)) else: def get_url(url, compressed=False): import urllib2 if compressed: url += '.gz' mutter("get_url %s" % url) url_f = urllib2.urlopen(url) if compressed: return gzip.GzipFile(fileobj=StringIO(url_f.read())) else: return url_f def _find_remote_root(url): """Return the prefix URL that corresponds to the branch root.""" orig_url = url while True: try: ff = get_url(url + '/.bzr/branch-format') fmt = ff.read() ff.close() fmt = fmt.rstrip('\r\n') if fmt != BZR_BRANCH_FORMAT.rstrip('\r\n'): raise BzrError("sorry, branch format %r not supported at url %s" % (fmt, url)) return url except urllib2.URLError: pass try: idx = url.rindex('/') except ValueError: raise BzrError('no branch root found for URL %s' % orig_url) url = url[:idx] class RemoteBranch(Branch): def __init__(self, baseurl, find_root=True, lock_mode='r'): """Create new proxy for a remote branch.""" if lock_mode not in ('', 'r'): raise BzrError('lock mode %r is not supported for remote branches' % lock_mode) if find_root: self.baseurl = _find_remote_root(baseurl) else: self.baseurl = baseurl self._check_format() self.inventory_store = RemoteStore(baseurl + '/.bzr/inventory-store/') self.text_store = RemoteStore(baseurl + '/.bzr/text-store/') def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.baseurl) __repr__ = __str__ def controlfile(self, filename, mode): if mode not in ('rb', 'rt', 'r'): raise BzrError("file mode %r not supported for remote branches" % mode) return get_url(self.baseurl + '/.bzr/' + filename, False) def _need_readlock(self): # remote branch always safe for read pass def _need_writelock(self): raise BzrError("cannot get write lock on HTTP remote branch") def relpath(self, path): if not path.startswith(self.baseurl): raise BzrError('path %r is not under base URL %r' % (path, self.baseurl)) pl = len(self.baseurl) return path[pl:].lstrip('/') def get_revision(self, revision_id): from revision import Revision revf = get_url(self.baseurl + '/.bzr/revision-store/' + revision_id, True) r = Revision.read_xml(revf) if r.revision_id != revision_id: raise BzrCheckError('revision stored as {%s} actually contains {%s}' % (revision_id, r.revision_id)) return r class RemoteStore: def __init__(self, baseurl): self._baseurl = baseurl def _path(self, name): if '/' in name: raise ValueError('invalid store id', name) return self._baseurl + '/' + name def __getitem__(self, fileid): p = self._path(fileid) return get_url(p, compressed=True) def simple_walk(): """For experimental purposes, traverse many parts of a remote branch""" from revision import Revision from branch import Branch from inventory import Inventory from bzrlib import set got_invs = set() got_texts = set() print 'read history' history = get_url('/.bzr/revision-history').readlines() num_revs = len(history) for i, rev_id in enumerate(history): rev_id = rev_id.rstrip() print 'read revision %d/%d' % (i, num_revs) # python gzip needs a seekable file (!!) but the HTTP response # isn't, so we need to buffer it rev_f = get_url('/.bzr/revision-store/%s' % rev_id, compressed=True) rev = Revision.read_xml(rev_f) print rev.message inv_id = rev.inventory_id if inv_id not in got_invs: print 'get inventory %s' % inv_id inv_f = get_url('/.bzr/inventory-store/%s' % inv_id, compressed=True) inv = Inventory.read_xml(inv_f) print '%4d inventory entries' % len(inv) for path, ie in inv.iter_entries(): text_id = ie.text_id if text_id == None: continue if text_id in got_texts: continue print ' fetch %s text {%s}' % (path, text_id) text_f = get_url('/.bzr/text-store/%s' % text_id, compressed=True) got_texts.add(text_id) got_invs.add(inv_id) print '----' def try_me(): BASE_URL = 'http://bazaar-ng.org/bzr/bzr.dev/' b = RemoteBranch(BASE_URL) ## print '\n'.join(b.revision_history()) from log import show_log show_log(b) if __name__ == '__main__': try_me() M 644 inline bzrlib/statcache.py data 8327 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import stat, os, sha, time from binascii import b2a_qp, a2b_qp from trace import mutter from errors import BzrError, BzrCheckError """File stat cache to speed up tree comparisons. This module basically gives a quick way to find the SHA-1 and related information of a file in the working directory, without actually reading and hashing the whole file. Implementation ============== Users of this module should not need to know about how this is implemented, and in particular should not depend on the particular data which is stored or its format. This is done by maintaining a cache indexed by a file fingerprint of (path, size, mtime, ctime, ino, dev) pointing to the SHA-1. If the fingerprint has changed, we assume the file content has not changed either and the SHA-1 is therefore the same. If any of the fingerprint fields have changed then the file content *may* have changed, or it may not have. We need to reread the file contents to make sure, but this is not visible to the user or higher-level code (except as a delay of course). The mtime and ctime are stored with nanosecond fields, but not all filesystems give this level of precision. There is therefore a possible race: the file might be modified twice within a second without changing the size or mtime, and a SHA-1 cached from the first version would be wrong. We handle this by not recording a cached hash for any files which were modified in the current second and that therefore have the chance to change again before the second is up. The only known hole in this design is if the system clock jumps backwards crossing invocations of bzr. Please don't do that; use ntp to gradually adjust your clock or don't use bzr over the step. At the moment this is stored in a simple textfile; it might be nice to use a tdb instead. The cache is represented as a map from file_id to a tuple of (file_id, sha1, path, size, mtime, ctime, ino, dev). The SHA-1 is stored in memory as a hexdigest. File names and file-ids are written out as the quoted-printable encoding of their UTF-8 representation. (file-ids shouldn't contain wierd characters, but it might happen.) """ # order of fields returned by fingerprint() FP_SIZE = 0 FP_MTIME = 1 FP_CTIME = 2 FP_INO = 3 FP_DEV = 4 # order of fields in the statcache file and in the in-memory map SC_FILE_ID = 0 SC_SHA1 = 1 SC_PATH = 2 SC_SIZE = 3 SC_MTIME = 4 SC_CTIME = 5 SC_INO = 6 SC_DEV = 7 CACHE_HEADER = "### bzr statcache v2" def fingerprint(abspath): try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) def _write_cache(basedir, entry_iter, dangerfiles): from atomicfile import AtomicFile cachefn = os.path.join(basedir, '.bzr', 'stat-cache') outf = AtomicFile(cachefn, 'wb') outf.write(CACHE_HEADER + '\n') try: for entry in entry_iter: if len(entry) != 8: raise ValueError("invalid statcache entry tuple %r" % entry) if entry[SC_FILE_ID] in dangerfiles: continue # changed too recently outf.write(b2a_qp(entry[0].encode('utf-8'))) # file id outf.write(' ') outf.write(entry[1]) # hex sha1 outf.write(' ') outf.write(b2a_qp(entry[2].encode('utf-8'), True)) # name for nf in entry[3:]: outf.write(' %d' % nf) outf.write('\n') outf.commit() finally: if not outf.closed: outf.abort() def load_cache(basedir): import re cache = {} seen_paths = {} sha_re = re.compile(r'[a-f0-9]{40}') try: cachefn = os.path.join(basedir, '.bzr', 'stat-cache') cachefile = open(cachefn, 'rb') except IOError: return cache line1 = cachefile.readline().rstrip('\r\n') if line1 != CACHE_HEADER: mutter('cache header marker not found at top of %s' % cachefn) return cache for l in cachefile: f = l.split(' ') file_id = a2b_qp(f[0]).decode('utf-8') if file_id in cache: raise BzrCheckError("duplicated file_id in cache: {%s}" % file_id) text_sha = f[1] if len(text_sha) != 40 or not sha_re.match(text_sha): raise BzrCheckError("invalid file SHA-1 in cache: %r" % text_sha) path = a2b_qp(f[2]).decode('utf-8') if path in seen_paths: raise BzrCheckError("duplicated path in cache: %r" % path) seen_paths[path] = True entry = (file_id, text_sha, path) + tuple([long(x) for x in f[3:]]) if len(entry) != 8: raise ValueError("invalid statcache entry tuple %r" % entry) cache[file_id] = entry return cache def _files_from_inventory(inv): for path, ie in inv.iter_entries(): if ie.kind != 'file': continue yield ie.file_id, path def update_cache(basedir, inv, flush=False): """Update and return the cache for the branch. The returned cache may contain entries that have not been written to disk for files recently touched. flush -- discard any previous cache and recalculate from scratch. """ # TODO: It's supposed to be faster to stat the files in order by inum. # We don't directly know the inum of the files of course but we do # know where they were last sighted, so we can sort by that. assert isinstance(flush, bool) if flush: cache = {} else: cache = load_cache(basedir) return _update_cache_from_list(basedir, cache, _files_from_inventory(inv)) def _update_cache_from_list(basedir, cache, to_update): """Update and return the cache for given files. cache -- Previously cached values to be validated. to_update -- Sequence of (file_id, path) pairs to check. """ stat_cnt = missing_cnt = hardcheck = change_cnt = 0 # dangerfiles have been recently touched and can't be # committed to a persistent cache yet. dangerfiles = {} now = int(time.time()) ## mutter('update statcache under %r' % basedir) for file_id, path in to_update: abspath = os.path.join(basedir, path) fp = fingerprint(abspath) stat_cnt += 1 cacheentry = cache.get(file_id) if fp == None: # not here if cacheentry: del cache[file_id] change_cnt += 1 missing_cnt += 1 continue if (fp[FP_MTIME] >= now) or (fp[FP_CTIME] >= now): dangerfiles[file_id] = True if cacheentry and (cacheentry[3:] == fp): continue # all stat fields unchanged hardcheck += 1 dig = sha.new(file(abspath, 'rb').read()).hexdigest() # We update the cache even if the digest has not changed from # last time we looked, so that the fingerprint fields will # match in future. cacheentry = (file_id, dig, path) + fp cache[file_id] = cacheentry change_cnt += 1 mutter('statcache: statted %d files, read %d files, %d changed, %d dangerous, ' '%d in cache' % (stat_cnt, hardcheck, change_cnt, len(dangerfiles), len(cache))) if change_cnt: mutter('updating on-disk statcache') _write_cache(basedir, cache.itervalues(), dangerfiles) return cache commit refs/heads/master mark :541 committer Martin Pool 1116486866 +1000 data 30 - add lazy test for 'bzr info' from :540 M 644 inline testbzr data 10729 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. usage: testbzr [-p PYTHON] [BZR] By default this tests the copy of bzr found in the same directory as testbzr, or the first one found on the $PATH. A copy of bzr may be given on the command line to override this, for example when applying a new test suite to an old copy of bzr or vice versa. testbzr normally invokes bzr using the same version of python as it would normally use to run -- that is, the system default python, unless that is older than 2.3. The -p option allows specification of a different Python interpreter, such as when testing that bzr still works on python2.3. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" OVERRIDE_PYTHON = None LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = BZRPATH if OVERRIDE_PYTHON: cmd.insert(0, OVERRIDE_PYTHON) logfile.write('$ %r\n' % cmd) return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) start_dir = os.getcwd() logfile = open(LOGFILENAME, 'wt', buffering=1) try: from getopt import getopt opts, args = getopt(sys.argv[1:], 'p:') for option, value in opts: if option == '-p': OVERRIDE_PYTHON = value mypath = os.path.abspath(sys.argv[0]) print '%-30s %s' % ('running tests from', mypath) global BZRPATH if args: BZRPATH = args[1] else: BZRPATH = os.path.join(os.path.split(mypath)[0], 'bzr') print '%-30s %s' % ('against bzr', BZRPATH) print '%-30s %s' % ('in directory', os.getcwd()) print '%-30s %s' % ('with python', (OVERRIDE_PYTHON or '(default)')) print print backtick([BZRPATH, 'version']) runcmd(['mkdir', TESTDIR]) cd(TESTDIR) test_root = os.getcwd() progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("internal tests") runcmd("bzr selftest") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) runcmd("bzr diff --message foo", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == 'unknown:\n test.txt\n' out = backtick("bzr status --all") assert out == "unknown:\n test.txt\n" out = backtick("bzr status test.txt --all") assert out == "unknown:\n test.txt\n" f = file('test2.txt', 'wt') f.write('goodbye cruel world...\n') f.close() out = backtick("bzr status test.txt") assert out == "unknown:\n test.txt\n" out = backtick("bzr status") assert out == ("unknown:\n" " test.txt\n" " test2.txt\n") os.unlink('test2.txt') progress("command aliases") out = backtick("bzr st --all") assert out == ("unknown:\n" " test.txt\n") out = backtick("bzr stat") assert out == ("unknown:\n" " test.txt\n") progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == ("added:\n" " test.txt\n") progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == "sub2/hello.txt\n" assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == 'sub1/sub2/hello.txt\n' assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') assert backtick('bzr relpath sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') runcmd('bzr log') runcmd('bzr log -v') progress("file with spaces in name") mkdir('sub directory') file('sub directory/file with spaces ', 'wt').write('see how this works\n') runcmd('bzr add .') runcmd('bzr diff') runcmd('bzr commit -m add-spaces') runcmd('bzr check') runcmd('bzr info') cd('..') cd('..') progress('ignore patterns') mkdir('ignorebranch') cd('ignorebranch') runcmd('bzr init') assert backtick('bzr unknowns') == '' file('foo.tmp', 'wt').write('tmp files are ignored') assert backtick('bzr unknowns') == '' file('foo.c', 'wt').write('int main() {}') assert backtick('bzr unknowns') == 'foo.c\n' runcmd('bzr add foo.c') assert backtick('bzr unknowns') == '' # 'ignore' works when creating the .bzignore file file('foo.blah', 'wt').write('blah') assert backtick('bzr unknowns') == 'foo.blah\n' runcmd('bzr ignore *.blah') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\n' # 'ignore' works when then .bzrignore file already exists file('garh', 'wt').write('garh') assert backtick('bzr unknowns') == 'garh\n' runcmd('bzr ignore garh') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\ngarh\n' progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) logfile.close() sys.stdout.writelines(file(os.path.join(start_dir, LOGFILENAME), 'rt').readlines()[-50:]) sys.exit(1) commit refs/heads/master mark :542 committer Martin Pool 1116491466 +1000 data 10 - fix typo from :541 M 644 inline bzrlib/info.py data 3499 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import time from osutils import format_date def _countiter(it): # surely there's a builtin for this? i = 0 for j in it: i += 1 return i def show_info(b): import diff print 'branch format:', b.controlfile('branch-format', 'r').readline().rstrip('\n') def plural(n, base='', pl=None): if n == 1: return base elif pl != None: return pl else: return 's' count_version_dirs = 0 basis = b.basis_tree() working = b.working_tree() work_inv = working.inventory delta = diff.compare_trees(basis, working, want_unchanged=True) print print 'in the working tree:' print ' %8s unchanged' % len(delta.unchanged) print ' %8d modified' % len(delta.modified) print ' %8d added' % len(delta.added) print ' %8d removed' % len(delta.removed) print ' %8d renamed' % len(delta.renamed) ignore_cnt = unknown_cnt = 0 for path in working.extras(): if working.is_ignored(path): ignore_cnt += 1 else: unknown_cnt += 1 print ' %8d unknown' % unknown_cnt print ' %8d ignored' % ignore_cnt dir_cnt = 0 for file_id in work_inv: if work_inv.get_file_kind(file_id) == 'directory': dir_cnt += 1 print ' %8d versioned %s' \ % (dir_cnt, plural(dir_cnt, 'subdirectory', 'subdirectories')) print print 'branch history:' history = b.revision_history() revno = len(history) print ' %8d revision%s' % (revno, plural(revno)) committers = {} for rev in history: committers[b.get_revision(rev).committer] = True print ' %8d committer%s' % (len(committers), plural(len(committers))) if revno > 0: firstrev = b.get_revision(history[0]) age = int((time.time() - firstrev.timestamp) / 3600 / 24) print ' %8d day%s old' % (age, plural(age)) print ' first revision: %s' % format_date(firstrev.timestamp, firstrev.timezone) lastrev = b.get_revision(history[-1]) print ' latest revision: %s' % format_date(lastrev.timestamp, lastrev.timezone) print print 'text store:' c, t = b.text_store.total_size() print ' %8d file texts' % c print ' %8d kB' % (t/1024) print print 'revision store:' c, t = b.revision_store.total_size() print ' %8d revisions' % c print ' %8d kB' % (t/1024) print print 'inventory store:' c, t = b.inventory_store.total_size() print ' %8d inventories' % c print ' %8d kB' % (t/1024) commit refs/heads/master mark :543 committer Martin Pool 1116496789 +1000 data 179 - More cleanups for set type - Clean up Inventory cmp method - Remove the Inventory.id_set and Tree.id_set methods: don't built sets when just using the dictionaries will do. from :542 M 644 inline bzrlib/__init__.py data 1757 # (C) 2005 Canonical Development Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr library""" from inventory import Inventory, InventoryEntry from branch import Branch, ScratchBranch, find_branch from osutils import format_date from tree import Tree from diff import compare_trees from trace import mutter, warning, open_tracefile from log import show_log import add BZRDIR = ".bzr" DEFAULT_IGNORE = ['.bzr.log', '*~', '#*#', '*$', '.#*', '.*.swp', '.*.tmp', '*.tmp', '*.bak', '*.BAK', '*.orig', '*.o', '*.obj', '*.a', '*.py[oc]', '*.so', '*.exe', '*.elc', '{arch}', 'CVS', 'CVS.adm', '.svn', '_darcs', 'SCCS', 'RCS', '*,v', 'BitKeeper', 'TAGS', '.make.state', '.sconsign', '.tmp*', '.del-*'] IGNORE_FILENAME = ".bzrignore" import locale user_encoding = locale.getpreferredencoding() del locale __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __version__ = '0.0.5pre' M 644 inline bzrlib/check.py data 4276 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ###################################################################### # consistency checks import sys from trace import mutter from errors import bailout import osutils def check(branch, progress=True): out = sys.stdout # TODO: factor out if not (hasattr(out, 'isatty') and out.isatty()): progress=False if progress: def p(m): mutter('checking ' + m) out.write('\rchecking: %-50.50s' % m) out.flush() else: def p(m): mutter('checking ' + m) p('history of %r' % branch.base) last_ptr = None checked_revs = {} history = branch.revision_history() revno = 0 revcount = len(history) checked_texts = {} for rid in history: revno += 1 p('revision %d/%d' % (revno, revcount)) mutter(' revision {%s}' % rid) rev = branch.get_revision(rid) if rev.revision_id != rid: bailout('wrong internal revision id in revision {%s}' % rid) if rev.precursor != last_ptr: bailout('mismatched precursor in revision {%s}' % rid) last_ptr = rid if rid in checked_revs: bailout('repeated revision {%s}' % rid) checked_revs[rid] = True ## TODO: Check all the required fields are present on the revision. inv = branch.get_inventory(rev.inventory_id) seen_ids = {} seen_names = {} p('revision %d/%d file ids' % (revno, revcount)) for file_id in inv: if file_id in seen_ids: bailout('duplicated file_id {%s} in inventory for revision {%s}' % (file_id, rid)) seen_ids[file_id] = True i = 0 len_inv = len(inv) for file_id in inv: i += 1 if (i % 100) == 0: p('revision %d/%d file text %d/%d' % (revno, revcount, i, len_inv)) ie = inv[file_id] if ie.parent_id != None: if ie.parent_id not in seen_ids: bailout('missing parent {%s} in inventory for revision {%s}' % (ie.parent_id, rid)) if ie.kind == 'file': if ie.text_id in checked_texts: fp = checked_texts[ie.text_id] else: if not ie.text_id in branch.text_store: bailout('text {%s} not in text_store' % ie.text_id) tf = branch.text_store[ie.text_id] fp = osutils.fingerprint_file(tf) checked_texts[ie.text_id] = fp if ie.text_size != fp['size']: bailout('text {%s} wrong size' % ie.text_id) if ie.text_sha1 != fp['sha1']: bailout('text {%s} wrong sha1' % ie.text_id) elif ie.kind == 'directory': if ie.text_sha1 != None or ie.text_size != None or ie.text_id != None: bailout('directory {%s} has text in revision {%s}' % (file_id, rid)) p('revision %d/%d file paths' % (revno, revcount)) for path, ie in inv.iter_entries(): if path in seen_names: bailout('duplicated path %r in inventory for revision {%s}' % (path, revid)) seen_names[path] = True p('done') if progress: print print 'checked %d revisions, %d file texts' % (revcount, len(checked_texts)) M 644 inline bzrlib/inventory.py data 19016 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # This should really be an id randomly assigned when the tree is # created, but it's not for now. ROOT_ID = "TREE_ROOT" import sys, os.path, types, re try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from xml import XMLMixin from errors import bailout, BzrError, BzrCheckError import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') 'TREE_ROOT' >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID)) >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123')) >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' TODO: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ # TODO: split InventoryEntry into subclasses for files, # directories, etc etc. text_sha1 = None text_size = None def __init__(self, file_id, name, kind, parent_id, text_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID) >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID) Traceback (most recent call last): BzrCheckError: InventoryEntry name 'src/hello.c' is invalid """ if '/' in name or '\\' in name: raise BzrCheckError('InventoryEntry name %r is invalid' % name) self.file_id = file_id self.name = name self.kind = kind self.text_id = text_id self.parent_id = parent_id if kind == 'directory': self.children = {} elif kind == 'file': pass else: raise BzrError("unhandled entry kind %r" % kind) def sorted_children(self): l = self.children.items() l.sort() return l def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.parent_id, text_id=self.text_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size # note that children are *not* copied; they're pulled across when # others are added return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size != None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1']: v = getattr(self, f) if v != None: e.set(f, v) # to be conservative, we don't externalize the root pointers # for now, leaving them as null in the xml form. in a future # version it will be implied by nested elements. if self.parent_id != ROOT_ID: assert isinstance(self.parent_id, basestring) e.set('parent_id', self.parent_id) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' ## original format inventories don't have a parent_id for ## nodes in the root directory, but it's cleaner to use one ## internally. parent_id = elt.get('parent_id') if parent_id == None: parent_id = ROOT_ID self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __cmp__(self, other): if self is other: return 0 if not isinstance(other, InventoryEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.name, other.name) \ or cmp(self.text_sha1, other.text_sha1) \ or cmp(self.text_size, other.text_size) \ or cmp(self.text_id, other.text_id) \ or cmp(self.parent_id, other.parent_id) \ or cmp(self.kind, other.kind) class RootEntry(InventoryEntry): def __init__(self, file_id): self.file_id = file_id self.children = {} self.kind = 'root_directory' self.parent_id = None self.name = '' def __cmp__(self, other): if self is other: return 0 if not isinstance(other, RootEntry): return NotImplemented return cmp(self.file_id, other.file_id) \ or cmp(self.children, other.children) class Inventory(XMLMixin): """Inventory of versioned files in a tree. This describes which file_id is present at each point in the tree, and possibly the SHA-1 or other information about the file. Entries can be looked up either by path or by file_id. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted, other than through the Inventory API. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID)) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. The inventory is created with a default root directory, with an id of None. """ self.root = RootEntry(ROOT_ID) self._byid = {self.root.file_id: self.root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, from_dir=None): """Return (path, entry) pairs, in order by name.""" if from_dir == None: assert self.root from_dir = self.root elif isinstance(from_dir, basestring): from_dir = self._byid[from_dir] kids = from_dir.children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(from_dir=ie.file_id): yield os.path.join(name, cn), cie def directories(self): """Return (path, entry) pairs for all directories. """ def descend(parent_ie): parent_name = parent_ie.name yield parent_name, parent_ie # directory children in sorted order dn = [] for ie in parent_ie.children.itervalues(): if ie.kind == 'directory': dn.append((ie.name, ie)) dn.sort() for name, child_ie in dn: for sub_name, sub_ie in descend(child_ie): yield appendpath(parent_name, sub_name), sub_ie for name, ie in descend(self.root): yield name, ie def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID)) >>> inv['123123'].name 'hello.c' """ try: return self._byid[file_id] except KeyError: if file_id == None: raise BzrError("can't look up file_id None") else: raise BzrError("file_id {%s} not in inventory" % file_id) def get_file_kind(self, file_id): return self._byid[file_id].kind def get_child(self, parent_id, filename): return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self._byid: bailout("inventory already contains entry with id {%s}" % entry.file_id) try: parent = self._byid[entry.parent_id] except KeyError: bailout("parent_id {%s} not in inventory" % entry.parent_id) if parent.children.has_key(entry.name): bailout("%s is already versioned" % appendpath(self.id2path(parent.file_id), entry.name)) self._byid[entry.file_id] = entry parent.children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: bailout("cannot re-add root of inventory") if file_id == None: file_id = bzrlib.branch.gen_file_id(relpath) parent_id = self.path2id(parts[:-1]) assert parent_id != None ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID)) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __cmp__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 True """ if self is other: return 0 if not isinstance(other, Inventory): return NotImplemented byid = self._byid otherids = other._byid if len(byid) != len(otherids): # shortcut: obviously not the same return 1 for file_id in byid: if file_id not in otherids: return 1 c = cmp(byid[file_id], otherids[file_id]) if c: return c for file_id in otherids: if file_id not in byid: return 1 return 0 def get_idpath(self, file_id): """Return a list of file_ids for the path to an entry. The list contains one element for each directory followed by the id of the file itself. So the length of the returned list is equal to the depth of the file in the tree, counting the root directory as depth 1. """ p = [] while file_id != None: try: ie = self._byid[file_id] except KeyError: bailout("file_id {%s} not found in inventory" % file_id) p.insert(0, ie.file_id) file_id = ie.parent_id return p def id2path(self, file_id): """Return as a list the path to file_id.""" # get all names, skipping root p = [self[fid].name for fid in self.get_idpath(file_id)[1:]] return os.sep.join(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. Returns None iff the path is not found. """ if isinstance(name, types.StringTypes): name = splitpath(name) mutter("lookup path %r" % name) parent = self.root for f in name: try: cie = parent.children[f] assert cie.name == f assert cie.parent_id == parent.file_id parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): return self._byid.has_key(file_id) def rename(self, file_id, new_parent_id, new_name): """Move a file within the inventory. This can change either the name, or the parent, or both. This does not move the working file.""" if not is_valid_name(new_name): bailout("not an acceptable filename: %r" % new_name) new_parent = self._byid[new_parent_id] if new_name in new_parent.children: bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id))) new_parent_idpath = self.get_idpath(new_parent_id) if file_id in new_parent_idpath: bailout("cannot move directory %r into a subdirectory of itself, %r" % (self.id2path(file_id), self.id2path(new_parent_id))) file_ie = self._byid[file_id] old_parent = self._byid[file_ie.parent_id] # TODO: Don't leave things messed up if this fails del old_parent.children[file_ie.name] new_parent.children[new_name] = file_ie file_ie.name = new_name file_ie.parent_id = new_parent_id _NAME_RE = re.compile(r'^[^/\\]+$') def is_valid_name(name): return bool(_NAME_RE.match(name)) M 644 inline bzrlib/remotebranch.py data 6885 #! /usr/bin/env python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Proxy object for access to remote branches. At the moment remote branches are only for HTTP and only for read access. """ import gzip from cStringIO import StringIO import urllib2 from errors import BzrError, BzrCheckError from branch import Branch, BZR_BRANCH_FORMAT from trace import mutter # velocitynet.com.au transparently proxies connections and thereby # breaks keep-alive -- sucks! ENABLE_URLGRABBER = True if ENABLE_URLGRABBER: import urlgrabber import urlgrabber.keepalive urlgrabber.keepalive.DEBUG = 0 def get_url(path, compressed=False): try: url = path if compressed: url += '.gz' mutter("grab url %s" % url) url_f = urlgrabber.urlopen(url, keepalive=1, close_connection=0) if not compressed: return url_f else: return gzip.GzipFile(fileobj=StringIO(url_f.read())) except urllib2.URLError, e: raise BzrError("remote fetch failed: %r: %s" % (url, e)) else: def get_url(url, compressed=False): import urllib2 if compressed: url += '.gz' mutter("get_url %s" % url) url_f = urllib2.urlopen(url) if compressed: return gzip.GzipFile(fileobj=StringIO(url_f.read())) else: return url_f def _find_remote_root(url): """Return the prefix URL that corresponds to the branch root.""" orig_url = url while True: try: ff = get_url(url + '/.bzr/branch-format') fmt = ff.read() ff.close() fmt = fmt.rstrip('\r\n') if fmt != BZR_BRANCH_FORMAT.rstrip('\r\n'): raise BzrError("sorry, branch format %r not supported at url %s" % (fmt, url)) return url except urllib2.URLError: pass try: idx = url.rindex('/') except ValueError: raise BzrError('no branch root found for URL %s' % orig_url) url = url[:idx] class RemoteBranch(Branch): def __init__(self, baseurl, find_root=True, lock_mode='r'): """Create new proxy for a remote branch.""" if lock_mode not in ('', 'r'): raise BzrError('lock mode %r is not supported for remote branches' % lock_mode) if find_root: self.baseurl = _find_remote_root(baseurl) else: self.baseurl = baseurl self._check_format() self.inventory_store = RemoteStore(baseurl + '/.bzr/inventory-store/') self.text_store = RemoteStore(baseurl + '/.bzr/text-store/') def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.baseurl) __repr__ = __str__ def controlfile(self, filename, mode): if mode not in ('rb', 'rt', 'r'): raise BzrError("file mode %r not supported for remote branches" % mode) return get_url(self.baseurl + '/.bzr/' + filename, False) def _need_readlock(self): # remote branch always safe for read pass def _need_writelock(self): raise BzrError("cannot get write lock on HTTP remote branch") def relpath(self, path): if not path.startswith(self.baseurl): raise BzrError('path %r is not under base URL %r' % (path, self.baseurl)) pl = len(self.baseurl) return path[pl:].lstrip('/') def get_revision(self, revision_id): from revision import Revision revf = get_url(self.baseurl + '/.bzr/revision-store/' + revision_id, True) r = Revision.read_xml(revf) if r.revision_id != revision_id: raise BzrCheckError('revision stored as {%s} actually contains {%s}' % (revision_id, r.revision_id)) return r class RemoteStore: def __init__(self, baseurl): self._baseurl = baseurl def _path(self, name): if '/' in name: raise ValueError('invalid store id', name) return self._baseurl + '/' + name def __getitem__(self, fileid): p = self._path(fileid) return get_url(p, compressed=True) def simple_walk(): """For experimental purposes, traverse many parts of a remote branch""" from revision import Revision from branch import Branch from inventory import Inventory got_invs = {} got_texts = {} print 'read history' history = get_url('/.bzr/revision-history').readlines() num_revs = len(history) for i, rev_id in enumerate(history): rev_id = rev_id.rstrip() print 'read revision %d/%d' % (i, num_revs) # python gzip needs a seekable file (!!) but the HTTP response # isn't, so we need to buffer it rev_f = get_url('/.bzr/revision-store/%s' % rev_id, compressed=True) rev = Revision.read_xml(rev_f) print rev.message inv_id = rev.inventory_id if inv_id not in got_invs: print 'get inventory %s' % inv_id inv_f = get_url('/.bzr/inventory-store/%s' % inv_id, compressed=True) inv = Inventory.read_xml(inv_f) print '%4d inventory entries' % len(inv) for path, ie in inv.iter_entries(): text_id = ie.text_id if text_id == None: continue if text_id in got_texts: continue print ' fetch %s text {%s}' % (path, text_id) text_f = get_url('/.bzr/text-store/%s' % text_id, compressed=True) got_texts[text_id] = True got_invs.add[inv_id] = True print '----' def try_me(): BASE_URL = 'http://bazaar-ng.org/bzr/bzr.dev/' b = RemoteBranch(BASE_URL) ## print '\n'.join(b.revision_history()) from log import show_log show_log(b) if __name__ == '__main__': try_me() M 644 inline bzrlib/tree.py data 7745 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tree classes, representing directory at point in time. """ from sets import Set import os.path, os, fnmatch from osutils import pumpfile, filesize, quotefn, sha_file, \ joinpath, splitpath, appendpath, isdir, isfile, file_kind, fingerprint_file import errno from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE from inventory import Inventory from trace import mutter, note from errors import bailout import branch import bzrlib class Tree: """Abstract file tree. There are several subclasses: * `WorkingTree` exists as files on disk editable by the user. * `RevisionTree` is a tree as recorded at some point in the past. * `EmptyTree` Trees contain an `Inventory` object, and also know how to retrieve file texts mentioned in the inventory, either from a working directory or from a store. It is possible for trees to contain files that are not described in their inventory or vice versa; for this use `filenames()`. Trees can be compared, etc, regardless of whether they are working trees or versioned trees. """ def has_filename(self, filename): """True if the tree has given filename.""" raise NotImplementedError() def has_id(self, file_id): return self.inventory.has_id(file_id) __contains__ = has_id def __iter__(self): return iter(self.inventory) def id2path(self, file_id): return self.inventory.id2path(file_id) def _get_inventory(self): return self._inventory inventory = property(_get_inventory, doc="Inventory of this Tree") def _check_retrieved(self, ie, f): fp = fingerprint_file(f) f.seek(0) if ie.text_size != None: if ie.text_size != fp['size']: bailout("mismatched size for file %r in %r" % (ie.file_id, self._store), ["inventory expects %d bytes" % ie.text_size, "file is actually %d bytes" % fp['size'], "store is probably damaged/corrupt"]) if ie.text_sha1 != fp['sha1']: bailout("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store), ["inventory expects %s" % ie.text_sha1, "file is actually %s" % fp['sha1'], "store is probably damaged/corrupt"]) def print_file(self, fileid): """Print file with id `fileid` to stdout.""" import sys pumpfile(self.get_file(fileid), sys.stdout) def export(self, dest): """Export this tree to a new directory. `dest` should not exist, and will be created holding the contents of this tree. TODO: To handle subdirectories we need to create the directories first. :note: If the export fails, the destination directory will be left in a half-assed state. """ os.mkdir(dest) mutter('export version %r' % self) inv = self.inventory for dp, ie in inv.iter_entries(): kind = ie.kind fullpath = appendpath(dest, dp) if kind == 'directory': os.mkdir(fullpath) elif kind == 'file': pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb')) else: bailout("don't know how to export {%s} of kind %r" % (ie.file_id, kind)) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) class RevisionTree(Tree): """Tree viewing a previous revision. File text can be retrieved from the text store. TODO: Some kind of `__repr__` method, but a good one probably means knowing the branch and revision number, or at least passing a description to the constructor. """ def __init__(self, store, inv): self._store = store self._inventory = inv def get_file(self, file_id): ie = self._inventory[file_id] f = self._store[ie.text_id] mutter(" get fileid{%s} from %r" % (file_id, self)) self._check_retrieved(ie, f) return f def get_file_size(self, file_id): return self._inventory[file_id].text_size def get_file_sha1(self, file_id): ie = self._inventory[file_id] return ie.text_sha1 def has_filename(self, filename): return bool(self.inventory.path2id(filename)) def list_files(self): # The only files returned by this are those from the version for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id class EmptyTree(Tree): def __init__(self): self._inventory = Inventory() def has_filename(self, filename): return False def list_files(self): if False: # just to make it a generator yield None ###################################################################### # diff # TODO: Merge these two functions into a single one that can operate # on either a whole tree or a set of files. # TODO: Return the diff in order by filename, not by category or in # random order. Can probably be done by lock-stepping through the # filenames from both trees. def file_status(filename, old_tree, new_tree): """Return single-letter status, old and new names for a file. The complexity here is in deciding how to represent renames; many complex cases are possible. """ old_inv = old_tree.inventory new_inv = new_tree.inventory new_id = new_inv.path2id(filename) old_id = old_inv.path2id(filename) if not new_id and not old_id: # easy: doesn't exist in either; not versioned at all if new_tree.is_ignored(filename): return 'I', None, None else: return '?', None, None elif new_id: # There is now a file of this name, great. pass else: # There is no longer a file of this name, but we can describe # what happened to the file that used to have # this name. There are two possibilities: either it was # deleted entirely, or renamed. assert old_id if new_inv.has_id(old_id): return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id) else: return 'D', old_inv.id2path(old_id), None # if the file_id is new in this revision, it is added if new_id and not old_inv.has_id(new_id): return 'A' # if there used to be a file of this name, but that ID has now # disappeared, it is deleted if old_id and not new_inv.has_id(old_id): return 'D' return 'wtf?' def find_renames(old_inv, new_inv): for file_id in old_inv: if file_id not in new_inv: continue old_name = old_inv.id2path(file_id) new_name = new_inv.id2path(file_id) if old_name != new_name: yield (old_name, new_name) commit refs/heads/master mark :544 committer Martin Pool 1116500856 +1000 data 198 - Define __eq__ and __ne__ for Inventory and InventoryEntry objects, not __cmp__. (Apparently this is the preferred style for modern Python code.) - Make those classes explicitly not hashable. from :543 M 644 inline bzrlib/inventory.py data 18803 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # This should really be an id randomly assigned when the tree is # created, but it's not for now. ROOT_ID = "TREE_ROOT" import sys, os.path, types, re try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from xml import XMLMixin from errors import bailout, BzrError, BzrCheckError import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') 'TREE_ROOT' >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID)) >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123')) >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' TODO: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ # TODO: split InventoryEntry into subclasses for files, # directories, etc etc. text_sha1 = None text_size = None def __init__(self, file_id, name, kind, parent_id, text_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID) >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID) Traceback (most recent call last): BzrCheckError: InventoryEntry name 'src/hello.c' is invalid """ if '/' in name or '\\' in name: raise BzrCheckError('InventoryEntry name %r is invalid' % name) self.file_id = file_id self.name = name self.kind = kind self.text_id = text_id self.parent_id = parent_id if kind == 'directory': self.children = {} elif kind == 'file': pass else: raise BzrError("unhandled entry kind %r" % kind) def sorted_children(self): l = self.children.items() l.sort() return l def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.parent_id, text_id=self.text_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size # note that children are *not* copied; they're pulled across when # others are added return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size != None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1']: v = getattr(self, f) if v != None: e.set(f, v) # to be conservative, we don't externalize the root pointers # for now, leaving them as null in the xml form. in a future # version it will be implied by nested elements. if self.parent_id != ROOT_ID: assert isinstance(self.parent_id, basestring) e.set('parent_id', self.parent_id) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' ## original format inventories don't have a parent_id for ## nodes in the root directory, but it's cleaner to use one ## internally. parent_id = elt.get('parent_id') if parent_id == None: parent_id = ROOT_ID self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __eq__(self, other): if not isinstance(other, InventoryEntry): return NotImplemented return (self.file_id == other.file_id) \ and (self.name == other.name) \ and (self.text_sha1 == other.text_sha1) \ and (self.text_size == other.text_size) \ and (self.text_id == other.text_id) \ and (self.parent_id == other.parent_id) \ and (self.kind == other.kind) def __ne__(self, other): return not (self == other) def __hash__(self): raise ValueError('not hashable') class RootEntry(InventoryEntry): def __init__(self, file_id): self.file_id = file_id self.children = {} self.kind = 'root_directory' self.parent_id = None self.name = '' def __eq__(self, other): if not isinstance(other, RootEntry): return NotImplemented return (self.file_id == other.file_id) \ and (self.children == other.children) class Inventory(XMLMixin): """Inventory of versioned files in a tree. This describes which file_id is present at each point in the tree, and possibly the SHA-1 or other information about the file. Entries can be looked up either by path or by file_id. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted, other than through the Inventory API. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID)) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. The inventory is created with a default root directory, with an id of None. """ self.root = RootEntry(ROOT_ID) self._byid = {self.root.file_id: self.root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, from_dir=None): """Return (path, entry) pairs, in order by name.""" if from_dir == None: assert self.root from_dir = self.root elif isinstance(from_dir, basestring): from_dir = self._byid[from_dir] kids = from_dir.children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(from_dir=ie.file_id): yield os.path.join(name, cn), cie def directories(self): """Return (path, entry) pairs for all directories. """ def descend(parent_ie): parent_name = parent_ie.name yield parent_name, parent_ie # directory children in sorted order dn = [] for ie in parent_ie.children.itervalues(): if ie.kind == 'directory': dn.append((ie.name, ie)) dn.sort() for name, child_ie in dn: for sub_name, sub_ie in descend(child_ie): yield appendpath(parent_name, sub_name), sub_ie for name, ie in descend(self.root): yield name, ie def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID)) >>> inv['123123'].name 'hello.c' """ try: return self._byid[file_id] except KeyError: if file_id == None: raise BzrError("can't look up file_id None") else: raise BzrError("file_id {%s} not in inventory" % file_id) def get_file_kind(self, file_id): return self._byid[file_id].kind def get_child(self, parent_id, filename): return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self._byid: bailout("inventory already contains entry with id {%s}" % entry.file_id) try: parent = self._byid[entry.parent_id] except KeyError: bailout("parent_id {%s} not in inventory" % entry.parent_id) if parent.children.has_key(entry.name): bailout("%s is already versioned" % appendpath(self.id2path(parent.file_id), entry.name)) self._byid[entry.file_id] = entry parent.children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: bailout("cannot re-add root of inventory") if file_id == None: file_id = bzrlib.branch.gen_file_id(relpath) parent_id = self.path2id(parts[:-1]) assert parent_id != None ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID)) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __eq__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 True """ if not isinstance(other, Inventory): return NotImplemented if len(self._byid) != len(other._byid): # shortcut: obviously not the same return False return self._byid == other._byid def __ne__(self, other): return not (self == other) def __hash__(self): raise ValueError('not hashable') def get_idpath(self, file_id): """Return a list of file_ids for the path to an entry. The list contains one element for each directory followed by the id of the file itself. So the length of the returned list is equal to the depth of the file in the tree, counting the root directory as depth 1. """ p = [] while file_id != None: try: ie = self._byid[file_id] except KeyError: bailout("file_id {%s} not found in inventory" % file_id) p.insert(0, ie.file_id) file_id = ie.parent_id return p def id2path(self, file_id): """Return as a list the path to file_id.""" # get all names, skipping root p = [self[fid].name for fid in self.get_idpath(file_id)[1:]] return os.sep.join(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. Returns None iff the path is not found. """ if isinstance(name, types.StringTypes): name = splitpath(name) mutter("lookup path %r" % name) parent = self.root for f in name: try: cie = parent.children[f] assert cie.name == f assert cie.parent_id == parent.file_id parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): return self._byid.has_key(file_id) def rename(self, file_id, new_parent_id, new_name): """Move a file within the inventory. This can change either the name, or the parent, or both. This does not move the working file.""" if not is_valid_name(new_name): bailout("not an acceptable filename: %r" % new_name) new_parent = self._byid[new_parent_id] if new_name in new_parent.children: bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id))) new_parent_idpath = self.get_idpath(new_parent_id) if file_id in new_parent_idpath: bailout("cannot move directory %r into a subdirectory of itself, %r" % (self.id2path(file_id), self.id2path(new_parent_id))) file_ie = self._byid[file_id] old_parent = self._byid[file_ie.parent_id] # TODO: Don't leave things messed up if this fails del old_parent.children[file_ie.name] new_parent.children[new_name] = file_ie file_ie.name = new_name file_ie.parent_id = new_parent_id _NAME_RE = re.compile(r'^[^/\\]+$') def is_valid_name(name): return bool(_NAME_RE.match(name)) commit refs/heads/master mark :545 committer Martin Pool 1116501113 +1000 data 26 - --forward option for log from :544 M 644 inline NEWS data 7678 bzr-0.0.5 NOT RELEASED YET CHANGES: * ``bzr`` with no command now shows help rather than giving an error. Suggested by Michael Ellerman. * ``bzr status`` output format changed, because svn-style output doesn't really match the model of bzr. Now files are grouped by status and can be shown with their IDs. ``bzr status --all`` shows all versioned files and unknown files but not ignored files. * ``bzr log`` runs from most-recent to least-recent, the reverse of the previous order. The previous behaviour can be obtained with the ``--forward`` option. ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New ``bzr ignore PATTERN`` command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. File may be in a branch other than the working directory. * ``bzr log`` and ``bzr root`` can be given an http URL instead of a filename. * Commands can now be defined by external programs or scripts in a directory on $BZRPATH. * New "stat cache" avoids reading the contents of files if they haven't changed since the previous time. * If the Python interpreter is too old, try to find a better one or give an error. Based on a patch from Fredrik Lundh. * New optional parameter ``bzr info [BRANCH]``. * New form ``bzr commit SELECTED`` to commit only selected files. BUG FIXES: * Fixed diff format so that added and removed files will be handled properly by patch. Fix from Lalo Martins. * Various fixes for files whose names contain spaces or other metacharacters. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. * testbzr requires python2.4, but can be used to test bzr running under a different version. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. * New developer commands ``added``, ``modified``. PORTABILITY: * Cope on Windows on python2.3 by using the weaker random seed. 2.4 is now only recommended. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 36093 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date from bzrlib import merge def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0], lock_mode='r') file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.', lock_mode='r') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.'), revision, specific_files=file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False): from bzrlib import show_log, find_branch direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename, lock_mode='r') fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.', lock_mode='r') file_id = None show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=sys.stdout, direction=direction) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures return 1 else: print return 0 class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): merge.merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'forward': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline testbzr data 10784 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. usage: testbzr [-p PYTHON] [BZR] By default this tests the copy of bzr found in the same directory as testbzr, or the first one found on the $PATH. A copy of bzr may be given on the command line to override this, for example when applying a new test suite to an old copy of bzr or vice versa. testbzr normally invokes bzr using the same version of python as it would normally use to run -- that is, the system default python, unless that is older than 2.3. The -p option allows specification of a different Python interpreter, such as when testing that bzr still works on python2.3. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" OVERRIDE_PYTHON = None LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = BZRPATH if OVERRIDE_PYTHON: cmd.insert(0, OVERRIDE_PYTHON) logfile.write('$ %r\n' % cmd) return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) start_dir = os.getcwd() logfile = open(LOGFILENAME, 'wt', buffering=1) try: from getopt import getopt opts, args = getopt(sys.argv[1:], 'p:') for option, value in opts: if option == '-p': OVERRIDE_PYTHON = value mypath = os.path.abspath(sys.argv[0]) print '%-30s %s' % ('running tests from', mypath) global BZRPATH if args: BZRPATH = args[1] else: BZRPATH = os.path.join(os.path.split(mypath)[0], 'bzr') print '%-30s %s' % ('against bzr', BZRPATH) print '%-30s %s' % ('in directory', os.getcwd()) print '%-30s %s' % ('with python', (OVERRIDE_PYTHON or '(default)')) print print backtick([BZRPATH, 'version']) runcmd(['mkdir', TESTDIR]) cd(TESTDIR) test_root = os.getcwd() progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("internal tests") runcmd("bzr selftest") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) runcmd("bzr diff --message foo", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == 'unknown:\n test.txt\n' out = backtick("bzr status --all") assert out == "unknown:\n test.txt\n" out = backtick("bzr status test.txt --all") assert out == "unknown:\n test.txt\n" f = file('test2.txt', 'wt') f.write('goodbye cruel world...\n') f.close() out = backtick("bzr status test.txt") assert out == "unknown:\n test.txt\n" out = backtick("bzr status") assert out == ("unknown:\n" " test.txt\n" " test2.txt\n") os.unlink('test2.txt') progress("command aliases") out = backtick("bzr st --all") assert out == ("unknown:\n" " test.txt\n") out = backtick("bzr stat") assert out == ("unknown:\n" " test.txt\n") progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == ("added:\n" " test.txt\n") progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == "sub2/hello.txt\n" assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == 'sub1/sub2/hello.txt\n' assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') assert backtick('bzr relpath sub2/hello.txt') == 'sub1/sub2/hello.txt\n' runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') runcmd('bzr log') runcmd('bzr log -v') progress("file with spaces in name") mkdir('sub directory') file('sub directory/file with spaces ', 'wt').write('see how this works\n') runcmd('bzr add .') runcmd('bzr diff') runcmd('bzr commit -m add-spaces') runcmd('bzr check') runcmd('bzr log') runcmd('bzr log --forward') runcmd('bzr info') cd('..') cd('..') progress('ignore patterns') mkdir('ignorebranch') cd('ignorebranch') runcmd('bzr init') assert backtick('bzr unknowns') == '' file('foo.tmp', 'wt').write('tmp files are ignored') assert backtick('bzr unknowns') == '' file('foo.c', 'wt').write('int main() {}') assert backtick('bzr unknowns') == 'foo.c\n' runcmd('bzr add foo.c') assert backtick('bzr unknowns') == '' # 'ignore' works when creating the .bzignore file file('foo.blah', 'wt').write('blah') assert backtick('bzr unknowns') == 'foo.blah\n' runcmd('bzr ignore *.blah') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\n' # 'ignore' works when then .bzrignore file already exists file('garh', 'wt').write('garh') assert backtick('bzr unknowns') == 'garh\n' runcmd('bzr ignore garh') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\ngarh\n' progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) logfile.close() sys.stdout.writelines(file(os.path.join(start_dir, LOGFILENAME), 'rt').readlines()[-50:]) sys.exit(1) commit refs/heads/master mark :546 committer Martin Pool 1116555148 +1000 data 29 - diff only needs a read lock from :545 M 644 inline bzrlib/commands.py data 36108 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date from bzrlib import merge def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0], lock_mode='r') file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.', lock_mode='r') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff show_diff(Branch('.', lock_mode='r'), revision, specific_files=file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False): from bzrlib import show_log, find_branch direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename, lock_mode='r') fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.', lock_mode='r') file_id = None show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=sys.stdout, direction=direction) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures return 1 else: print return 0 class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): merge.merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'forward': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :547 committer Martin Pool 1116555361 +1000 data 70 - bzr diff finds a branch from the first parameter, if any are given from :546 M 644 inline bzrlib/commands.py data 36431 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date from bzrlib import merge def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0], lock_mode='r') file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.', lock_mode='r') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.iter_entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0], lock_mode='r') file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.', lock_mode='r') show_diff(b, revision, specific_files=file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False): from bzrlib import show_log, find_branch direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename, lock_mode='r') fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.', lock_mode='r') file_id = None show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=sys.stdout, direction=direction) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures return 1 else: print return 0 class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): merge.merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'forward': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :548 committer Martin Pool 1116969268 +1200 data 109 - Write statcache using \u style encoding to avoid problems with quopri encoding causing line-wrapping etc. from :547 M 644 inline TODO data 11415 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * ``bzr log DIR`` should give changes to any files within DIR. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Atomic file class that renames into place when it's closed. * Don't abort if ``~/.bzr.log`` can't be used. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. * Maybe also store directories in the statcache so that we can quickly identify that they still exist. Medium things ------------- * Change command functions into Command() objects, like in hct, and then the grammar can be described directly in there. Since all option definitions are global we can define them just once and reference them from each command. * Merge Aaron's merge code. * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. We should be able to just get the id for the selected files, look up their location and diff just those files. No need to traverse the entire inventories. * ``bzr status DIR`` or ``bzr diff DIR`` should report on all changes under that directory. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. - Selected-file commit - Impossible selected-file commit: adding things in non-versioned directories, crossing renames, etc. * Write a reproducible benchmark, perhaps importing various kernel versions. * Change test.sh from Bourne shell into something in pure Python so that it can be more portable. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Should be a signature at the top of the cache file. * Paranoid mode where we never trust SHA-1 matches. * Don't commit if there are no changes unless forced. * --dry-run mode for commit? (Or maybe just run with check-command=false?) * Generally, be a bit more verbose unless --silent is specified. * Function that finds all changes to files under a given directory; perhaps log should use this if a directory is given. * XML attributes might have trouble with filenames containing \n and \r. Do we really want to support this? I think perhaps not. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. Possibly this should be done by splitting the commit function into several parts (under a single interface). It is already rather large. Decomposition: - find tree modifications and prepare in-memory inventory - export that inventory to a temporary directory - run the test in that temporary directory - if that succeeded, continue to actually finish the commit What should be done with the text of modified files while this is underway? I don't think we want to count on holding them in memory and we can't trust the working files to stay in one place so I suppose we need to move them into the text store, or otherwise into a temporary directory. If the commit does not actually complete, we would rather the content was not left behind in the stores. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` M 644 inline bzrlib/statcache.py data 8462 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import stat, os, sha, time from trace import mutter from errors import BzrError, BzrCheckError """File stat cache to speed up tree comparisons. This module basically gives a quick way to find the SHA-1 and related information of a file in the working directory, without actually reading and hashing the whole file. Implementation ============== Users of this module should not need to know about how this is implemented, and in particular should not depend on the particular data which is stored or its format. This is done by maintaining a cache indexed by a file fingerprint of (path, size, mtime, ctime, ino, dev) pointing to the SHA-1. If the fingerprint has changed, we assume the file content has not changed either and the SHA-1 is therefore the same. If any of the fingerprint fields have changed then the file content *may* have changed, or it may not have. We need to reread the file contents to make sure, but this is not visible to the user or higher-level code (except as a delay of course). The mtime and ctime are stored with nanosecond fields, but not all filesystems give this level of precision. There is therefore a possible race: the file might be modified twice within a second without changing the size or mtime, and a SHA-1 cached from the first version would be wrong. We handle this by not recording a cached hash for any files which were modified in the current second and that therefore have the chance to change again before the second is up. The only known hole in this design is if the system clock jumps backwards crossing invocations of bzr. Please don't do that; use ntp to gradually adjust your clock or don't use bzr over the step. At the moment this is stored in a simple textfile; it might be nice to use a tdb instead. The cache is represented as a map from file_id to a tuple of (file_id, sha1, path, size, mtime, ctime, ino, dev). The SHA-1 is stored in memory as a hexdigest. File names and file-ids are written out with non-ascii or whitespace characters given as python-style unicode escapes. (file-ids shouldn't contain wierd characters, but it might happen.) """ # order of fields returned by fingerprint() FP_SIZE = 0 FP_MTIME = 1 FP_CTIME = 2 FP_INO = 3 FP_DEV = 4 # order of fields in the statcache file and in the in-memory map SC_FILE_ID = 0 SC_SHA1 = 1 SC_PATH = 2 SC_SIZE = 3 SC_MTIME = 4 SC_CTIME = 5 SC_INO = 6 SC_DEV = 7 CACHE_HEADER = "### bzr statcache v3" def fingerprint(abspath): try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) def safe_quote(s): return s.encode('unicode_escape') \ .replace('\n', '\\u000a') \ .replace(' ', '\\u0020') \ .replace('\r', '\\u000d') def _write_cache(basedir, entry_iter, dangerfiles): from atomicfile import AtomicFile cachefn = os.path.join(basedir, '.bzr', 'stat-cache') outf = AtomicFile(cachefn, 'wb') outf.write(CACHE_HEADER + '\n') try: for entry in entry_iter: if len(entry) != 8: raise ValueError("invalid statcache entry tuple %r" % entry) if entry[SC_FILE_ID] in dangerfiles: continue # changed too recently outf.write(safe_quote(entry[0])) # file id outf.write(' ') outf.write(entry[1]) # hex sha1 outf.write(' ') outf.write(safe_quote(entry[2])) # name for nf in entry[3:]: outf.write(' %d' % nf) outf.write('\n') outf.commit() finally: if not outf.closed: outf.abort() def load_cache(basedir): import re cache = {} seen_paths = {} sha_re = re.compile(r'[a-f0-9]{40}') try: cachefn = os.path.join(basedir, '.bzr', 'stat-cache') cachefile = open(cachefn, 'rb') except IOError: return cache line1 = cachefile.readline().rstrip('\r\n') if line1 != CACHE_HEADER: mutter('cache header marker not found at top of %s' % cachefn) return cache for l in cachefile: f = l.split(' ') file_id = f[0].decode('unicode_escape') if file_id in cache: raise BzrCheckError("duplicated file_id in cache: {%s}" % file_id) text_sha = f[1] if len(text_sha) != 40 or not sha_re.match(text_sha): raise BzrCheckError("invalid file SHA-1 in cache: %r" % text_sha) path = f[2].decode('unicode_escape') if path in seen_paths: raise BzrCheckError("duplicated path in cache: %r" % path) seen_paths[path] = True entry = (file_id, text_sha, path) + tuple([long(x) for x in f[3:]]) if len(entry) != 8: raise ValueError("invalid statcache entry tuple %r" % entry) cache[file_id] = entry return cache def _files_from_inventory(inv): for path, ie in inv.iter_entries(): if ie.kind != 'file': continue yield ie.file_id, path def update_cache(basedir, inv, flush=False): """Update and return the cache for the branch. The returned cache may contain entries that have not been written to disk for files recently touched. flush -- discard any previous cache and recalculate from scratch. """ # TODO: It's supposed to be faster to stat the files in order by inum. # We don't directly know the inum of the files of course but we do # know where they were last sighted, so we can sort by that. assert isinstance(flush, bool) if flush: cache = {} else: cache = load_cache(basedir) return _update_cache_from_list(basedir, cache, _files_from_inventory(inv)) def _update_cache_from_list(basedir, cache, to_update): """Update and return the cache for given files. cache -- Previously cached values to be validated. to_update -- Sequence of (file_id, path) pairs to check. """ stat_cnt = missing_cnt = hardcheck = change_cnt = 0 # dangerfiles have been recently touched and can't be # committed to a persistent cache yet. dangerfiles = {} now = int(time.time()) ## mutter('update statcache under %r' % basedir) for file_id, path in to_update: abspath = os.path.join(basedir, path) fp = fingerprint(abspath) stat_cnt += 1 cacheentry = cache.get(file_id) if fp == None: # not here if cacheentry: del cache[file_id] change_cnt += 1 missing_cnt += 1 continue if (fp[FP_MTIME] >= now) or (fp[FP_CTIME] >= now): dangerfiles[file_id] = True if cacheentry and (cacheentry[3:] == fp): continue # all stat fields unchanged hardcheck += 1 dig = sha.new(file(abspath, 'rb').read()).hexdigest() # We update the cache even if the digest has not changed from # last time we looked, so that the fingerprint fields will # match in future. cacheentry = (file_id, dig, path) + fp cache[file_id] = cacheentry change_cnt += 1 mutter('statcache: statted %d files, read %d files, %d changed, %d dangerous, ' '%d in cache' % (stat_cnt, hardcheck, change_cnt, len(dangerfiles), len(cache))) if change_cnt: mutter('updating on-disk statcache') _write_cache(basedir, cache.itervalues(), dangerfiles) return cache commit refs/heads/master mark :549 committer Martin Pool 1116969482 +1200 data 3 doc from :548 M 644 inline bzrlib/statcache.py data 8499 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import stat, os, sha, time from trace import mutter from errors import BzrError, BzrCheckError """File stat cache to speed up tree comparisons. This module basically gives a quick way to find the SHA-1 and related information of a file in the working directory, without actually reading and hashing the whole file. Implementation ============== Users of this module should not need to know about how this is implemented, and in particular should not depend on the particular data which is stored or its format. This is done by maintaining a cache indexed by a file fingerprint of (path, size, mtime, ctime, ino, dev) pointing to the SHA-1. If the fingerprint has changed, we assume the file content has not changed either and the SHA-1 is therefore the same. If any of the fingerprint fields have changed then the file content *may* have changed, or it may not have. We need to reread the file contents to make sure, but this is not visible to the user or higher-level code (except as a delay of course). The mtime and ctime are stored with nanosecond fields, but not all filesystems give this level of precision. There is therefore a possible race: the file might be modified twice within a second without changing the size or mtime, and a SHA-1 cached from the first version would be wrong. We handle this by not recording a cached hash for any files which were modified in the current second and that therefore have the chance to change again before the second is up. The only known hole in this design is if the system clock jumps backwards crossing invocations of bzr. Please don't do that; use ntp to gradually adjust your clock or don't use bzr over the step. At the moment this is stored in a simple textfile; it might be nice to use a tdb instead. The cache is represented as a map from file_id to a tuple of (file_id, sha1, path, size, mtime, ctime, ino, dev). The SHA-1 is stored in memory as a hexdigest. File names and file-ids are written out with non-ascii or whitespace characters given as python-style unicode escapes. (file-ids shouldn't contain wierd characters, but it might happen.) """ # order of fields returned by fingerprint() FP_SIZE = 0 FP_MTIME = 1 FP_CTIME = 2 FP_INO = 3 FP_DEV = 4 # order of fields in the statcache file and in the in-memory map SC_FILE_ID = 0 SC_SHA1 = 1 SC_PATH = 2 SC_SIZE = 3 SC_MTIME = 4 SC_CTIME = 5 SC_INO = 6 SC_DEV = 7 CACHE_HEADER = "### bzr statcache v3" def fingerprint(abspath): try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) def safe_quote(s): return s.encode('unicode_escape') \ .replace('\n', '\\u000a') \ .replace(' ', '\\u0020') \ .replace('\r', '\\u000d') def _write_cache(basedir, entry_iter, dangerfiles): from atomicfile import AtomicFile cachefn = os.path.join(basedir, '.bzr', 'stat-cache') outf = AtomicFile(cachefn, 'wb') outf.write(CACHE_HEADER + '\n') try: for entry in entry_iter: if len(entry) != 8: raise ValueError("invalid statcache entry tuple %r" % entry) if entry[SC_FILE_ID] in dangerfiles: continue # changed too recently outf.write(safe_quote(entry[0])) # file id outf.write(' ') outf.write(entry[1]) # hex sha1 outf.write(' ') outf.write(safe_quote(entry[2])) # name for nf in entry[3:]: outf.write(' %d' % nf) outf.write('\n') outf.commit() finally: if not outf.closed: outf.abort() def load_cache(basedir): import re cache = {} seen_paths = {} sha_re = re.compile(r'[a-f0-9]{40}') try: cachefn = os.path.join(basedir, '.bzr', 'stat-cache') cachefile = open(cachefn, 'rb') except IOError: return cache line1 = cachefile.readline().rstrip('\r\n') if line1 != CACHE_HEADER: mutter('cache header marker not found at top of %s' % cachefn) return cache for l in cachefile: f = l.split(' ') file_id = f[0].decode('unicode_escape') if file_id in cache: raise BzrCheckError("duplicated file_id in cache: {%s}" % file_id) text_sha = f[1] if len(text_sha) != 40 or not sha_re.match(text_sha): raise BzrCheckError("invalid file SHA-1 in cache: %r" % text_sha) path = f[2].decode('unicode_escape') if path in seen_paths: raise BzrCheckError("duplicated path in cache: %r" % path) seen_paths[path] = True entry = (file_id, text_sha, path) + tuple([long(x) for x in f[3:]]) if len(entry) != 8: raise ValueError("invalid statcache entry tuple %r" % entry) cache[file_id] = entry return cache def _files_from_inventory(inv): for path, ie in inv.iter_entries(): if ie.kind != 'file': continue yield ie.file_id, path def update_cache(basedir, inv, flush=False): """Update and return the cache for the branch. The returned cache may contain entries that have not been written to disk for files recently touched. flush -- discard any previous cache and recalculate from scratch. """ # TODO: It's supposed to be faster to stat the files in order by inum. # We don't directly know the inum of the files of course but we do # know where they were last sighted, so we can sort by that. assert isinstance(flush, bool) if flush: cache = {} else: cache = load_cache(basedir) return _update_cache_from_list(basedir, cache, _files_from_inventory(inv)) def _update_cache_from_list(basedir, cache, to_update): """Update and return the cache for given files. cache -- Previously cached values to be validated. to_update -- Sequence of (file_id, path) pairs to check. """ stat_cnt = missing_cnt = hardcheck = change_cnt = 0 # dangerfiles have been recently touched and can't be committed to # a persistent cache yet, but they are returned to the caller. dangerfiles = {} now = int(time.time()) ## mutter('update statcache under %r' % basedir) for file_id, path in to_update: abspath = os.path.join(basedir, path) fp = fingerprint(abspath) stat_cnt += 1 cacheentry = cache.get(file_id) if fp == None: # not here if cacheentry: del cache[file_id] change_cnt += 1 missing_cnt += 1 continue if (fp[FP_MTIME] >= now) or (fp[FP_CTIME] >= now): dangerfiles[file_id] = True if cacheentry and (cacheentry[3:] == fp): continue # all stat fields unchanged hardcheck += 1 dig = sha.new(file(abspath, 'rb').read()).hexdigest() # We update the cache even if the digest has not changed from # last time we looked, so that the fingerprint fields will # match in future. cacheentry = (file_id, dig, path) + fp cache[file_id] = cacheentry change_cnt += 1 mutter('statcache: statted %d files, read %d files, %d changed, %d dangerous, ' '%d in cache' % (stat_cnt, hardcheck, change_cnt, len(dangerfiles), len(cache))) if change_cnt: mutter('updating on-disk statcache') _write_cache(basedir, cache.itervalues(), dangerfiles) return cache commit refs/heads/master mark :550 committer Martin Pool 1116982102 +1200 data 66 - Refactor diff code into one that works purely on Tree objects from :549 M 644 inline bzrlib/diff.py data 10245 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from trace import mutter from errors import BzrError def _diff_one(oldlines, newlines, to_file, **kw): import difflib # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') to_file.writelines(ud) if nonl: print >>to_file, "\\ No newline at end of file" print >>to_file def show_diff(b, revision, specific_files): import sys if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() show_diff_trees(old_tree, new_tree, sys.stdout, specific_files) def show_diff_trees(old_tree, new_tree, to_file, specific_files=None): """Show in text form the changes from one tree to another. to_files If set, include only changes to these files. """ # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. delta = compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=specific_files) for path, file_id, kind in delta.removed: print '*** removed %s %r' % (kind, path) if kind == 'file': _diff_one(old_tree.get_file(file_id).readlines(), [], to_file, fromfile=old_label + path, tofile=DEVNULL) for path, file_id, kind in delta.added: print '*** added %s %r' % (kind, path) if kind == 'file': _diff_one([], new_tree.get_file(file_id).readlines(), to_file, fromfile=DEVNULL, tofile=new_label + path) for old_path, new_path, file_id, kind, text_modified in delta.renamed: print '*** renamed %s %r => %r' % (kind, old_path, new_path) if text_modified: _diff_one(old_tree.get_file(file_id).readlines(), new_tree.get_file(file_id).readlines(), to_file, fromfile=old_label + old_path, tofile=new_label + new_path) for path, file_id, kind in delta.modified: print '*** modified %s %r' % (kind, path) if kind == 'file': _diff_one(old_tree.get_file(file_id).readlines(), new_tree.get_file(file_id).readlines(), to_file, fromfile=old_label + path, tofile=new_label + path) class TreeDelta: """Describes changes from one tree to another. Contains four lists: added (path, id, kind) removed (path, id, kind) renamed (oldpath, newpath, id, kind, text_modified) modified (path, id, kind) unchanged (path, id, kind) Each id is listed only once. Files that are both modified and renamed are listed only in renamed, with the text_modified flag true. The lists are normally sorted when the delta is created. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] self.unchanged = [] def touches_file_id(self, file_id): """Return True if file_id is modified by this delta.""" for l in self.added, self.removed, self.modified: for v in l: if v[1] == file_id: return True for v in self.renamed: if v[2] == file_id: return True return False def show(self, to_file, show_ids=False, show_unchanged=False): def show_list(files): for path, fid, kind in files: if kind == 'directory': path += '/' elif kind == 'symlink': path += '@' if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if self.removed: print >>to_file, 'removed:' show_list(self.removed) if self.added: print >>to_file, 'added:' show_list(self.added) if self.renamed: print >>to_file, 'renamed:' for oldpath, newpath, fid, kind, text_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if self.modified: print >>to_file, 'modified:' show_list(self.modified) if show_unchanged and self.unchanged: print >>to_file, 'unchanged:' show_list(self.unchanged) def compare_trees(old_tree, new_tree, want_unchanged, specific_files=None): """Describe changes from one tree to another. Returns a TreeDelta with details of added, modified, renamed, and deleted entries. The root entry is specifically exempt. This only considers versioned files. want_unchanged If true, also list files unchanged from one version to the next. specific_files If true, only check for changes to specified names or files within them. """ from osutils import is_inside_any old_inv = old_tree.inventory new_inv = new_tree.inventory delta = TreeDelta() mutter('start compare_trees') # TODO: match for specific files can be rather smarter by finding # the IDs of those files up front and then considering only that. for file_id in old_tree: if file_id in new_tree: kind = old_inv.get_file_kind(file_id) assert kind == new_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ 'invalid file kind %r' % kind if kind == 'root_directory': continue old_path = old_inv.id2path(file_id) new_path = new_inv.id2path(file_id) if specific_files: if (not is_inside_any(specific_files, old_path) and not is_inside_any(specific_files, new_path)): continue if kind == 'file': old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False # TODO: Can possibly avoid calculating path strings if the # two files are unchanged and their names and parents are # the same and the parents are unchanged all the way up. # May not be worthwhile. if old_path != new_path: delta.renamed.append((old_path, new_path, file_id, kind, text_modified)) elif text_modified: delta.modified.append((new_path, file_id, kind)) elif want_unchanged: delta.unchanged.append((new_path, file_id, kind)) else: old_path = old_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, old_path): continue delta.removed.append((old_path, file_id, kind)) mutter('start looking for new files') for file_id in new_inv: if file_id in old_inv: continue new_path = new_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, new_path): continue kind = new_inv.get_file_kind(file_id) delta.added.append((new_path, file_id, kind)) delta.removed.sort() delta.added.sort() delta.renamed.sort() delta.modified.sort() delta.unchanged.sort() return delta commit refs/heads/master mark :551 committer Martin Pool 1116989884 +1000 data 46 - Don't fail if unable to update the statcache from :550 M 644 inline bzrlib/statcache.py data 8825 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import stat, os, sha, time from trace import mutter from errors import BzrError, BzrCheckError """File stat cache to speed up tree comparisons. This module basically gives a quick way to find the SHA-1 and related information of a file in the working directory, without actually reading and hashing the whole file. Implementation ============== Users of this module should not need to know about how this is implemented, and in particular should not depend on the particular data which is stored or its format. This is done by maintaining a cache indexed by a file fingerprint of (path, size, mtime, ctime, ino, dev) pointing to the SHA-1. If the fingerprint has changed, we assume the file content has not changed either and the SHA-1 is therefore the same. If any of the fingerprint fields have changed then the file content *may* have changed, or it may not have. We need to reread the file contents to make sure, but this is not visible to the user or higher-level code (except as a delay of course). The mtime and ctime are stored with nanosecond fields, but not all filesystems give this level of precision. There is therefore a possible race: the file might be modified twice within a second without changing the size or mtime, and a SHA-1 cached from the first version would be wrong. We handle this by not recording a cached hash for any files which were modified in the current second and that therefore have the chance to change again before the second is up. The only known hole in this design is if the system clock jumps backwards crossing invocations of bzr. Please don't do that; use ntp to gradually adjust your clock or don't use bzr over the step. At the moment this is stored in a simple textfile; it might be nice to use a tdb instead. The cache is represented as a map from file_id to a tuple of (file_id, sha1, path, size, mtime, ctime, ino, dev). The SHA-1 is stored in memory as a hexdigest. File names and file-ids are written out with non-ascii or whitespace characters given as python-style unicode escapes. (file-ids shouldn't contain wierd characters, but it might happen.) """ # order of fields returned by fingerprint() FP_SIZE = 0 FP_MTIME = 1 FP_CTIME = 2 FP_INO = 3 FP_DEV = 4 # order of fields in the statcache file and in the in-memory map SC_FILE_ID = 0 SC_SHA1 = 1 SC_PATH = 2 SC_SIZE = 3 SC_MTIME = 4 SC_CTIME = 5 SC_INO = 6 SC_DEV = 7 CACHE_HEADER = "### bzr statcache v3" def fingerprint(abspath): try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) def safe_quote(s): return s.encode('unicode_escape') \ .replace('\n', '\\u000a') \ .replace(' ', '\\u0020') \ .replace('\r', '\\u000d') def _write_cache(basedir, entry_iter, dangerfiles): from atomicfile import AtomicFile cachefn = os.path.join(basedir, '.bzr', 'stat-cache') outf = AtomicFile(cachefn, 'wb') outf.write(CACHE_HEADER + '\n') try: for entry in entry_iter: if len(entry) != 8: raise ValueError("invalid statcache entry tuple %r" % entry) if entry[SC_FILE_ID] in dangerfiles: continue # changed too recently outf.write(safe_quote(entry[0])) # file id outf.write(' ') outf.write(entry[1]) # hex sha1 outf.write(' ') outf.write(safe_quote(entry[2])) # name for nf in entry[3:]: outf.write(' %d' % nf) outf.write('\n') outf.commit() finally: if not outf.closed: outf.abort() def _write_cache_maybe(basedir, entry_iter, dangerfiles): try: return _write_cache(basedir, entry_iter, dangerfiles) except IOError, e: mutter("cannot update statcache in %s: %s" % (basedir, e)) except OSError, e: mutter("cannot update statcache in %s: %s" % (basedir, e)) def load_cache(basedir): import re cache = {} seen_paths = {} sha_re = re.compile(r'[a-f0-9]{40}') try: cachefn = os.path.join(basedir, '.bzr', 'stat-cache') cachefile = open(cachefn, 'rb') except IOError: return cache line1 = cachefile.readline().rstrip('\r\n') if line1 != CACHE_HEADER: mutter('cache header marker not found at top of %s' % cachefn) return cache for l in cachefile: f = l.split(' ') file_id = f[0].decode('unicode_escape') if file_id in cache: raise BzrCheckError("duplicated file_id in cache: {%s}" % file_id) text_sha = f[1] if len(text_sha) != 40 or not sha_re.match(text_sha): raise BzrCheckError("invalid file SHA-1 in cache: %r" % text_sha) path = f[2].decode('unicode_escape') if path in seen_paths: raise BzrCheckError("duplicated path in cache: %r" % path) seen_paths[path] = True entry = (file_id, text_sha, path) + tuple([long(x) for x in f[3:]]) if len(entry) != 8: raise ValueError("invalid statcache entry tuple %r" % entry) cache[file_id] = entry return cache def _files_from_inventory(inv): for path, ie in inv.iter_entries(): if ie.kind != 'file': continue yield ie.file_id, path def update_cache(basedir, inv, flush=False): """Update and return the cache for the branch. The returned cache may contain entries that have not been written to disk for files recently touched. flush -- discard any previous cache and recalculate from scratch. """ # TODO: It's supposed to be faster to stat the files in order by inum. # We don't directly know the inum of the files of course but we do # know where they were last sighted, so we can sort by that. assert isinstance(flush, bool) if flush: cache = {} else: cache = load_cache(basedir) return _update_cache_from_list(basedir, cache, _files_from_inventory(inv)) def _update_cache_from_list(basedir, cache, to_update): """Update and return the cache for given files. cache -- Previously cached values to be validated. to_update -- Sequence of (file_id, path) pairs to check. """ stat_cnt = missing_cnt = hardcheck = change_cnt = 0 # dangerfiles have been recently touched and can't be committed to # a persistent cache yet, but they are returned to the caller. dangerfiles = {} now = int(time.time()) ## mutter('update statcache under %r' % basedir) for file_id, path in to_update: abspath = os.path.join(basedir, path) fp = fingerprint(abspath) stat_cnt += 1 cacheentry = cache.get(file_id) if fp == None: # not here if cacheentry: del cache[file_id] change_cnt += 1 missing_cnt += 1 continue if (fp[FP_MTIME] >= now) or (fp[FP_CTIME] >= now): dangerfiles[file_id] = True if cacheentry and (cacheentry[3:] == fp): continue # all stat fields unchanged hardcheck += 1 dig = sha.new(file(abspath, 'rb').read()).hexdigest() # We update the cache even if the digest has not changed from # last time we looked, so that the fingerprint fields will # match in future. cacheentry = (file_id, dig, path) + fp cache[file_id] = cacheentry change_cnt += 1 mutter('statcache: statted %d files, read %d files, %d changed, %d dangerous, ' '%d in cache' % (stat_cnt, hardcheck, change_cnt, len(dangerfiles), len(cache))) if change_cnt: mutter('updating on-disk statcache') _write_cache_maybe(basedir, cache.itervalues(), dangerfiles) return cache commit refs/heads/master mark :552 committer Martin Pool 1116990287 +1000 data 18 - update todo list from :551 M 644 inline TODO data 11005 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * ``bzr log DIR`` should give changes to any files within DIR. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. * Maybe also store directories in the statcache so that we can quickly identify that they still exist. Medium things ------------- * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. We should be able to just get the id for the selected files, look up their location and diff just those files. No need to traverse the entire inventories. * ``bzr status DIR`` or ``bzr diff DIR`` should report on all changes under that directory. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from ElementTree to an object when it is read in, but rather wait until the program actually wants to know about that node. * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. - Selected-file commit - Impossible selected-file commit: adding things in non-versioned directories, crossing renames, etc. * Write a reproducible benchmark, perhaps importing various kernel versions. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Paranoid mode where we never trust SHA-1 matches. * Don't commit if there are no changes unless forced. * --dry-run mode for commit? (Or maybe just run with check-command=false?) * Generally, be a bit more verbose unless --silent is specified. * Function that finds all changes to files under a given directory; perhaps log should use this if a directory is given. * XML attributes might have trouble with filenames containing \n and \r. Do we really want to support this? I think perhaps not. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. Possibly this should be done by splitting the commit function into several parts (under a single interface). It is already rather large. Decomposition: - find tree modifications and prepare in-memory inventory - export that inventory to a temporary directory - run the test in that temporary directory - if that succeeded, continue to actually finish the commit What should be done with the text of modified files while this is underway? I don't think we want to count on holding them in memory and we can't trust the working files to stay in one place so I suppose we need to move them into the text store, or otherwise into a temporary directory. If the commit does not actually complete, we would rather the content was not left behind in the stores. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :553 committer Martin Pool 1116990689 +1000 data 57 - refactor handling of dangerous files (changed recently) from :552 M 644 inline bzrlib/statcache.py data 8834 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import stat, os, sha, time from trace import mutter from errors import BzrError, BzrCheckError """File stat cache to speed up tree comparisons. This module basically gives a quick way to find the SHA-1 and related information of a file in the working directory, without actually reading and hashing the whole file. Implementation ============== Users of this module should not need to know about how this is implemented, and in particular should not depend on the particular data which is stored or its format. This is done by maintaining a cache indexed by a file fingerprint of (path, size, mtime, ctime, ino, dev) pointing to the SHA-1. If the fingerprint has changed, we assume the file content has not changed either and the SHA-1 is therefore the same. If any of the fingerprint fields have changed then the file content *may* have changed, or it may not have. We need to reread the file contents to make sure, but this is not visible to the user or higher-level code (except as a delay of course). The mtime and ctime are stored with nanosecond fields, but not all filesystems give this level of precision. There is therefore a possible race: the file might be modified twice within a second without changing the size or mtime, and a SHA-1 cached from the first version would be wrong. We handle this by not recording a cached hash for any files which were modified in the current second and that therefore have the chance to change again before the second is up. The only known hole in this design is if the system clock jumps backwards crossing invocations of bzr. Please don't do that; use ntp to gradually adjust your clock or don't use bzr over the step. At the moment this is stored in a simple textfile; it might be nice to use a tdb instead. The cache is represented as a map from file_id to a tuple of (file_id, sha1, path, size, mtime, ctime, ino, dev). The SHA-1 is stored in memory as a hexdigest. File names and file-ids are written out with non-ascii or whitespace characters given as python-style unicode escapes. (file-ids shouldn't contain wierd characters, but it might happen.) """ # order of fields returned by fingerprint() FP_SIZE = 0 FP_MTIME = 1 FP_CTIME = 2 FP_INO = 3 FP_DEV = 4 # order of fields in the statcache file and in the in-memory map SC_FILE_ID = 0 SC_SHA1 = 1 SC_PATH = 2 SC_SIZE = 3 SC_MTIME = 4 SC_CTIME = 5 SC_INO = 6 SC_DEV = 7 CACHE_HEADER = "### bzr statcache v3" def fingerprint(abspath): try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) def safe_quote(s): return s.encode('unicode_escape') \ .replace('\n', '\\u000a') \ .replace(' ', '\\u0020') \ .replace('\r', '\\u000d') def _write_cache(basedir, entries): from atomicfile import AtomicFile cachefn = os.path.join(basedir, '.bzr', 'stat-cache') outf = AtomicFile(cachefn, 'wb') outf.write(CACHE_HEADER + '\n') try: for entry in entries: if len(entry) != 8: raise ValueError("invalid statcache entry tuple %r" % entry) outf.write(safe_quote(entry[0])) # file id outf.write(' ') outf.write(entry[1]) # hex sha1 outf.write(' ') outf.write(safe_quote(entry[2])) # name for nf in entry[3:]: outf.write(' %d' % nf) outf.write('\n') outf.commit() finally: if not outf.closed: outf.abort() def _try_write_cache(basedir, entries): try: return _write_cache(basedir, entries) except IOError, e: mutter("cannot update statcache in %s: %s" % (basedir, e)) except OSError, e: mutter("cannot update statcache in %s: %s" % (basedir, e)) def load_cache(basedir): import re cache = {} seen_paths = {} sha_re = re.compile(r'[a-f0-9]{40}') try: cachefn = os.path.join(basedir, '.bzr', 'stat-cache') cachefile = open(cachefn, 'rb') except IOError: return cache line1 = cachefile.readline().rstrip('\r\n') if line1 != CACHE_HEADER: mutter('cache header marker not found at top of %s' % cachefn) return cache for l in cachefile: f = l.split(' ') file_id = f[0].decode('unicode_escape') if file_id in cache: raise BzrCheckError("duplicated file_id in cache: {%s}" % file_id) text_sha = f[1] if len(text_sha) != 40 or not sha_re.match(text_sha): raise BzrCheckError("invalid file SHA-1 in cache: %r" % text_sha) path = f[2].decode('unicode_escape') if path in seen_paths: raise BzrCheckError("duplicated path in cache: %r" % path) seen_paths[path] = True entry = (file_id, text_sha, path) + tuple([long(x) for x in f[3:]]) if len(entry) != 8: raise ValueError("invalid statcache entry tuple %r" % entry) cache[file_id] = entry return cache def _files_from_inventory(inv): for path, ie in inv.iter_entries(): if ie.kind != 'file': continue yield ie.file_id, path def update_cache(basedir, inv, flush=False): """Update and return the cache for the branch. The returned cache may contain entries that have not been written to disk for files recently touched. flush -- discard any previous cache and recalculate from scratch. """ # TODO: It's supposed to be faster to stat the files in order by inum. # We don't directly know the inum of the files of course but we do # know where they were last sighted, so we can sort by that. assert isinstance(flush, bool) if flush: cache = {} else: cache = load_cache(basedir) return _update_cache_from_list(basedir, cache, _files_from_inventory(inv)) def _update_cache_from_list(basedir, cache, to_update): """Update and return the cache for given files. cache -- Previously cached values to be validated. to_update -- Sequence of (file_id, path) pairs to check. """ stat_cnt = missing_cnt = hardcheck = change_cnt = 0 # dangerfiles have been recently touched and can't be committed to # a persistent cache yet, but they are returned to the caller. dangerfiles = [] now = int(time.time()) ## mutter('update statcache under %r' % basedir) for file_id, path in to_update: abspath = os.path.join(basedir, path) fp = fingerprint(abspath) stat_cnt += 1 cacheentry = cache.get(file_id) if fp == None: # not here if cacheentry: del cache[file_id] change_cnt += 1 missing_cnt += 1 continue if (fp[FP_MTIME] >= now) or (fp[FP_CTIME] >= now): dangerfiles.append(file_id) if cacheentry and (cacheentry[3:] == fp): continue # all stat fields unchanged hardcheck += 1 dig = sha.new(file(abspath, 'rb').read()).hexdigest() # We update the cache even if the digest has not changed from # last time we looked, so that the fingerprint fields will # match in future. cacheentry = (file_id, dig, path) + fp cache[file_id] = cacheentry change_cnt += 1 mutter('statcache: statted %d files, read %d files, %d changed, %d dangerous, ' '%d in cache' % (stat_cnt, hardcheck, change_cnt, len(dangerfiles), len(cache))) if change_cnt: mutter('updating on-disk statcache') if dangerfiles: safe_cache = cache.copy() for file_id in dangerfiles: del safe_cache[file_id] else: safe_cache = cache _try_write_cache(basedir, safe_cache.itervalues()) return cache commit refs/heads/master mark :554 committer Martin Pool 1116991622 +1000 data 87 - clean up statcache code - stat files in order by inum - report on added/deleted files from :553 M 644 inline bzrlib/statcache.py data 9019 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import stat, os, sha, time from trace import mutter from errors import BzrError, BzrCheckError """File stat cache to speed up tree comparisons. This module basically gives a quick way to find the SHA-1 and related information of a file in the working directory, without actually reading and hashing the whole file. Implementation ============== Users of this module should not need to know about how this is implemented, and in particular should not depend on the particular data which is stored or its format. This is done by maintaining a cache indexed by a file fingerprint of (path, size, mtime, ctime, ino, dev) pointing to the SHA-1. If the fingerprint has changed, we assume the file content has not changed either and the SHA-1 is therefore the same. If any of the fingerprint fields have changed then the file content *may* have changed, or it may not have. We need to reread the file contents to make sure, but this is not visible to the user or higher-level code (except as a delay of course). The mtime and ctime are stored with nanosecond fields, but not all filesystems give this level of precision. There is therefore a possible race: the file might be modified twice within a second without changing the size or mtime, and a SHA-1 cached from the first version would be wrong. We handle this by not recording a cached hash for any files which were modified in the current second and that therefore have the chance to change again before the second is up. The only known hole in this design is if the system clock jumps backwards crossing invocations of bzr. Please don't do that; use ntp to gradually adjust your clock or don't use bzr over the step. At the moment this is stored in a simple textfile; it might be nice to use a tdb instead. The cache is represented as a map from file_id to a tuple of (file_id, sha1, path, size, mtime, ctime, ino, dev). The SHA-1 is stored in memory as a hexdigest. File names and file-ids are written out with non-ascii or whitespace characters given as python-style unicode escapes. (file-ids shouldn't contain wierd characters, but it might happen.) """ # order of fields returned by fingerprint() FP_SIZE = 0 FP_MTIME = 1 FP_CTIME = 2 FP_INO = 3 FP_DEV = 4 # order of fields in the statcache file and in the in-memory map SC_FILE_ID = 0 SC_SHA1 = 1 SC_PATH = 2 SC_SIZE = 3 SC_MTIME = 4 SC_CTIME = 5 SC_INO = 6 SC_DEV = 7 CACHE_HEADER = "### bzr statcache v3" def fingerprint(abspath): try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) def safe_quote(s): return s.encode('unicode_escape') \ .replace('\n', '\\u000a') \ .replace(' ', '\\u0020') \ .replace('\r', '\\u000d') def _write_cache(basedir, entries): from atomicfile import AtomicFile cachefn = os.path.join(basedir, '.bzr', 'stat-cache') outf = AtomicFile(cachefn, 'wb') outf.write(CACHE_HEADER + '\n') try: for entry in entries: if len(entry) != 8: raise ValueError("invalid statcache entry tuple %r" % entry) outf.write(safe_quote(entry[0])) # file id outf.write(' ') outf.write(entry[1]) # hex sha1 outf.write(' ') outf.write(safe_quote(entry[2])) # name for nf in entry[3:]: outf.write(' %d' % nf) outf.write('\n') outf.commit() finally: if not outf.closed: outf.abort() def _try_write_cache(basedir, entries): try: return _write_cache(basedir, entries) except IOError, e: mutter("cannot update statcache in %s: %s" % (basedir, e)) except OSError, e: mutter("cannot update statcache in %s: %s" % (basedir, e)) def load_cache(basedir): import re cache = {} seen_paths = {} sha_re = re.compile(r'[a-f0-9]{40}') try: cachefn = os.path.join(basedir, '.bzr', 'stat-cache') cachefile = open(cachefn, 'rb') except IOError: return cache line1 = cachefile.readline().rstrip('\r\n') if line1 != CACHE_HEADER: mutter('cache header marker not found at top of %s' % cachefn) return cache for l in cachefile: f = l.split(' ') file_id = f[0].decode('unicode_escape') if file_id in cache: raise BzrCheckError("duplicated file_id in cache: {%s}" % file_id) text_sha = f[1] if len(text_sha) != 40 or not sha_re.match(text_sha): raise BzrCheckError("invalid file SHA-1 in cache: %r" % text_sha) path = f[2].decode('unicode_escape') if path in seen_paths: raise BzrCheckError("duplicated path in cache: %r" % path) seen_paths[path] = True entry = (file_id, text_sha, path) + tuple([long(x) for x in f[3:]]) if len(entry) != 8: raise ValueError("invalid statcache entry tuple %r" % entry) cache[file_id] = entry return cache def _files_from_inventory(inv): for path, ie in inv.iter_entries(): if ie.kind != 'file': continue yield ie.file_id, path def update_cache(basedir, inv, flush=False): """Update and return the cache for the branch. The returned cache may contain entries that have not been written to disk for files recently touched. flush -- discard any previous cache and recalculate from scratch. """ # load the existing cache; use information there to find a list of # files ordered by inode, which is alleged to be the fastest order # to stat the files. to_update = _files_from_inventory(inv) assert isinstance(flush, bool) if flush: cache = {} else: cache = load_cache(basedir) by_inode = [] without_inode = [] for file_id, path in to_update: if file_id in cache: by_inode.append((cache[file_id][SC_INO], file_id, path)) else: without_inode.append((file_id, path)) by_inode.sort() to_update = [a[1:] for a in by_inode] + without_inode stat_cnt = missing_cnt = new_cnt = hardcheck = change_cnt = 0 # dangerfiles have been recently touched and can't be committed to # a persistent cache yet, but they are returned to the caller. dangerfiles = [] now = int(time.time()) ## mutter('update statcache under %r' % basedir) for file_id, path in to_update: abspath = os.path.join(basedir, path) fp = fingerprint(abspath) stat_cnt += 1 cacheentry = cache.get(file_id) if fp == None: # not here if cacheentry: del cache[file_id] change_cnt += 1 missing_cnt += 1 continue elif not cacheentry: new_cnt += 1 if (fp[FP_MTIME] >= now) or (fp[FP_CTIME] >= now): dangerfiles.append(file_id) if cacheentry and (cacheentry[3:] == fp): continue # all stat fields unchanged hardcheck += 1 dig = sha.new(file(abspath, 'rb').read()).hexdigest() # We update the cache even if the digest has not changed from # last time we looked, so that the fingerprint fields will # match in future. cacheentry = (file_id, dig, path) + fp cache[file_id] = cacheentry change_cnt += 1 mutter('statcache: statted %d files, read %d files, %d changed, %d dangerous, ' '%d deleted, %d new, ' '%d in cache' % (stat_cnt, hardcheck, change_cnt, len(dangerfiles), missing_cnt, new_cnt, len(cache))) if change_cnt: mutter('updating on-disk statcache') if dangerfiles: safe_cache = cache.copy() for file_id in dangerfiles: del safe_cache[file_id] else: safe_cache = cache _try_write_cache(basedir, safe_cache.itervalues()) return cache commit refs/heads/master mark :555 committer Martin Pool 1116992889 +1000 data 32 - New Inventory.entries() method from :554 M 644 inline bzrlib/inventory.py data 19346 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # This should really be an id randomly assigned when the tree is # created, but it's not for now. ROOT_ID = "TREE_ROOT" import sys, os.path, types, re try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from xml import XMLMixin from errors import bailout, BzrError, BzrCheckError import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') 'TREE_ROOT' >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID)) >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123')) >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' TODO: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ # TODO: split InventoryEntry into subclasses for files, # directories, etc etc. text_sha1 = None text_size = None def __init__(self, file_id, name, kind, parent_id, text_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID) >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID) Traceback (most recent call last): BzrCheckError: InventoryEntry name 'src/hello.c' is invalid """ if '/' in name or '\\' in name: raise BzrCheckError('InventoryEntry name %r is invalid' % name) self.file_id = file_id self.name = name self.kind = kind self.text_id = text_id self.parent_id = parent_id if kind == 'directory': self.children = {} elif kind == 'file': pass else: raise BzrError("unhandled entry kind %r" % kind) def sorted_children(self): l = self.children.items() l.sort() return l def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.parent_id, text_id=self.text_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size # note that children are *not* copied; they're pulled across when # others are added return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size != None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1']: v = getattr(self, f) if v != None: e.set(f, v) # to be conservative, we don't externalize the root pointers # for now, leaving them as null in the xml form. in a future # version it will be implied by nested elements. if self.parent_id != ROOT_ID: assert isinstance(self.parent_id, basestring) e.set('parent_id', self.parent_id) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' ## original format inventories don't have a parent_id for ## nodes in the root directory, but it's cleaner to use one ## internally. parent_id = elt.get('parent_id') if parent_id == None: parent_id = ROOT_ID self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __eq__(self, other): if not isinstance(other, InventoryEntry): return NotImplemented return (self.file_id == other.file_id) \ and (self.name == other.name) \ and (self.text_sha1 == other.text_sha1) \ and (self.text_size == other.text_size) \ and (self.text_id == other.text_id) \ and (self.parent_id == other.parent_id) \ and (self.kind == other.kind) def __ne__(self, other): return not (self == other) def __hash__(self): raise ValueError('not hashable') class RootEntry(InventoryEntry): def __init__(self, file_id): self.file_id = file_id self.children = {} self.kind = 'root_directory' self.parent_id = None self.name = '' def __eq__(self, other): if not isinstance(other, RootEntry): return NotImplemented return (self.file_id == other.file_id) \ and (self.children == other.children) class Inventory(XMLMixin): """Inventory of versioned files in a tree. This describes which file_id is present at each point in the tree, and possibly the SHA-1 or other information about the file. Entries can be looked up either by path or by file_id. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted, other than through the Inventory API. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID)) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. The inventory is created with a default root directory, with an id of None. """ self.root = RootEntry(ROOT_ID) self._byid = {self.root.file_id: self.root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, from_dir=None): """Return (path, entry) pairs, in order by name.""" if from_dir == None: assert self.root from_dir = self.root elif isinstance(from_dir, basestring): from_dir = self._byid[from_dir] kids = from_dir.children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(from_dir=ie.file_id): yield os.path.join(name, cn), cie def entries(self): """Return list of (path, ie) for all entries except the root. This may be faster than iter_entries. """ def accum(self, dir_ie, dir_path, a): kids = from_dir.children.items() kids.sort() for name, ie in kids: child_path = os.path.join(dir_path, name) a.append((child_path, ie)) if ie.kind == 'directory': accum(ie, ie, child_path, a) a = [] accumt(self, self.root, a) return a def directories(self): """Return (path, entry) pairs for all directories. """ def descend(parent_ie): parent_name = parent_ie.name yield parent_name, parent_ie # directory children in sorted order dn = [] for ie in parent_ie.children.itervalues(): if ie.kind == 'directory': dn.append((ie.name, ie)) dn.sort() for name, child_ie in dn: for sub_name, sub_ie in descend(child_ie): yield appendpath(parent_name, sub_name), sub_ie for name, ie in descend(self.root): yield name, ie def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID)) >>> inv['123123'].name 'hello.c' """ try: return self._byid[file_id] except KeyError: if file_id == None: raise BzrError("can't look up file_id None") else: raise BzrError("file_id {%s} not in inventory" % file_id) def get_file_kind(self, file_id): return self._byid[file_id].kind def get_child(self, parent_id, filename): return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self._byid: bailout("inventory already contains entry with id {%s}" % entry.file_id) try: parent = self._byid[entry.parent_id] except KeyError: bailout("parent_id {%s} not in inventory" % entry.parent_id) if parent.children.has_key(entry.name): bailout("%s is already versioned" % appendpath(self.id2path(parent.file_id), entry.name)) self._byid[entry.file_id] = entry parent.children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: bailout("cannot re-add root of inventory") if file_id == None: file_id = bzrlib.branch.gen_file_id(relpath) parent_id = self.path2id(parts[:-1]) assert parent_id != None ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID)) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __eq__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 True """ if not isinstance(other, Inventory): return NotImplemented if len(self._byid) != len(other._byid): # shortcut: obviously not the same return False return self._byid == other._byid def __ne__(self, other): return not (self == other) def __hash__(self): raise ValueError('not hashable') def get_idpath(self, file_id): """Return a list of file_ids for the path to an entry. The list contains one element for each directory followed by the id of the file itself. So the length of the returned list is equal to the depth of the file in the tree, counting the root directory as depth 1. """ p = [] while file_id != None: try: ie = self._byid[file_id] except KeyError: bailout("file_id {%s} not found in inventory" % file_id) p.insert(0, ie.file_id) file_id = ie.parent_id return p def id2path(self, file_id): """Return as a list the path to file_id.""" # get all names, skipping root p = [self[fid].name for fid in self.get_idpath(file_id)[1:]] return os.sep.join(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. Returns None iff the path is not found. """ if isinstance(name, types.StringTypes): name = splitpath(name) mutter("lookup path %r" % name) parent = self.root for f in name: try: cie = parent.children[f] assert cie.name == f assert cie.parent_id == parent.file_id parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): return self._byid.has_key(file_id) def rename(self, file_id, new_parent_id, new_name): """Move a file within the inventory. This can change either the name, or the parent, or both. This does not move the working file.""" if not is_valid_name(new_name): bailout("not an acceptable filename: %r" % new_name) new_parent = self._byid[new_parent_id] if new_name in new_parent.children: bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id))) new_parent_idpath = self.get_idpath(new_parent_id) if file_id in new_parent_idpath: bailout("cannot move directory %r into a subdirectory of itself, %r" % (self.id2path(file_id), self.id2path(new_parent_id))) file_ie = self._byid[file_id] old_parent = self._byid[file_ie.parent_id] # TODO: Don't leave things messed up if this fails del old_parent.children[file_ie.name] new_parent.children[new_name] = file_ie file_ie.name = new_name file_ie.parent_id = new_parent_id _NAME_RE = re.compile(r'^[^/\\]+$') def is_valid_name(name): return bool(_NAME_RE.match(name)) commit refs/heads/master mark :556 committer Martin Pool 1116993291 +1000 data 93 - fix up Inventory.entries() - make 'inventory' command use entries() for performance testing from :555 M 644 inline bzrlib/commands.py data 36426 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date from bzrlib import merge def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command: """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0], lock_mode='r') file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.', lock_mode='r') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0], lock_mode='r') file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.', lock_mode='r') show_diff(b, revision, specific_files=file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False): from bzrlib import show_log, find_branch direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename, lock_mode='r') fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.', lock_mode='r') file_id = None show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=sys.stdout, direction=direction) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures return 1 else: print return 0 class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): merge.merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'forward': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/inventory.py data 19331 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # This should really be an id randomly assigned when the tree is # created, but it's not for now. ROOT_ID = "TREE_ROOT" import sys, os.path, types, re try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from xml import XMLMixin from errors import bailout, BzrError, BzrCheckError import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') 'TREE_ROOT' >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID)) >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123')) >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' TODO: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ # TODO: split InventoryEntry into subclasses for files, # directories, etc etc. text_sha1 = None text_size = None def __init__(self, file_id, name, kind, parent_id, text_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID) >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID) Traceback (most recent call last): BzrCheckError: InventoryEntry name 'src/hello.c' is invalid """ if '/' in name or '\\' in name: raise BzrCheckError('InventoryEntry name %r is invalid' % name) self.file_id = file_id self.name = name self.kind = kind self.text_id = text_id self.parent_id = parent_id if kind == 'directory': self.children = {} elif kind == 'file': pass else: raise BzrError("unhandled entry kind %r" % kind) def sorted_children(self): l = self.children.items() l.sort() return l def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.parent_id, text_id=self.text_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size # note that children are *not* copied; they're pulled across when # others are added return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size != None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1']: v = getattr(self, f) if v != None: e.set(f, v) # to be conservative, we don't externalize the root pointers # for now, leaving them as null in the xml form. in a future # version it will be implied by nested elements. if self.parent_id != ROOT_ID: assert isinstance(self.parent_id, basestring) e.set('parent_id', self.parent_id) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' ## original format inventories don't have a parent_id for ## nodes in the root directory, but it's cleaner to use one ## internally. parent_id = elt.get('parent_id') if parent_id == None: parent_id = ROOT_ID self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __eq__(self, other): if not isinstance(other, InventoryEntry): return NotImplemented return (self.file_id == other.file_id) \ and (self.name == other.name) \ and (self.text_sha1 == other.text_sha1) \ and (self.text_size == other.text_size) \ and (self.text_id == other.text_id) \ and (self.parent_id == other.parent_id) \ and (self.kind == other.kind) def __ne__(self, other): return not (self == other) def __hash__(self): raise ValueError('not hashable') class RootEntry(InventoryEntry): def __init__(self, file_id): self.file_id = file_id self.children = {} self.kind = 'root_directory' self.parent_id = None self.name = '' def __eq__(self, other): if not isinstance(other, RootEntry): return NotImplemented return (self.file_id == other.file_id) \ and (self.children == other.children) class Inventory(XMLMixin): """Inventory of versioned files in a tree. This describes which file_id is present at each point in the tree, and possibly the SHA-1 or other information about the file. Entries can be looked up either by path or by file_id. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted, other than through the Inventory API. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID)) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. The inventory is created with a default root directory, with an id of None. """ self.root = RootEntry(ROOT_ID) self._byid = {self.root.file_id: self.root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, from_dir=None): """Return (path, entry) pairs, in order by name.""" if from_dir == None: assert self.root from_dir = self.root elif isinstance(from_dir, basestring): from_dir = self._byid[from_dir] kids = from_dir.children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(from_dir=ie.file_id): yield os.path.join(name, cn), cie def entries(self): """Return list of (path, ie) for all entries except the root. This may be faster than iter_entries. """ def accum(dir_ie, dir_path, a): kids = dir_ie.children.items() kids.sort() for name, ie in kids: child_path = os.path.join(dir_path, name) a.append((child_path, ie)) if ie.kind == 'directory': accum(ie, child_path, a) a = [] accum(self.root, '', a) return a def directories(self): """Return (path, entry) pairs for all directories. """ def descend(parent_ie): parent_name = parent_ie.name yield parent_name, parent_ie # directory children in sorted order dn = [] for ie in parent_ie.children.itervalues(): if ie.kind == 'directory': dn.append((ie.name, ie)) dn.sort() for name, child_ie in dn: for sub_name, sub_ie in descend(child_ie): yield appendpath(parent_name, sub_name), sub_ie for name, ie in descend(self.root): yield name, ie def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID)) >>> inv['123123'].name 'hello.c' """ try: return self._byid[file_id] except KeyError: if file_id == None: raise BzrError("can't look up file_id None") else: raise BzrError("file_id {%s} not in inventory" % file_id) def get_file_kind(self, file_id): return self._byid[file_id].kind def get_child(self, parent_id, filename): return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self._byid: bailout("inventory already contains entry with id {%s}" % entry.file_id) try: parent = self._byid[entry.parent_id] except KeyError: bailout("parent_id {%s} not in inventory" % entry.parent_id) if parent.children.has_key(entry.name): bailout("%s is already versioned" % appendpath(self.id2path(parent.file_id), entry.name)) self._byid[entry.file_id] = entry parent.children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: bailout("cannot re-add root of inventory") if file_id == None: file_id = bzrlib.branch.gen_file_id(relpath) parent_id = self.path2id(parts[:-1]) assert parent_id != None ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID)) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __eq__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 True """ if not isinstance(other, Inventory): return NotImplemented if len(self._byid) != len(other._byid): # shortcut: obviously not the same return False return self._byid == other._byid def __ne__(self, other): return not (self == other) def __hash__(self): raise ValueError('not hashable') def get_idpath(self, file_id): """Return a list of file_ids for the path to an entry. The list contains one element for each directory followed by the id of the file itself. So the length of the returned list is equal to the depth of the file in the tree, counting the root directory as depth 1. """ p = [] while file_id != None: try: ie = self._byid[file_id] except KeyError: bailout("file_id {%s} not found in inventory" % file_id) p.insert(0, ie.file_id) file_id = ie.parent_id return p def id2path(self, file_id): """Return as a list the path to file_id.""" # get all names, skipping root p = [self[fid].name for fid in self.get_idpath(file_id)[1:]] return os.sep.join(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. Returns None iff the path is not found. """ if isinstance(name, types.StringTypes): name = splitpath(name) mutter("lookup path %r" % name) parent = self.root for f in name: try: cie = parent.children[f] assert cie.name == f assert cie.parent_id == parent.file_id parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): return self._byid.has_key(file_id) def rename(self, file_id, new_parent_id, new_name): """Move a file within the inventory. This can change either the name, or the parent, or both. This does not move the working file.""" if not is_valid_name(new_name): bailout("not an acceptable filename: %r" % new_name) new_parent = self._byid[new_parent_id] if new_name in new_parent.children: bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id))) new_parent_idpath = self.get_idpath(new_parent_id) if file_id in new_parent_idpath: bailout("cannot move directory %r into a subdirectory of itself, %r" % (self.id2path(file_id), self.id2path(new_parent_id))) file_ie = self._byid[file_id] old_parent = self._byid[file_ie.parent_id] # TODO: Don't leave things messed up if this fails del old_parent.children[file_ie.name] new_parent.children[new_name] = file_ie file_ie.name = new_name file_ie.parent_id = new_parent_id _NAME_RE = re.compile(r'^[^/\\]+$') def is_valid_name(name): return bool(_NAME_RE.match(name)) commit refs/heads/master mark :557 committer Martin Pool 1117072067 +1000 data 150 - Refactor/cleanup Inventory.entries() - Rewrite Inventory.directories() to return a list rather than recursive generators; simpler and much faster from :556 M 644 inline bzrlib/inventory.py data 19216 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # This should really be an id randomly assigned when the tree is # created, but it's not for now. ROOT_ID = "TREE_ROOT" import sys, os.path, types, re try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from xml import XMLMixin from errors import bailout, BzrError, BzrCheckError import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') 'TREE_ROOT' >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID)) >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123')) Traceback (most recent call last): ... BzrError: ('inventory already contains entry with id {2323}', []) >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123')) >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' TODO: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ # TODO: split InventoryEntry into subclasses for files, # directories, etc etc. text_sha1 = None text_size = None def __init__(self, file_id, name, kind, parent_id, text_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID) >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID) Traceback (most recent call last): BzrCheckError: InventoryEntry name 'src/hello.c' is invalid """ if '/' in name or '\\' in name: raise BzrCheckError('InventoryEntry name %r is invalid' % name) self.file_id = file_id self.name = name self.kind = kind self.text_id = text_id self.parent_id = parent_id if kind == 'directory': self.children = {} elif kind == 'file': pass else: raise BzrError("unhandled entry kind %r" % kind) def sorted_children(self): l = self.children.items() l.sort() return l def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.parent_id, text_id=self.text_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size # note that children are *not* copied; they're pulled across when # others are added return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size != None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1']: v = getattr(self, f) if v != None: e.set(f, v) # to be conservative, we don't externalize the root pointers # for now, leaving them as null in the xml form. in a future # version it will be implied by nested elements. if self.parent_id != ROOT_ID: assert isinstance(self.parent_id, basestring) e.set('parent_id', self.parent_id) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' ## original format inventories don't have a parent_id for ## nodes in the root directory, but it's cleaner to use one ## internally. parent_id = elt.get('parent_id') if parent_id == None: parent_id = ROOT_ID self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __eq__(self, other): if not isinstance(other, InventoryEntry): return NotImplemented return (self.file_id == other.file_id) \ and (self.name == other.name) \ and (self.text_sha1 == other.text_sha1) \ and (self.text_size == other.text_size) \ and (self.text_id == other.text_id) \ and (self.parent_id == other.parent_id) \ and (self.kind == other.kind) def __ne__(self, other): return not (self == other) def __hash__(self): raise ValueError('not hashable') class RootEntry(InventoryEntry): def __init__(self, file_id): self.file_id = file_id self.children = {} self.kind = 'root_directory' self.parent_id = None self.name = '' def __eq__(self, other): if not isinstance(other, RootEntry): return NotImplemented return (self.file_id == other.file_id) \ and (self.children == other.children) class Inventory(XMLMixin): """Inventory of versioned files in a tree. This describes which file_id is present at each point in the tree, and possibly the SHA-1 or other information about the file. Entries can be looked up either by path or by file_id. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted, other than through the Inventory API. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID)) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. The inventory is created with a default root directory, with an id of None. """ self.root = RootEntry(ROOT_ID) self._byid = {self.root.file_id: self.root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, from_dir=None): """Return (path, entry) pairs, in order by name.""" if from_dir == None: assert self.root from_dir = self.root elif isinstance(from_dir, basestring): from_dir = self._byid[from_dir] kids = from_dir.children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(from_dir=ie.file_id): yield os.path.join(name, cn), cie def entries(self): """Return list of (path, ie) for all entries except the root. This may be faster than iter_entries. """ accum = [] def descend(dir_ie, dir_path): kids = dir_ie.children.items() kids.sort() for name, ie in kids: child_path = os.path.join(dir_path, name) accum.append((child_path, ie)) if ie.kind == 'directory': descend(ie, child_path) descend(self.root, '') return accum def directories(self): """Return (path, entry) pairs for all directories, including the root. """ accum = [] def descend(parent_ie, parent_path): accum.append((parent_path, parent_ie)) kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory'] kids.sort() for name, child_ie in kids: child_path = os.path.join(parent_path, name) descend(child_ie, child_path) descend(self.root, '') return accum def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID)) >>> inv['123123'].name 'hello.c' """ try: return self._byid[file_id] except KeyError: if file_id == None: raise BzrError("can't look up file_id None") else: raise BzrError("file_id {%s} not in inventory" % file_id) def get_file_kind(self, file_id): return self._byid[file_id].kind def get_child(self, parent_id, filename): return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self._byid: bailout("inventory already contains entry with id {%s}" % entry.file_id) try: parent = self._byid[entry.parent_id] except KeyError: bailout("parent_id {%s} not in inventory" % entry.parent_id) if parent.children.has_key(entry.name): bailout("%s is already versioned" % appendpath(self.id2path(parent.file_id), entry.name)) self._byid[entry.file_id] = entry parent.children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: bailout("cannot re-add root of inventory") if file_id == None: file_id = bzrlib.branch.gen_file_id(relpath) parent_id = self.path2id(parts[:-1]) assert parent_id != None ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID)) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __eq__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 True """ if not isinstance(other, Inventory): return NotImplemented if len(self._byid) != len(other._byid): # shortcut: obviously not the same return False return self._byid == other._byid def __ne__(self, other): return not (self == other) def __hash__(self): raise ValueError('not hashable') def get_idpath(self, file_id): """Return a list of file_ids for the path to an entry. The list contains one element for each directory followed by the id of the file itself. So the length of the returned list is equal to the depth of the file in the tree, counting the root directory as depth 1. """ p = [] while file_id != None: try: ie = self._byid[file_id] except KeyError: bailout("file_id {%s} not found in inventory" % file_id) p.insert(0, ie.file_id) file_id = ie.parent_id return p def id2path(self, file_id): """Return as a list the path to file_id.""" # get all names, skipping root p = [self[fid].name for fid in self.get_idpath(file_id)[1:]] return os.sep.join(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. Returns None iff the path is not found. """ if isinstance(name, types.StringTypes): name = splitpath(name) mutter("lookup path %r" % name) parent = self.root for f in name: try: cie = parent.children[f] assert cie.name == f assert cie.parent_id == parent.file_id parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): return self._byid.has_key(file_id) def rename(self, file_id, new_parent_id, new_name): """Move a file within the inventory. This can change either the name, or the parent, or both. This does not move the working file.""" if not is_valid_name(new_name): bailout("not an acceptable filename: %r" % new_name) new_parent = self._byid[new_parent_id] if new_name in new_parent.children: bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id))) new_parent_idpath = self.get_idpath(new_parent_id) if file_id in new_parent_idpath: bailout("cannot move directory %r into a subdirectory of itself, %r" % (self.id2path(file_id), self.id2path(new_parent_id))) file_ie = self._byid[file_id] old_parent = self._byid[file_ie.parent_id] # TODO: Don't leave things messed up if this fails del old_parent.children[file_ie.name] new_parent.children[new_name] = file_ie file_ie.name = new_name file_ie.parent_id = new_parent_id _NAME_RE = re.compile(r'^[^/\\]+$') def is_valid_name(name): return bool(_NAME_RE.match(name)) commit refs/heads/master mark :558 committer Martin Pool 1117073637 +1000 data 72 - All top-level classes inherit from object (Python new-style classes) from :557 M 644 inline bzrlib/atomicfile.py data 2268 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA class AtomicFile(object): """A file that does an atomic-rename to move into place. This also causes hardlinks to break when it's written out. Open this as for a regular file, then use commit() to move into place or abort() to cancel. An encoding can be specified; otherwise the default is ascii. """ def __init__(self, filename, mode='wb', encoding=None): if mode != 'wb' and mode != 'wt': raise ValueError("invalid AtomicFile mode %r" % mode) import os, socket self.tmpfilename = '%s.%d.%s.tmp' % (filename, os.getpid(), socket.gethostname()) self.realfilename = filename self.f = open(self.tmpfilename, mode) if encoding: import codecs self.f = codecs.EncodedFile(self.f, encoding) self.write = self.f.write self.closed = property(self.f.closed) def commit(self): """Close the file and move to final name.""" import sys, os self.f.close() if sys.platform == 'win32': os.remove(self.realfilename) os.rename(self.tmpfilename, self.realfilename) def abort(self): """Discard temporary file without committing changes.""" import os self.f.close() os.remove(self.tmpfilename) def close(self): """Discard the file unless already committed.""" if not self.closed: self.abort() M 644 inline bzrlib/branch.py data 26876 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch(object): """Branch holding a history of revisions. base Base directory of the branch. """ _lockmode = None def __init__(self, base, init=False, find_root=True, lock_mode='w'): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.lock(lock_mode) self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def lock(self, mode='w'): """Lock the on-disk branch, excluding other processes.""" try: import fcntl, errno if mode == 'w': lm = fcntl.LOCK_EX om = os.O_WRONLY | os.O_CREAT elif mode == 'r': lm = fcntl.LOCK_SH om = os.O_RDONLY else: raise BzrError("invalid locking mode %r" % mode) try: lockfile = os.open(self.controlfilename('branch-lock'), om) except OSError, e: if e.errno == errno.ENOENT: # might not exist on branches from <0.0.4 self.controlfile('branch-lock', 'w').close() lockfile = os.open(self.controlfilename('branch-lock'), om) else: raise e fcntl.lockf(lockfile, lm) def unlock(): fcntl.lockf(lockfile, fcntl.LOCK_UN) os.close(lockfile) self._lockmode = None self.unlock = unlock self._lockmode = mode except ImportError: warning("please write a locking method for platform %r" % sys.platform) def unlock(): self._lockmode = None self.unlock = unlock self._lockmode = mode def _need_readlock(self): if self._lockmode not in ['r', 'w']: raise BzrError('need read lock on branch, only have %r' % self._lockmode) def _need_writelock(self): if self._lockmode not in ['w']: raise BzrError('need write lock on branch, only have %r' % self._lockmode) def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" self._need_readlock() before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self._need_writelock() ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ self._need_writelock() # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" self._need_readlock() tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability self._need_writelock() if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" self._need_readlock() r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" self._need_readlock() i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" self._need_readlock() if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self._need_readlock() return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def commit(self, *args, **kw): """Deprecated""" from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. self._need_readlock() if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self._need_writelock() tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self._need_writelock() ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/changeset.py data 53448 # Copyright (C) 2004 Aaron Bentley # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os.path import errno import patch import stat """ Represent and apply a changeset """ __docformat__ = "restructuredtext" NULL_ID = "!NULL" def invert_dict(dict): newdict = {} for (key,value) in dict.iteritems(): newdict[value] = key return newdict class PatchApply(object): """Patch application as a kind of content change""" def __init__(self, contents): """Constructor. :param contents: The text of the patch to apply :type contents: str""" self.contents = contents def __eq__(self, other): if not isinstance(other, PatchApply): return False elif self.contents != other.contents: return False else: return True def __ne__(self, other): return not (self == other) def apply(self, filename, conflict_handler, reverse=False): """Applies the patch to the specified file. :param filename: the file to apply the patch to :type filename: str :param reverse: If true, apply the patch in reverse :type reverse: bool """ input_name = filename+".orig" try: os.rename(filename, input_name) except OSError, e: if e.errno != errno.ENOENT: raise if conflict_handler.patch_target_missing(filename, self.contents)\ == "skip": return os.rename(filename, input_name) status = patch.patch(self.contents, input_name, filename, reverse) os.chmod(filename, os.stat(input_name).st_mode) if status == 0: os.unlink(input_name) elif status == 1: conflict_handler.failed_hunks(filename) class ChangeUnixPermissions(object): """This is two-way change, suitable for file modification, creation, deletion""" def __init__(self, old_mode, new_mode): self.old_mode = old_mode self.new_mode = new_mode def apply(self, filename, conflict_handler, reverse=False): if not reverse: from_mode = self.old_mode to_mode = self.new_mode else: from_mode = self.new_mode to_mode = self.old_mode try: current_mode = os.stat(filename).st_mode &0777 except OSError, e: if e.errno == errno.ENOENT: if conflict_handler.missing_for_chmod(filename) == "skip": return else: current_mode = from_mode if from_mode is not None and current_mode != from_mode: if conflict_handler.wrong_old_perms(filename, from_mode, current_mode) != "continue": return if to_mode is not None: try: os.chmod(filename, to_mode) except IOError, e: if e.errno == errno.ENOENT: conflict_handler.missing_for_chmod(filename) def __eq__(self, other): if not isinstance(other, ChangeUnixPermissions): return False elif self.old_mode != other.old_mode: return False elif self.new_mode != other.new_mode: return False else: return True def __ne__(self, other): return not (self == other) def dir_create(filename, conflict_handler, reverse): """Creates the directory, or deletes it if reverse is true. Intended to be used with ReplaceContents. :param filename: The name of the directory to create :type filename: str :param reverse: If true, delete the directory, instead :type reverse: bool """ if not reverse: try: os.mkdir(filename) except OSError, e: if e.errno != errno.EEXIST: raise if conflict_handler.dir_exists(filename) == "continue": os.mkdir(filename) except IOError, e: if e.errno == errno.ENOENT: if conflict_handler.missing_parent(filename)=="continue": file(filename, "wb").write(self.contents) else: try: os.rmdir(filename) except OSError, e: if e.errno != 39: raise if conflict_handler.rmdir_non_empty(filename) == "skip": return os.rmdir(filename) class SymlinkCreate(object): """Creates or deletes a symlink (for use with ReplaceContents)""" def __init__(self, contents): """Constructor. :param contents: The filename of the target the symlink should point to :type contents: str """ self.target = contents def __call__(self, filename, conflict_handler, reverse): """Creates or destroys the symlink. :param filename: The name of the symlink to create :type filename: str """ if reverse: assert(os.readlink(filename) == self.target) os.unlink(filename) else: try: os.symlink(self.target, filename) except OSError, e: if e.errno != errno.EEXIST: raise if conflict_handler.link_name_exists(filename) == "continue": os.symlink(self.target, filename) def __eq__(self, other): if not isinstance(other, SymlinkCreate): return False elif self.target != other.target: return False else: return True def __ne__(self, other): return not (self == other) class FileCreate(object): """Create or delete a file (for use with ReplaceContents)""" def __init__(self, contents): """Constructor :param contents: The contents of the file to write :type contents: str """ self.contents = contents def __repr__(self): return "FileCreate(%i b)" % len(self.contents) def __eq__(self, other): if not isinstance(other, FileCreate): return False elif self.contents != other.contents: return False else: return True def __ne__(self, other): return not (self == other) def __call__(self, filename, conflict_handler, reverse): """Create or delete a file :param filename: The name of the file to create :type filename: str :param reverse: Delete the file instead of creating it :type reverse: bool """ if not reverse: try: file(filename, "wb").write(self.contents) except IOError, e: if e.errno == errno.ENOENT: if conflict_handler.missing_parent(filename)=="continue": file(filename, "wb").write(self.contents) else: raise else: try: if (file(filename, "rb").read() != self.contents): direction = conflict_handler.wrong_old_contents(filename, self.contents) if direction != "continue": return os.unlink(filename) except IOError, e: if e.errno != errno.ENOENT: raise if conflict_handler.missing_for_rm(filename, undo) == "skip": return def reversed(sequence): max = len(sequence) - 1 for i in range(len(sequence)): yield sequence[max - i] class ReplaceContents(object): """A contents-replacement framework. It allows a file/directory/symlink to be created, deleted, or replaced with another file/directory/symlink. Arguments must be callable with (filename, reverse). """ def __init__(self, old_contents, new_contents): """Constructor. :param old_contents: The change to reverse apply (e.g. a deletion), \ when going forwards. :type old_contents: `dir_create`, `SymlinkCreate`, `FileCreate`, \ NoneType, etc. :param new_contents: The second change to apply (e.g. a creation), \ when going forwards. :type new_contents: `dir_create`, `SymlinkCreate`, `FileCreate`, \ NoneType, etc. """ self.old_contents=old_contents self.new_contents=new_contents def __repr__(self): return "ReplaceContents(%r -> %r)" % (self.old_contents, self.new_contents) def __eq__(self, other): if not isinstance(other, ReplaceContents): return False elif self.old_contents != other.old_contents: return False elif self.new_contents != other.new_contents: return False else: return True def __ne__(self, other): return not (self == other) def apply(self, filename, conflict_handler, reverse=False): """Applies the FileReplacement to the specified filename :param filename: The name of the file to apply changes to :type filename: str :param reverse: If true, apply the change in reverse :type reverse: bool """ if not reverse: undo = self.old_contents perform = self.new_contents else: undo = self.new_contents perform = self.old_contents mode = None if undo is not None: try: mode = os.lstat(filename).st_mode if stat.S_ISLNK(mode): mode = None except OSError, e: if e.errno != errno.ENOENT: raise if conflict_handler.missing_for_rm(filename, undo) == "skip": return undo(filename, conflict_handler, reverse=True) if perform is not None: perform(filename, conflict_handler, reverse=False) if mode is not None: os.chmod(filename, mode) class ApplySequence(object): def __init__(self, changes=None): self.changes = [] if changes is not None: self.changes.extend(changes) def __eq__(self, other): if not isinstance(other, ApplySequence): return False elif len(other.changes) != len(self.changes): return False else: for i in range(len(self.changes)): if self.changes[i] != other.changes[i]: return False return True def __ne__(self, other): return not (self == other) def apply(self, filename, conflict_handler, reverse=False): if not reverse: iter = self.changes else: iter = reversed(self.changes) for change in iter: change.apply(filename, conflict_handler, reverse) class Diff3Merge(object): def __init__(self, base_file, other_file): self.base_file = base_file self.other_file = other_file def __eq__(self, other): if not isinstance(other, Diff3Merge): return False return (self.base_file == other.base_file and self.other_file == other.other_file) def __ne__(self, other): return not (self == other) def apply(self, filename, conflict_handler, reverse=False): new_file = filename+".new" if not reverse: base = self.base_file other = self.other_file else: base = self.other_file other = self.base_file status = patch.diff3(new_file, filename, base, other) if status == 0: os.chmod(new_file, os.stat(filename).st_mode) os.rename(new_file, filename) return else: assert(status == 1) conflict_handler.merge_conflict(new_file, filename, base, other) def CreateDir(): """Convenience function to create a directory. :return: A ReplaceContents that will create a directory :rtype: `ReplaceContents` """ return ReplaceContents(None, dir_create) def DeleteDir(): """Convenience function to delete a directory. :return: A ReplaceContents that will delete a directory :rtype: `ReplaceContents` """ return ReplaceContents(dir_create, None) def CreateFile(contents): """Convenience fucntion to create a file. :param contents: The contents of the file to create :type contents: str :return: A ReplaceContents that will create a file :rtype: `ReplaceContents` """ return ReplaceContents(None, FileCreate(contents)) def DeleteFile(contents): """Convenience fucntion to delete a file. :param contents: The contents of the file to delete :type contents: str :return: A ReplaceContents that will delete a file :rtype: `ReplaceContents` """ return ReplaceContents(FileCreate(contents), None) def ReplaceFileContents(old_contents, new_contents): """Convenience fucntion to replace the contents of a file. :param old_contents: The contents of the file to replace :type old_contents: str :param new_contents: The contents to replace the file with :type new_contents: str :return: A ReplaceContents that will replace the contents of a file a file :rtype: `ReplaceContents` """ return ReplaceContents(FileCreate(old_contents), FileCreate(new_contents)) def CreateSymlink(target): """Convenience fucntion to create a symlink. :param target: The path the link should point to :type target: str :return: A ReplaceContents that will delete a file :rtype: `ReplaceContents` """ return ReplaceContents(None, SymlinkCreate(target)) def DeleteSymlink(target): """Convenience fucntion to delete a symlink. :param target: The path the link should point to :type target: str :return: A ReplaceContents that will delete a file :rtype: `ReplaceContents` """ return ReplaceContents(SymlinkCreate(target), None) def ChangeTarget(old_target, new_target): """Convenience fucntion to change the target of a symlink. :param old_target: The current link target :type old_target: str :param new_target: The new link target to use :type new_target: str :return: A ReplaceContents that will delete a file :rtype: `ReplaceContents` """ return ReplaceContents(SymlinkCreate(old_target), SymlinkCreate(new_target)) class InvalidEntry(Exception): """Raise when a ChangesetEntry is invalid in some way""" def __init__(self, entry, problem): """Constructor. :param entry: The invalid ChangesetEntry :type entry: `ChangesetEntry` :param problem: The problem with the entry :type problem: str """ msg = "Changeset entry for %s (%s) is invalid.\n%s" % (entry.id, entry.path, problem) Exception.__init__(self, msg) self.entry = entry class SourceRootHasName(InvalidEntry): """This changeset entry has a name other than "", but its parent is !NULL""" def __init__(self, entry, name): """Constructor. :param entry: The invalid ChangesetEntry :type entry: `ChangesetEntry` :param name: The name of the entry :type name: str """ msg = 'Child of !NULL is named "%s", not "./.".' % name InvalidEntry.__init__(self, entry, msg) class NullIDAssigned(InvalidEntry): """The id !NULL was assigned to a real entry""" def __init__(self, entry): """Constructor. :param entry: The invalid ChangesetEntry :type entry: `ChangesetEntry` """ msg = '"!NULL" id assigned to a file "%s".' % entry.path InvalidEntry.__init__(self, entry, msg) class ParentIDIsSelf(InvalidEntry): """An entry is marked as its own parent""" def __init__(self, entry): """Constructor. :param entry: The invalid ChangesetEntry :type entry: `ChangesetEntry` """ msg = 'file %s has "%s" id for both self id and parent id.' % \ (entry.path, entry.id) InvalidEntry.__init__(self, entry, msg) class ChangesetEntry(object): """An entry the changeset""" def __init__(self, id, parent, path): """Constructor. Sets parent and name assuming it was not renamed/created/deleted. :param id: The id associated with the entry :param parent: The id of the parent of this entry (or !NULL if no parent) :param path: The file path relative to the tree root of this entry """ self.id = id self.path = path self.new_path = path self.parent = parent self.new_parent = parent self.contents_change = None self.metadata_change = None if parent == NULL_ID and path !='./.': raise SourceRootHasName(self, path) if self.id == NULL_ID: raise NullIDAssigned(self) if self.id == self.parent: raise ParentIDIsSelf(self) def __str__(self): return "ChangesetEntry(%s)" % self.id def __get_dir(self): if self.path is None: return None return os.path.dirname(self.path) def __set_dir(self, dir): self.path = os.path.join(dir, os.path.basename(self.path)) dir = property(__get_dir, __set_dir) def __get_name(self): if self.path is None: return None return os.path.basename(self.path) def __set_name(self, name): self.path = os.path.join(os.path.dirname(self.path), name) name = property(__get_name, __set_name) def __get_new_dir(self): if self.new_path is None: return None return os.path.dirname(self.new_path) def __set_new_dir(self, dir): self.new_path = os.path.join(dir, os.path.basename(self.new_path)) new_dir = property(__get_new_dir, __set_new_dir) def __get_new_name(self): if self.new_path is None: return None return os.path.basename(self.new_path) def __set_new_name(self, name): self.new_path = os.path.join(os.path.dirname(self.new_path), name) new_name = property(__get_new_name, __set_new_name) def needs_rename(self): """Determines whether the entry requires renaming. :rtype: bool """ return (self.parent != self.new_parent or self.name != self.new_name) def is_deletion(self, reverse): """Return true if applying the entry would delete a file/directory. :param reverse: if true, the changeset is being applied in reverse :rtype: bool """ return ((self.new_parent is None and not reverse) or (self.parent is None and reverse)) def is_creation(self, reverse): """Return true if applying the entry would create a file/directory. :param reverse: if true, the changeset is being applied in reverse :rtype: bool """ return ((self.parent is None and not reverse) or (self.new_parent is None and reverse)) def is_creation_or_deletion(self): """Return true if applying the entry would create or delete a file/directory. :rtype: bool """ return self.parent is None or self.new_parent is None def get_cset_path(self, mod=False): """Determine the path of the entry according to the changeset. :param changeset: The changeset to derive the path from :type changeset: `Changeset` :param mod: If true, generate the MOD path. Otherwise, generate the \ ORIG path. :return: the path of the entry, or None if it did not exist in the \ requested tree. :rtype: str or NoneType """ if mod: if self.new_parent == NULL_ID: return "./." elif self.new_parent is None: return None return self.new_path else: if self.parent == NULL_ID: return "./." elif self.parent is None: return None return self.path def summarize_name(self, changeset, reverse=False): """Produce a one-line summary of the filename. Indicates renames as old => new, indicates creation as None => new, indicates deletion as old => None. :param changeset: The changeset to get paths from :type changeset: `Changeset` :param reverse: If true, reverse the names in the output :type reverse: bool :rtype: str """ orig_path = self.get_cset_path(False) mod_path = self.get_cset_path(True) if orig_path is not None: orig_path = orig_path[2:] if mod_path is not None: mod_path = mod_path[2:] if orig_path == mod_path: return orig_path else: if not reverse: return "%s => %s" % (orig_path, mod_path) else: return "%s => %s" % (mod_path, orig_path) def get_new_path(self, id_map, changeset, reverse=False): """Determine the full pathname to rename to :param id_map: The map of ids to filenames for the tree :type id_map: Dictionary :param changeset: The changeset to get data from :type changeset: `Changeset` :param reverse: If true, we're applying the changeset in reverse :type reverse: bool :rtype: str """ if reverse: parent = self.parent to_dir = self.dir from_dir = self.new_dir to_name = self.name from_name = self.new_name else: parent = self.new_parent to_dir = self.new_dir from_dir = self.dir to_name = self.new_name from_name = self.name if to_name is None: return None if parent == NULL_ID or parent is None: if to_name != '.': raise SourceRootHasName(self, to_name) else: return '.' if from_dir == to_dir: dir = os.path.dirname(id_map[self.id]) else: parent_entry = changeset.entries[parent] dir = parent_entry.get_new_path(id_map, changeset, reverse) if from_name == to_name: name = os.path.basename(id_map[self.id]) else: name = to_name assert(from_name is None or from_name == os.path.basename(id_map[self.id])) return os.path.join(dir, name) def is_boring(self): """Determines whether the entry does nothing :return: True if the entry does no renames or content changes :rtype: bool """ if self.contents_change is not None: return False elif self.metadata_change is not None: return False elif self.parent != self.new_parent: return False elif self.name != self.new_name: return False else: return True def apply(self, filename, conflict_handler, reverse=False): """Applies the file content and/or metadata changes. :param filename: the filename of the entry :type filename: str :param reverse: If true, apply the changes in reverse :type reverse: bool """ if self.is_deletion(reverse) and self.metadata_change is not None: self.metadata_change.apply(filename, conflict_handler, reverse) if self.contents_change is not None: self.contents_change.apply(filename, conflict_handler, reverse) if not self.is_deletion(reverse) and self.metadata_change is not None: self.metadata_change.apply(filename, conflict_handler, reverse) class IDPresent(Exception): def __init__(self, id): msg = "Cannot add entry because that id has already been used:\n%s" %\ id Exception.__init__(self, msg) self.id = id class Changeset(object): """A set of changes to apply""" def __init__(self): self.entries = {} def add_entry(self, entry): """Add an entry to the list of entries""" if self.entries.has_key(entry.id): raise IDPresent(entry.id) self.entries[entry.id] = entry def my_sort(sequence, key, reverse=False): """A sort function that supports supplying a key for comparison :param sequence: The sequence to sort :param key: A callable object that returns the values to be compared :param reverse: If true, sort in reverse order :type reverse: bool """ def cmp_by_key(entry_a, entry_b): if reverse: tmp=entry_a entry_a = entry_b entry_b = tmp return cmp(key(entry_a), key(entry_b)) sequence.sort(cmp_by_key) def get_rename_entries(changeset, inventory, reverse): """Return a list of entries that will be renamed. Entries are sorted from longest to shortest source path and from shortest to longest target path. :param changeset: The changeset to look in :type changeset: `Changeset` :param inventory: The source of current tree paths for the given ids :type inventory: Dictionary :param reverse: If true, the changeset is being applied in reverse :type reverse: bool :return: source entries and target entries as a tuple :rtype: (List, List) """ source_entries = [x for x in changeset.entries.itervalues() if x.needs_rename()] # these are done from longest path to shortest, to avoid deleting a # parent before its children are deleted/renamed def longest_to_shortest(entry): path = inventory.get(entry.id) if path is None: return 0 else: return len(path) my_sort(source_entries, longest_to_shortest, reverse=True) target_entries = source_entries[:] # These are done from shortest to longest path, to avoid creating a # child before its parent has been created/renamed def shortest_to_longest(entry): path = entry.get_new_path(inventory, changeset, reverse) if path is None: return 0 else: return len(path) my_sort(target_entries, shortest_to_longest) return (source_entries, target_entries) def rename_to_temp_delete(source_entries, inventory, dir, conflict_handler, reverse): """Delete and rename entries as appropriate. Entries are renamed to temp names. A map of id -> temp name is returned. :param source_entries: The entries to rename and delete :type source_entries: List of `ChangesetEntry` :param inventory: The map of id -> filename in the current tree :type inventory: Dictionary :param dir: The directory to apply changes to :type dir: str :param reverse: Apply changes in reverse :type reverse: bool :return: a mapping of id to temporary name :rtype: Dictionary """ temp_dir = os.path.join(dir, "temp") temp_name = {} for i in range(len(source_entries)): entry = source_entries[i] if entry.is_deletion(reverse): path = os.path.join(dir, inventory[entry.id]) entry.apply(path, conflict_handler, reverse) else: to_name = temp_dir+"/"+str(i) src_path = inventory.get(entry.id) if src_path is not None: src_path = os.path.join(dir, src_path) try: os.rename(src_path, to_name) temp_name[entry.id] = to_name except OSError, e: if e.errno != errno.ENOENT: raise if conflict_handler.missing_for_rename(src_path) == "skip": continue return temp_name def rename_to_new_create(temp_name, target_entries, inventory, changeset, dir, conflict_handler, reverse): """Rename entries with temp names to their final names, create new files. :param temp_name: A mapping of id to temporary name :type temp_name: Dictionary :param target_entries: The entries to apply changes to :type target_entries: List of `ChangesetEntry` :param changeset: The changeset to apply :type changeset: `Changeset` :param dir: The directory to apply changes to :type dir: str :param reverse: If true, apply changes in reverse :type reverse: bool """ for entry in target_entries: new_path = entry.get_new_path(inventory, changeset, reverse) if new_path is None: continue new_path = os.path.join(dir, new_path) old_path = temp_name.get(entry.id) if os.path.exists(new_path): if conflict_handler.target_exists(entry, new_path, old_path) == \ "skip": continue if entry.is_creation(reverse): entry.apply(new_path, conflict_handler, reverse) else: if old_path is None: continue try: os.rename(old_path, new_path) except OSError, e: raise Exception ("%s is missing" % new_path) class TargetExists(Exception): def __init__(self, entry, target): msg = "The path %s already exists" % target Exception.__init__(self, msg) self.entry = entry self.target = target class RenameConflict(Exception): def __init__(self, id, this_name, base_name, other_name): msg = """Trees all have different names for a file this: %s base: %s other: %s id: %s""" % (this_name, base_name, other_name, id) Exception.__init__(self, msg) self.this_name = this_name self.base_name = base_name self_other_name = other_name class MoveConflict(Exception): def __init__(self, id, this_parent, base_parent, other_parent): msg = """The file is in different directories in every tree this: %s base: %s other: %s id: %s""" % (this_parent, base_parent, other_parent, id) Exception.__init__(self, msg) self.this_parent = this_parent self.base_parent = base_parent self_other_parent = other_parent class MergeConflict(Exception): def __init__(self, this_path): Exception.__init__(self, "Conflict applying changes to %s" % this_path) self.this_path = this_path class MergePermissionConflict(Exception): def __init__(self, this_path, base_path, other_path): this_perms = os.stat(this_path).st_mode & 0755 base_perms = os.stat(base_path).st_mode & 0755 other_perms = os.stat(other_path).st_mode & 0755 msg = """Conflicting permission for %s this: %o base: %o other: %o """ % (this_path, this_perms, base_perms, other_perms) self.this_path = this_path self.base_path = base_path self.other_path = other_path Exception.__init__(self, msg) class WrongOldContents(Exception): def __init__(self, filename): msg = "Contents mismatch deleting %s" % filename self.filename = filename Exception.__init__(self, msg) class WrongOldPermissions(Exception): def __init__(self, filename, old_perms, new_perms): msg = "Permission missmatch on %s:\n" \ "Expected 0%o, got 0%o." % (filename, old_perms, new_perms) self.filename = filename Exception.__init__(self, msg) class RemoveContentsConflict(Exception): def __init__(self, filename): msg = "Conflict deleting %s, which has different contents in BASE"\ " and THIS" % filename self.filename = filename Exception.__init__(self, msg) class DeletingNonEmptyDirectory(Exception): def __init__(self, filename): msg = "Trying to remove dir %s while it still had files" % filename self.filename = filename Exception.__init__(self, msg) class PatchTargetMissing(Exception): def __init__(self, filename): msg = "Attempt to patch %s, which does not exist" % filename Exception.__init__(self, msg) self.filename = filename class MissingPermsFile(Exception): def __init__(self, filename): msg = "Attempt to change permissions on %s, which does not exist" %\ filename Exception.__init__(self, msg) self.filename = filename class MissingForRm(Exception): def __init__(self, filename): msg = "Attempt to remove missing path %s" % filename Exception.__init__(self, msg) self.filename = filename class MissingForRename(Exception): def __init__(self, filename): msg = "Attempt to move missing path %s" % (filename) Exception.__init__(self, msg) self.filename = filename class ExceptionConflictHandler(object): def __init__(self, dir): self.dir = dir def missing_parent(self, pathname): parent = os.path.dirname(pathname) raise Exception("Parent directory missing for %s" % pathname) def dir_exists(self, pathname): raise Exception("Directory already exists for %s" % pathname) def failed_hunks(self, pathname): raise Exception("Failed to apply some hunks for %s" % pathname) def target_exists(self, entry, target, old_path): raise TargetExists(entry, target) def rename_conflict(self, id, this_name, base_name, other_name): raise RenameConflict(id, this_name, base_name, other_name) def move_conflict(self, id, inventory): this_dir = inventory.this.get_dir(id) base_dir = inventory.base.get_dir(id) other_dir = inventory.other.get_dir(id) raise MoveConflict(id, this_dir, base_dir, other_dir) def merge_conflict(self, new_file, this_path, base_path, other_path): os.unlink(new_file) raise MergeConflict(this_path) def permission_conflict(self, this_path, base_path, other_path): raise MergePermissionConflict(this_path, base_path, other_path) def wrong_old_contents(self, filename, expected_contents): raise WrongOldContents(filename) def rem_contents_conflict(self, filename, this_contents, base_contents): raise RemoveContentsConflict(filename) def wrong_old_perms(self, filename, old_perms, new_perms): raise WrongOldPermissions(filename, old_perms, new_perms) def rmdir_non_empty(self, filename): raise DeletingNonEmptyDirectory(filename) def link_name_exists(self, filename): raise TargetExists(filename) def patch_target_missing(self, filename, contents): raise PatchTargetMissing(filename) def missing_for_chmod(self, filename): raise MissingPermsFile(filename) def missing_for_rm(self, filename, change): raise MissingForRm(filename) def missing_for_rename(self, filename): raise MissingForRename(filename) def apply_changeset(changeset, inventory, dir, conflict_handler=None, reverse=False): """Apply a changeset to a directory. :param changeset: The changes to perform :type changeset: `Changeset` :param inventory: The mapping of id to filename for the directory :type inventory: Dictionary :param dir: The path of the directory to apply the changes to :type dir: str :param reverse: If true, apply the changes in reverse :type reverse: bool :return: The mapping of the changed entries :rtype: Dictionary """ if conflict_handler is None: conflict_handler = ExceptionConflictHandler(dir) temp_dir = dir+"/temp" os.mkdir(temp_dir) #apply changes that don't affect filenames for entry in changeset.entries.itervalues(): if not entry.is_creation_or_deletion(): path = os.path.join(dir, inventory[entry.id]) entry.apply(path, conflict_handler, reverse) # Apply renames in stages, to minimize conflicts: # Only files whose name or parent change are interesting, because their # target name may exist in the source tree. If a directory's name changes, # that doesn't make its children interesting. (source_entries, target_entries) = get_rename_entries(changeset, inventory, reverse) temp_name = rename_to_temp_delete(source_entries, inventory, dir, conflict_handler, reverse) rename_to_new_create(temp_name, target_entries, inventory, changeset, dir, conflict_handler, reverse) os.rmdir(temp_dir) r_inventory = invert_dict(inventory) new_entries, removed_entries = get_inventory_change(inventory, r_inventory, changeset, reverse) new_inventory = {} for path, file_id in new_entries.iteritems(): new_inventory[file_id] = path for file_id in removed_entries: new_inventory[file_id] = None return new_inventory def apply_changeset_tree(cset, tree, reverse=False): r_inventory = {} for entry in tree.source_inventory().itervalues(): inventory[entry.id] = entry.path new_inventory = apply_changeset(cset, r_inventory, tree.root, reverse=reverse) new_entries, remove_entries = \ get_inventory_change(inventory, new_inventory, cset, reverse) tree.update_source_inventory(new_entries, remove_entries) def get_inventory_change(inventory, new_inventory, cset, reverse=False): new_entries = {} remove_entries = [] r_inventory = invert_dict(inventory) r_new_inventory = invert_dict(new_inventory) for entry in cset.entries.itervalues(): if entry.needs_rename(): old_path = r_inventory.get(entry.id) if old_path is not None: remove_entries.append(old_path) else: new_path = entry.get_new_path(inventory, cset) if new_path is not None: new_entries[new_path] = entry.id return new_entries, remove_entries def print_changeset(cset): """Print all non-boring changeset entries :param cset: The changeset to print :type cset: `Changeset` """ for entry in cset.entries.itervalues(): if entry.is_boring(): continue print entry.id print entry.summarize_name(cset) class CompositionFailure(Exception): def __init__(self, old_entry, new_entry, problem): msg = "Unable to conpose entries.\n %s" % problem Exception.__init__(self, msg) class IDMismatch(CompositionFailure): def __init__(self, old_entry, new_entry): problem = "Attempt to compose entries with different ids: %s and %s" %\ (old_entry.id, new_entry.id) CompositionFailure.__init__(self, old_entry, new_entry, problem) def compose_changesets(old_cset, new_cset): """Combine two changesets into one. This works well for exact patching. Otherwise, not so well. :param old_cset: The first changeset that would be applied :type old_cset: `Changeset` :param new_cset: The second changeset that would be applied :type new_cset: `Changeset` :return: A changeset that combines the changes in both changesets :rtype: `Changeset` """ composed = Changeset() for old_entry in old_cset.entries.itervalues(): new_entry = new_cset.entries.get(old_entry.id) if new_entry is None: composed.add_entry(old_entry) else: composed_entry = compose_entries(old_entry, new_entry) if composed_entry.parent is not None or\ composed_entry.new_parent is not None: composed.add_entry(composed_entry) for new_entry in new_cset.entries.itervalues(): if not old_cset.entries.has_key(new_entry.id): composed.add_entry(new_entry) return composed def compose_entries(old_entry, new_entry): """Combine two entries into one. :param old_entry: The first entry that would be applied :type old_entry: ChangesetEntry :param old_entry: The second entry that would be applied :type old_entry: ChangesetEntry :return: A changeset entry combining both entries :rtype: `ChangesetEntry` """ if old_entry.id != new_entry.id: raise IDMismatch(old_entry, new_entry) output = ChangesetEntry(old_entry.id, old_entry.parent, old_entry.path) if (old_entry.parent != old_entry.new_parent or new_entry.parent != new_entry.new_parent): output.new_parent = new_entry.new_parent if (old_entry.path != old_entry.new_path or new_entry.path != new_entry.new_path): output.new_path = new_entry.new_path output.contents_change = compose_contents(old_entry, new_entry) output.metadata_change = compose_metadata(old_entry, new_entry) return output def compose_contents(old_entry, new_entry): """Combine the contents of two changeset entries. Entries are combined intelligently where possible, but the fallback behavior returns an ApplySequence. :param old_entry: The first entry that would be applied :type old_entry: `ChangesetEntry` :param new_entry: The second entry that would be applied :type new_entry: `ChangesetEntry` :return: A combined contents change :rtype: anything supporting the apply(reverse=False) method """ old_contents = old_entry.contents_change new_contents = new_entry.contents_change if old_entry.contents_change is None: return new_entry.contents_change elif new_entry.contents_change is None: return old_entry.contents_change elif isinstance(old_contents, ReplaceContents) and \ isinstance(new_contents, ReplaceContents): if old_contents.old_contents == new_contents.new_contents: return None else: return ReplaceContents(old_contents.old_contents, new_contents.new_contents) elif isinstance(old_contents, ApplySequence): output = ApplySequence(old_contents.changes) if isinstance(new_contents, ApplySequence): output.changes.extend(new_contents.changes) else: output.changes.append(new_contents) return output elif isinstance(new_contents, ApplySequence): output = ApplySequence((old_contents.changes,)) output.extend(new_contents.changes) return output else: return ApplySequence((old_contents, new_contents)) def compose_metadata(old_entry, new_entry): old_meta = old_entry.metadata_change new_meta = new_entry.metadata_change if old_meta is None: return new_meta elif new_meta is None: return old_meta elif isinstance(old_meta, ChangeUnixPermissions) and \ isinstance(new_meta, ChangeUnixPermissions): return ChangeUnixPermissions(old_meta.old_mode, new_meta.new_mode) else: return ApplySequence(old_meta, new_meta) def changeset_is_null(changeset): for entry in changeset.entries.itervalues(): if not entry.is_boring(): return False return True class UnsuppportedFiletype(Exception): def __init__(self, full_path, stat_result): msg = "The file \"%s\" is not a supported filetype." % full_path Exception.__init__(self, msg) self.full_path = full_path self.stat_result = stat_result def generate_changeset(tree_a, tree_b, inventory_a=None, inventory_b=None): return ChangesetGenerator(tree_a, tree_b, inventory_a, inventory_b)() class ChangesetGenerator(object): def __init__(self, tree_a, tree_b, inventory_a=None, inventory_b=None): object.__init__(self) self.tree_a = tree_a self.tree_b = tree_b if inventory_a is not None: self.inventory_a = inventory_a else: self.inventory_a = tree_a.inventory() if inventory_b is not None: self.inventory_b = inventory_b else: self.inventory_b = tree_b.inventory() self.r_inventory_a = self.reverse_inventory(self.inventory_a) self.r_inventory_b = self.reverse_inventory(self.inventory_b) def reverse_inventory(self, inventory): r_inventory = {} for entry in inventory.itervalues(): if entry.id is None: continue r_inventory[entry.id] = entry return r_inventory def __call__(self): cset = Changeset() for entry in self.inventory_a.itervalues(): if entry.id is None: continue cs_entry = self.make_entry(entry.id) if cs_entry is not None and not cs_entry.is_boring(): cset.add_entry(cs_entry) for entry in self.inventory_b.itervalues(): if entry.id is None: continue if not self.r_inventory_a.has_key(entry.id): cs_entry = self.make_entry(entry.id) if cs_entry is not None and not cs_entry.is_boring(): cset.add_entry(cs_entry) for entry in list(cset.entries.itervalues()): if entry.parent != entry.new_parent: if not cset.entries.has_key(entry.parent) and\ entry.parent != NULL_ID and entry.parent is not None: parent_entry = self.make_boring_entry(entry.parent) cset.add_entry(parent_entry) if not cset.entries.has_key(entry.new_parent) and\ entry.new_parent != NULL_ID and \ entry.new_parent is not None: parent_entry = self.make_boring_entry(entry.new_parent) cset.add_entry(parent_entry) return cset def get_entry_parent(self, entry, inventory): if entry is None: return None if entry.path == "./.": return NULL_ID dirname = os.path.dirname(entry.path) if dirname == ".": dirname = "./." parent = inventory[dirname] return parent.id def get_paths(self, entry, tree): if entry is None: return (None, None) full_path = tree.readonly_path(entry.id) if entry.path == ".": return ("", full_path) return (entry.path, full_path) def make_basic_entry(self, id, only_interesting): entry_a = self.r_inventory_a.get(id) entry_b = self.r_inventory_b.get(id) if only_interesting and not self.is_interesting(entry_a, entry_b): return (None, None, None) parent = self.get_entry_parent(entry_a, self.inventory_a) (path, full_path_a) = self.get_paths(entry_a, self.tree_a) cs_entry = ChangesetEntry(id, parent, path) new_parent = self.get_entry_parent(entry_b, self.inventory_b) (new_path, full_path_b) = self.get_paths(entry_b, self.tree_b) cs_entry.new_path = new_path cs_entry.new_parent = new_parent return (cs_entry, full_path_a, full_path_b) def is_interesting(self, entry_a, entry_b): if entry_a is not None: if entry_a.interesting: return True if entry_b is not None: if entry_b.interesting: return True return False def make_boring_entry(self, id): (cs_entry, full_path_a, full_path_b) = \ self.make_basic_entry(id, only_interesting=False) if cs_entry.is_creation_or_deletion(): return self.make_entry(id, only_interesting=False) else: return cs_entry def make_entry(self, id, only_interesting=True): (cs_entry, full_path_a, full_path_b) = \ self.make_basic_entry(id, only_interesting) if cs_entry is None: return None stat_a = self.lstat(full_path_a) stat_b = self.lstat(full_path_b) if stat_b is None: cs_entry.new_parent = None cs_entry.new_path = None cs_entry.metadata_change = self.make_mode_change(stat_a, stat_b) cs_entry.contents_change = self.make_contents_change(full_path_a, stat_a, full_path_b, stat_b) return cs_entry def make_mode_change(self, stat_a, stat_b): mode_a = None if stat_a is not None and not stat.S_ISLNK(stat_a.st_mode): mode_a = stat_a.st_mode & 0777 mode_b = None if stat_b is not None and not stat.S_ISLNK(stat_b.st_mode): mode_b = stat_b.st_mode & 0777 if mode_a == mode_b: return None return ChangeUnixPermissions(mode_a, mode_b) def make_contents_change(self, full_path_a, stat_a, full_path_b, stat_b): if stat_a is None and stat_b is None: return None if None not in (stat_a, stat_b) and stat.S_ISDIR(stat_a.st_mode) and\ stat.S_ISDIR(stat_b.st_mode): return None if None not in (stat_a, stat_b) and stat.S_ISREG(stat_a.st_mode) and\ stat.S_ISREG(stat_b.st_mode): if stat_a.st_ino == stat_b.st_ino and \ stat_a.st_dev == stat_b.st_dev: return None if file(full_path_a, "rb").read() == \ file(full_path_b, "rb").read(): return None patch_contents = patch.diff(full_path_a, file(full_path_b, "rb").read()) if patch_contents is None: return None return PatchApply(patch_contents) a_contents = self.get_contents(stat_a, full_path_a) b_contents = self.get_contents(stat_b, full_path_b) if a_contents == b_contents: return None return ReplaceContents(a_contents, b_contents) def get_contents(self, stat_result, full_path): if stat_result is None: return None elif stat.S_ISREG(stat_result.st_mode): return FileCreate(file(full_path, "rb").read()) elif stat.S_ISDIR(stat_result.st_mode): return dir_create elif stat.S_ISLNK(stat_result.st_mode): return SymlinkCreate(os.readlink(full_path)) else: raise UnsupportedFiletype(full_path, stat_result) def lstat(self, full_path): stat_result = None if full_path is not None: try: stat_result = os.lstat(full_path) except OSError, e: if e.errno != errno.ENOENT: raise return stat_result def full_path(entry, tree): return os.path.join(tree.root, entry.path) def new_delete_entry(entry, tree, inventory, delete): if entry.path == "": parent = NULL_ID else: parent = inventory[dirname(entry.path)].id cs_entry = ChangesetEntry(parent, entry.path) if delete: cs_entry.new_path = None cs_entry.new_parent = None else: cs_entry.path = None cs_entry.parent = None full_path = full_path(entry, tree) status = os.lstat(full_path) if stat.S_ISDIR(file_stat.st_mode): action = dir_create class Inventory(object): def __init__(self, inventory): self.inventory = inventory self.rinventory = None def get_rinventory(self): if self.rinventory is None: self.rinventory = invert_dict(self.inventory) return self.rinventory def get_path(self, id): return self.inventory.get(id) def get_name(self, id): return os.path.basename(self.get_path(id)) def get_dir(self, id): path = self.get_path(id) if path == "": return None return os.path.dirname(path) def get_parent(self, id): directory = self.get_dir(id) if directory == '.': directory = './.' if directory is None: return NULL_ID return self.get_rinventory().get(directory) M 644 inline bzrlib/commands.py data 36434 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date from bzrlib import merge def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0], lock_mode='r') file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.', lock_mode='r') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0], lock_mode='r') file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.', lock_mode='r') show_diff(b, revision, specific_files=file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False): from bzrlib import show_log, find_branch direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename, lock_mode='r') fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.', lock_mode='r') file_id = None show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=sys.stdout, direction=direction) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures return 1 else: print return 0 class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): merge.merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'forward': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/diff.py data 10253 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from trace import mutter from errors import BzrError def _diff_one(oldlines, newlines, to_file, **kw): import difflib # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') to_file.writelines(ud) if nonl: print >>to_file, "\\ No newline at end of file" print >>to_file def show_diff(b, revision, specific_files): import sys if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() show_diff_trees(old_tree, new_tree, sys.stdout, specific_files) def show_diff_trees(old_tree, new_tree, to_file, specific_files=None): """Show in text form the changes from one tree to another. to_files If set, include only changes to these files. """ # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. delta = compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=specific_files) for path, file_id, kind in delta.removed: print '*** removed %s %r' % (kind, path) if kind == 'file': _diff_one(old_tree.get_file(file_id).readlines(), [], to_file, fromfile=old_label + path, tofile=DEVNULL) for path, file_id, kind in delta.added: print '*** added %s %r' % (kind, path) if kind == 'file': _diff_one([], new_tree.get_file(file_id).readlines(), to_file, fromfile=DEVNULL, tofile=new_label + path) for old_path, new_path, file_id, kind, text_modified in delta.renamed: print '*** renamed %s %r => %r' % (kind, old_path, new_path) if text_modified: _diff_one(old_tree.get_file(file_id).readlines(), new_tree.get_file(file_id).readlines(), to_file, fromfile=old_label + old_path, tofile=new_label + new_path) for path, file_id, kind in delta.modified: print '*** modified %s %r' % (kind, path) if kind == 'file': _diff_one(old_tree.get_file(file_id).readlines(), new_tree.get_file(file_id).readlines(), to_file, fromfile=old_label + path, tofile=new_label + path) class TreeDelta(object): """Describes changes from one tree to another. Contains four lists: added (path, id, kind) removed (path, id, kind) renamed (oldpath, newpath, id, kind, text_modified) modified (path, id, kind) unchanged (path, id, kind) Each id is listed only once. Files that are both modified and renamed are listed only in renamed, with the text_modified flag true. The lists are normally sorted when the delta is created. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] self.unchanged = [] def touches_file_id(self, file_id): """Return True if file_id is modified by this delta.""" for l in self.added, self.removed, self.modified: for v in l: if v[1] == file_id: return True for v in self.renamed: if v[2] == file_id: return True return False def show(self, to_file, show_ids=False, show_unchanged=False): def show_list(files): for path, fid, kind in files: if kind == 'directory': path += '/' elif kind == 'symlink': path += '@' if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if self.removed: print >>to_file, 'removed:' show_list(self.removed) if self.added: print >>to_file, 'added:' show_list(self.added) if self.renamed: print >>to_file, 'renamed:' for oldpath, newpath, fid, kind, text_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if self.modified: print >>to_file, 'modified:' show_list(self.modified) if show_unchanged and self.unchanged: print >>to_file, 'unchanged:' show_list(self.unchanged) def compare_trees(old_tree, new_tree, want_unchanged, specific_files=None): """Describe changes from one tree to another. Returns a TreeDelta with details of added, modified, renamed, and deleted entries. The root entry is specifically exempt. This only considers versioned files. want_unchanged If true, also list files unchanged from one version to the next. specific_files If true, only check for changes to specified names or files within them. """ from osutils import is_inside_any old_inv = old_tree.inventory new_inv = new_tree.inventory delta = TreeDelta() mutter('start compare_trees') # TODO: match for specific files can be rather smarter by finding # the IDs of those files up front and then considering only that. for file_id in old_tree: if file_id in new_tree: kind = old_inv.get_file_kind(file_id) assert kind == new_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ 'invalid file kind %r' % kind if kind == 'root_directory': continue old_path = old_inv.id2path(file_id) new_path = new_inv.id2path(file_id) if specific_files: if (not is_inside_any(specific_files, old_path) and not is_inside_any(specific_files, new_path)): continue if kind == 'file': old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False # TODO: Can possibly avoid calculating path strings if the # two files are unchanged and their names and parents are # the same and the parents are unchanged all the way up. # May not be worthwhile. if old_path != new_path: delta.renamed.append((old_path, new_path, file_id, kind, text_modified)) elif text_modified: delta.modified.append((new_path, file_id, kind)) elif want_unchanged: delta.unchanged.append((new_path, file_id, kind)) else: old_path = old_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, old_path): continue delta.removed.append((old_path, file_id, kind)) mutter('start looking for new files') for file_id in new_inv: if file_id in old_inv: continue new_path = new_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, new_path): continue kind = new_inv.get_file_kind(file_id) delta.added.append((new_path, file_id, kind)) delta.removed.sort() delta.added.sort() delta.renamed.sort() delta.modified.sort() delta.unchanged.sort() return delta M 644 inline bzrlib/merge.py data 8173 from merge_core import merge_flex from changeset import generate_changeset, ExceptionConflictHandler from changeset import Inventory from bzrlib import Branch import bzrlib.osutils from trace import mutter import os.path import tempfile import shutil import errno class MergeConflictHandler(ExceptionConflictHandler): """Handle conflicts encountered while merging""" def copy(self, source, dest): """Copy the text and mode of a file :param source: The path of the file to copy :param dest: The distination file to create """ s_file = file(source, "rb") d_file = file(dest, "wb") for line in s_file: d_file.write(line) os.chmod(dest, 0777 & os.stat(source).st_mode) def add_suffix(self, name, suffix, last_new_name=None): """Rename a file to append a suffix. If the new name exists, the suffix is added repeatedly until a non-existant name is found :param name: The path of the file :param suffix: The suffix to append :param last_new_name: (used for recursive calls) the last name tried """ if last_new_name is None: last_new_name = name new_name = last_new_name+suffix try: os.rename(name, new_name) except OSError, e: if e.errno != errno.EEXIST and e.errno != errno.ENOTEMPTY: raise self.add_suffix(name, suffix, last_new_name=new_name) def merge_conflict(self, new_file, this_path, base_path, other_path): """ Handle diff3 conflicts by producing a .THIS, .BASE and .OTHER. The main file will be a version with diff3 conflicts. :param new_file: Path to the output file with diff3 markers :param this_path: Path to the file text for the THIS tree :param base_path: Path to the file text for the BASE tree :param other_path: Path to the file text for the OTHER tree """ self.add_suffix(this_path, ".THIS") self.copy(base_path, this_path+".BASE") self.copy(other_path, this_path+".OTHER") os.rename(new_file, this_path) def target_exists(self, entry, target, old_path): """Handle the case when the target file or dir exists""" self.add_suffix(target, ".moved") class SourceFile(object): def __init__(self, path, id, present=None, isdir=None): self.path = path self.id = id self.present = present self.isdir = isdir self.interesting = True def __repr__(self): return "SourceFile(%s, %s)" % (self.path, self.id) def get_tree(treespec, temp_root, label): dir, revno = treespec branch = Branch(dir) if revno is None: base_tree = branch.working_tree() elif revno == -1: base_tree = branch.basis_tree() else: base_tree = branch.revision_tree(branch.lookup_revision(revno)) temp_path = os.path.join(temp_root, label) os.mkdir(temp_path) return MergeTree(base_tree, temp_path) def abspath(tree, file_id): path = tree.inventory.id2path(file_id) if path == "": return "./." return "./" + path def file_exists(tree, file_id): return tree.has_filename(tree.id2path(file_id)) def inventory_map(tree): inventory = {} for file_id in tree.inventory: if not file_exists(tree, file_id): continue path = abspath(tree, file_id) inventory[path] = SourceFile(path, file_id) return inventory class MergeTree(object): def __init__(self, tree, tempdir): object.__init__(self) if hasattr(tree, "basedir"): self.root = tree.basedir else: self.root = None self.inventory = inventory_map(tree) self.tree = tree self.tempdir = tempdir os.mkdir(os.path.join(self.tempdir, "texts")) self.cached = {} def readonly_path(self, id): if self.root is not None: return self.tree.abspath(self.tree.id2path(id)) else: if self.tree.inventory[id].kind in ("directory", "root_directory"): return self.tempdir if not self.cached.has_key(id): path = os.path.join(self.tempdir, "texts", id) outfile = file(path, "wb") outfile.write(self.tree.get_file(id).read()) assert(os.path.exists(path)) self.cached[id] = path return self.cached[id] def merge(other_revision, base_revision): tempdir = tempfile.mkdtemp(prefix="bzr-") try: this_branch = Branch('.') other_tree = get_tree(other_revision, tempdir, "other") base_tree = get_tree(base_revision, tempdir, "base") merge_inner(this_branch, other_tree, base_tree, tempdir) finally: shutil.rmtree(tempdir) def generate_cset_optimized(tree_a, tree_b, inventory_a, inventory_b): """Generate a changeset, using the text_id to mark really-changed files. This permits blazing comparisons when text_ids are present. It also disables metadata comparison for files with identical texts. """ for file_id in tree_a.tree.inventory: if file_id not in tree_b.tree.inventory: continue entry_a = tree_a.tree.inventory[file_id] entry_b = tree_b.tree.inventory[file_id] if (entry_a.kind, entry_b.kind) != ("file", "file"): continue if None in (entry_a.text_id, entry_b.text_id): continue if entry_a.text_id != entry_b.text_id: continue inventory_a[abspath(tree_a.tree, file_id)].interesting = False inventory_b[abspath(tree_b.tree, file_id)].interesting = False cset = generate_changeset(tree_a, tree_b, inventory_a, inventory_b) for entry in cset.entries.itervalues(): entry.metadata_change = None return cset def merge_inner(this_branch, other_tree, base_tree, tempdir): this_tree = get_tree(('.', None), tempdir, "this") def get_inventory(tree): return tree.inventory inv_changes = merge_flex(this_tree, base_tree, other_tree, generate_cset_optimized, get_inventory, MergeConflictHandler(base_tree.root)) adjust_ids = [] for id, path in inv_changes.iteritems(): if path is not None: if path == '.': path = '' else: assert path.startswith('./') path = path[2:] adjust_ids.append((path, id)) this_branch.set_inventory(regen_inventory(this_branch, this_tree.root, adjust_ids)) def regen_inventory(this_branch, root, new_entries): old_entries = this_branch.read_working_inventory() new_inventory = {} by_path = {} for file_id in old_entries: entry = old_entries[file_id] path = old_entries.id2path(file_id) new_inventory[file_id] = (path, file_id, entry.parent_id, entry.kind) by_path[path] = file_id deletions = 0 insertions = 0 new_path_list = [] for path, file_id in new_entries: if path is None: del new_inventory[file_id] deletions += 1 else: new_path_list.append((path, file_id)) if file_id not in old_entries: insertions += 1 # Ensure no file is added before its parent new_path_list.sort() for path, file_id in new_path_list: if path == '': parent = None else: parent = by_path[os.path.dirname(path)] kind = bzrlib.osutils.file_kind(os.path.join(root, path)) new_inventory[file_id] = (path, file_id, parent, kind) by_path[path] = file_id # Get a list in insertion order new_inventory_list = new_inventory.values() mutter ("""Inventory regeneration: old length: %i insertions: %i deletions: %i new_length: %i"""\ % (len(old_entries), insertions, deletions, len(new_inventory_list))) assert len(new_inventory_list) == len(old_entries) + insertions - deletions new_inventory_list.sort() return new_inventory_list M 644 inline bzrlib/merge_core.py data 19507 import changeset from changeset import Inventory, apply_changeset, invert_dict import os.path class ThreewayInventory(object): def __init__(self, this_inventory, base_inventory, other_inventory): self.this = this_inventory self.base = base_inventory self.other = other_inventory def invert_invent(inventory): invert_invent = {} for key, value in inventory.iteritems(): invert_invent[value.id] = key return invert_invent def make_inv(inventory): return Inventory(invert_invent(inventory)) def merge_flex(this, base, other, changeset_function, inventory_function, conflict_handler): this_inventory = inventory_function(this) base_inventory = inventory_function(base) other_inventory = inventory_function(other) inventory = ThreewayInventory(make_inv(this_inventory), make_inv(base_inventory), make_inv(other_inventory)) cset = changeset_function(base, other, base_inventory, other_inventory) new_cset = make_merge_changeset(cset, inventory, this, base, other, conflict_handler) return apply_changeset(new_cset, invert_invent(this_inventory), this.root, conflict_handler, False) def make_merge_changeset(cset, inventory, this, base, other, conflict_handler=None): new_cset = changeset.Changeset() def get_this_contents(id): path = os.path.join(this.root, inventory.this.get_path(id)) if os.path.isdir(path): return changeset.dir_create else: return changeset.FileCreate(file(path, "rb").read()) for entry in cset.entries.itervalues(): if entry.is_boring(): new_cset.add_entry(entry) elif entry.is_creation(False): if inventory.this.get_path(entry.id) is None: new_cset.add_entry(entry) else: this_contents = get_this_contents(entry.id) other_contents = entry.contents_change.new_contents if other_contents == this_contents: boring_entry = changeset.ChangesetEntry(entry.id, entry.new_parent, entry.new_path) new_cset.add_entry(boring_entry) else: conflict_handler.contents_conflict(this_contents, other_contents) elif entry.is_deletion(False): if inventory.this.get_path(entry.id) is None: boring_entry = changeset.ChangesetEntry(entry.id, entry.parent, entry.path) new_cset.add_entry(boring_entry) elif entry.contents_change is not None: this_contents = get_this_contents(entry.id) base_contents = entry.contents_change.old_contents if base_contents == this_contents: new_cset.add_entry(entry) else: entry_path = inventory.this.get_path(entry.id) conflict_handler.rem_contents_conflict(entry_path, this_contents, base_contents) else: new_cset.add_entry(entry) else: entry = get_merge_entry(entry, inventory, base, other, conflict_handler) if entry is not None: new_cset.add_entry(entry) return new_cset def get_merge_entry(entry, inventory, base, other, conflict_handler): this_name = inventory.this.get_name(entry.id) this_parent = inventory.this.get_parent(entry.id) this_dir = inventory.this.get_dir(entry.id) if this_dir is None: this_dir = "" if this_name is None: return conflict_handler.merge_missing(entry.id, inventory) base_name = inventory.base.get_name(entry.id) base_parent = inventory.base.get_parent(entry.id) base_dir = inventory.base.get_dir(entry.id) if base_dir is None: base_dir = "" other_name = inventory.other.get_name(entry.id) other_parent = inventory.other.get_parent(entry.id) other_dir = inventory.base.get_dir(entry.id) if other_dir is None: other_dir = "" if base_name == other_name: old_name = this_name new_name = this_name else: if this_name != base_name and this_name != other_name: conflict_handler.rename_conflict(entry.id, this_name, base_name, other_name) else: old_name = this_name new_name = other_name if base_parent == other_parent: old_parent = this_parent new_parent = this_parent old_dir = this_dir new_dir = this_dir else: if this_parent != base_parent and this_parent != other_parent: conflict_handler.move_conflict(entry.id, inventory) else: old_parent = this_parent old_dir = this_dir new_parent = other_parent new_dir = other_dir old_path = os.path.join(old_dir, old_name) new_entry = changeset.ChangesetEntry(entry.id, old_parent, old_name) if new_name is not None or new_parent is not None: new_entry.new_path = os.path.join(new_dir, new_name) else: new_entry.new_path = None new_entry.new_parent = new_parent base_path = base.readonly_path(entry.id) other_path = other.readonly_path(entry.id) if entry.contents_change is not None: new_entry.contents_change = changeset.Diff3Merge(base_path, other_path) if entry.metadata_change is not None: new_entry.metadata_change = PermissionsMerge(base_path, other_path) return new_entry class PermissionsMerge(object): def __init__(self, base_path, other_path): self.base_path = base_path self.other_path = other_path def apply(self, filename, conflict_handler, reverse=False): if not reverse: base = self.base_path other = self.other_path else: base = self.other_path other = self.base_path base_stat = os.stat(base).st_mode other_stat = os.stat(other).st_mode this_stat = os.stat(filename).st_mode if base_stat &0777 == other_stat &0777: return elif this_stat &0777 == other_stat &0777: return elif this_stat &0777 == base_stat &0777: os.chmod(filename, other_stat) else: conflict_handler.permission_conflict(filename, base, other) import unittest import tempfile import shutil class MergeTree(object): def __init__(self, dir): self.dir = dir; os.mkdir(dir) self.inventory = {'0': ""} def child_path(self, parent, name): return os.path.join(self.inventory[parent], name) def add_file(self, id, parent, name, contents, mode): path = self.child_path(parent, name) full_path = self.abs_path(path) assert not os.path.exists(full_path) file(full_path, "wb").write(contents) os.chmod(self.abs_path(path), mode) self.inventory[id] = path def add_dir(self, id, parent, name, mode): path = self.child_path(parent, name) full_path = self.abs_path(path) assert not os.path.exists(full_path) os.mkdir(self.abs_path(path)) os.chmod(self.abs_path(path), mode) self.inventory[id] = path def abs_path(self, path): return os.path.join(self.dir, path) def full_path(self, id): return self.abs_path(self.inventory[id]) def change_path(self, id, path): new = os.path.join(self.dir, self.inventory[id]) os.rename(self.abs_path(self.inventory[id]), self.abs_path(path)) self.inventory[id] = path class MergeBuilder(object): def __init__(self): self.dir = tempfile.mkdtemp(prefix="BaZing") self.base = MergeTree(os.path.join(self.dir, "base")) self.this = MergeTree(os.path.join(self.dir, "this")) self.other = MergeTree(os.path.join(self.dir, "other")) self.cset = changeset.Changeset() self.cset.add_entry(changeset.ChangesetEntry("0", changeset.NULL_ID, "./.")) def get_cset_path(self, parent, name): if name is None: assert (parent is None) return None return os.path.join(self.cset.entries[parent].path, name) def add_file(self, id, parent, name, contents, mode): self.base.add_file(id, parent, name, contents, mode) self.this.add_file(id, parent, name, contents, mode) self.other.add_file(id, parent, name, contents, mode) path = self.get_cset_path(parent, name) self.cset.add_entry(changeset.ChangesetEntry(id, parent, path)) def add_dir(self, id, parent, name, mode): path = self.get_cset_path(parent, name) self.base.add_dir(id, parent, name, mode) self.cset.add_entry(changeset.ChangesetEntry(id, parent, path)) self.this.add_dir(id, parent, name, mode) self.other.add_dir(id, parent, name, mode) def change_name(self, id, base=None, this=None, other=None): if base is not None: self.change_name_tree(id, self.base, base) self.cset.entries[id].name = base if this is not None: self.change_name_tree(id, self.this, this) if other is not None: self.change_name_tree(id, self.other, other) self.cset.entries[id].new_name = other def change_parent(self, id, base=None, this=None, other=None): if base is not None: self.change_parent_tree(id, self.base, base) self.cset.entries[id].parent = base self.cset.entries[id].dir = self.cset.entries[base].path if this is not None: self.change_parent_tree(id, self.this, this) if other is not None: self.change_parent_tree(id, self.other, other) self.cset.entries[id].new_parent = other self.cset.entries[id].new_dir = \ self.cset.entries[other].new_path def change_contents(self, id, base=None, this=None, other=None): if base is not None: self.change_contents_tree(id, self.base, base) if this is not None: self.change_contents_tree(id, self.this, this) if other is not None: self.change_contents_tree(id, self.other, other) if base is not None or other is not None: old_contents = file(self.base.full_path(id)).read() new_contents = file(self.other.full_path(id)).read() contents = changeset.ReplaceFileContents(old_contents, new_contents) self.cset.entries[id].contents_change = contents def change_perms(self, id, base=None, this=None, other=None): if base is not None: self.change_perms_tree(id, self.base, base) if this is not None: self.change_perms_tree(id, self.this, this) if other is not None: self.change_perms_tree(id, self.other, other) if base is not None or other is not None: old_perms = os.stat(self.base.full_path(id)).st_mode &077 new_perms = os.stat(self.other.full_path(id)).st_mode &077 contents = changeset.ChangeUnixPermissions(old_perms, new_perms) self.cset.entries[id].metadata_change = contents def change_name_tree(self, id, tree, name): new_path = tree.child_path(self.cset.entries[id].parent, name) tree.change_path(id, new_path) def change_parent_tree(self, id, tree, parent): new_path = tree.child_path(parent, self.cset.entries[id].name) tree.change_path(id, new_path) def change_contents_tree(self, id, tree, contents): path = tree.full_path(id) mode = os.stat(path).st_mode file(path, "w").write(contents) os.chmod(path, mode) def change_perms_tree(self, id, tree, mode): os.chmod(tree.full_path(id), mode) def merge_changeset(self): all_inventory = ThreewayInventory(Inventory(self.this.inventory), Inventory(self.base.inventory), Inventory(self.other.inventory)) conflict_handler = changeset.ExceptionConflictHandler(self.this.dir) return make_merge_changeset(self.cset, all_inventory, self.this.dir, self.base.dir, self.other.dir, conflict_handler) def apply_changeset(self, cset, conflict_handler=None, reverse=False): self.this.inventory = \ changeset.apply_changeset(cset, self.this.inventory, self.this.dir, conflict_handler, reverse) def cleanup(self): shutil.rmtree(self.dir) class MergeTest(unittest.TestCase): def test_change_name(self): """Test renames""" builder = MergeBuilder() builder.add_file("1", "0", "name1", "hello1", 0755) builder.change_name("1", other="name2") builder.add_file("2", "0", "name3", "hello2", 0755) builder.change_name("2", base="name4") builder.add_file("3", "0", "name5", "hello3", 0755) builder.change_name("3", this="name6") cset = builder.merge_changeset() assert(cset.entries["2"].is_boring()) assert(cset.entries["1"].name == "name1") assert(cset.entries["1"].new_name == "name2") assert(cset.entries["3"].is_boring()) for tree in (builder.this, builder.other, builder.base): assert(tree.dir != builder.dir and tree.dir.startswith(builder.dir)) for path in tree.inventory.itervalues(): fullpath = tree.abs_path(path) assert(fullpath.startswith(tree.dir)) assert(not path.startswith(tree.dir)) assert os.path.exists(fullpath) builder.apply_changeset(cset) builder.cleanup() builder = MergeBuilder() builder.add_file("1", "0", "name1", "hello1", 0644) builder.change_name("1", other="name2", this="name3") self.assertRaises(changeset.RenameConflict, builder.merge_changeset) builder.cleanup() def test_file_moves(self): """Test moves""" builder = MergeBuilder() builder.add_dir("1", "0", "dir1", 0755) builder.add_dir("2", "0", "dir2", 0755) builder.add_file("3", "1", "file1", "hello1", 0644) builder.add_file("4", "1", "file2", "hello2", 0644) builder.add_file("5", "1", "file3", "hello3", 0644) builder.change_parent("3", other="2") assert(Inventory(builder.other.inventory).get_parent("3") == "2") builder.change_parent("4", this="2") assert(Inventory(builder.this.inventory).get_parent("4") == "2") builder.change_parent("5", base="2") assert(Inventory(builder.base.inventory).get_parent("5") == "2") cset = builder.merge_changeset() for id in ("1", "2", "4", "5"): assert(cset.entries[id].is_boring()) assert(cset.entries["3"].parent == "1") assert(cset.entries["3"].new_parent == "2") builder.apply_changeset(cset) builder.cleanup() builder = MergeBuilder() builder.add_dir("1", "0", "dir1", 0755) builder.add_dir("2", "0", "dir2", 0755) builder.add_dir("3", "0", "dir3", 0755) builder.add_file("4", "1", "file1", "hello1", 0644) builder.change_parent("4", other="2", this="3") self.assertRaises(changeset.MoveConflict, builder.merge_changeset) builder.cleanup() def test_contents_merge(self): """Test diff3 merging""" builder = MergeBuilder() builder.add_file("1", "0", "name1", "text1", 0755) builder.change_contents("1", other="text4") builder.add_file("2", "0", "name3", "text2", 0655) builder.change_contents("2", base="text5") builder.add_file("3", "0", "name5", "text3", 0744) builder.change_contents("3", this="text6") cset = builder.merge_changeset() assert(cset.entries["1"].contents_change is not None) assert(isinstance(cset.entries["1"].contents_change, changeset.Diff3Merge)) assert(isinstance(cset.entries["2"].contents_change, changeset.Diff3Merge)) assert(cset.entries["3"].is_boring()) builder.apply_changeset(cset) assert(file(builder.this.full_path("1"), "rb").read() == "text4" ) assert(file(builder.this.full_path("2"), "rb").read() == "text2" ) assert(os.stat(builder.this.full_path("1")).st_mode &0777 == 0755) assert(os.stat(builder.this.full_path("2")).st_mode &0777 == 0655) assert(os.stat(builder.this.full_path("3")).st_mode &0777 == 0744) builder.cleanup() builder = MergeBuilder() builder.add_file("1", "0", "name1", "text1", 0755) builder.change_contents("1", other="text4", this="text3") cset = builder.merge_changeset() self.assertRaises(changeset.MergeConflict, builder.apply_changeset, cset) builder.cleanup() def test_perms_merge(self): builder = MergeBuilder() builder.add_file("1", "0", "name1", "text1", 0755) builder.change_perms("1", other=0655) builder.add_file("2", "0", "name2", "text2", 0755) builder.change_perms("2", base=0655) builder.add_file("3", "0", "name3", "text3", 0755) builder.change_perms("3", this=0655) cset = builder.merge_changeset() assert(cset.entries["1"].metadata_change is not None) assert(isinstance(cset.entries["1"].metadata_change, PermissionsMerge)) assert(isinstance(cset.entries["2"].metadata_change, PermissionsMerge)) assert(cset.entries["3"].is_boring()) builder.apply_changeset(cset) assert(os.stat(builder.this.full_path("1")).st_mode &0777 == 0655) assert(os.stat(builder.this.full_path("2")).st_mode &0777 == 0755) assert(os.stat(builder.this.full_path("3")).st_mode &0777 == 0655) builder.cleanup(); builder = MergeBuilder() builder.add_file("1", "0", "name1", "text1", 0755) builder.change_perms("1", other=0655, base=0555) cset = builder.merge_changeset() self.assertRaises(changeset.MergePermissionConflict, builder.apply_changeset, cset) builder.cleanup() def test(): changeset_suite = unittest.makeSuite(MergeTest, 'test_') runner = unittest.TextTestRunner() runner.run(changeset_suite) if __name__ == "__main__": test() M 644 inline bzrlib/remotebranch.py data 6893 #! /usr/bin/env python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Proxy object for access to remote branches. At the moment remote branches are only for HTTP and only for read access. """ import gzip from cStringIO import StringIO import urllib2 from errors import BzrError, BzrCheckError from branch import Branch, BZR_BRANCH_FORMAT from trace import mutter # velocitynet.com.au transparently proxies connections and thereby # breaks keep-alive -- sucks! ENABLE_URLGRABBER = True if ENABLE_URLGRABBER: import urlgrabber import urlgrabber.keepalive urlgrabber.keepalive.DEBUG = 0 def get_url(path, compressed=False): try: url = path if compressed: url += '.gz' mutter("grab url %s" % url) url_f = urlgrabber.urlopen(url, keepalive=1, close_connection=0) if not compressed: return url_f else: return gzip.GzipFile(fileobj=StringIO(url_f.read())) except urllib2.URLError, e: raise BzrError("remote fetch failed: %r: %s" % (url, e)) else: def get_url(url, compressed=False): import urllib2 if compressed: url += '.gz' mutter("get_url %s" % url) url_f = urllib2.urlopen(url) if compressed: return gzip.GzipFile(fileobj=StringIO(url_f.read())) else: return url_f def _find_remote_root(url): """Return the prefix URL that corresponds to the branch root.""" orig_url = url while True: try: ff = get_url(url + '/.bzr/branch-format') fmt = ff.read() ff.close() fmt = fmt.rstrip('\r\n') if fmt != BZR_BRANCH_FORMAT.rstrip('\r\n'): raise BzrError("sorry, branch format %r not supported at url %s" % (fmt, url)) return url except urllib2.URLError: pass try: idx = url.rindex('/') except ValueError: raise BzrError('no branch root found for URL %s' % orig_url) url = url[:idx] class RemoteBranch(Branch): def __init__(self, baseurl, find_root=True, lock_mode='r'): """Create new proxy for a remote branch.""" if lock_mode not in ('', 'r'): raise BzrError('lock mode %r is not supported for remote branches' % lock_mode) if find_root: self.baseurl = _find_remote_root(baseurl) else: self.baseurl = baseurl self._check_format() self.inventory_store = RemoteStore(baseurl + '/.bzr/inventory-store/') self.text_store = RemoteStore(baseurl + '/.bzr/text-store/') def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.baseurl) __repr__ = __str__ def controlfile(self, filename, mode): if mode not in ('rb', 'rt', 'r'): raise BzrError("file mode %r not supported for remote branches" % mode) return get_url(self.baseurl + '/.bzr/' + filename, False) def _need_readlock(self): # remote branch always safe for read pass def _need_writelock(self): raise BzrError("cannot get write lock on HTTP remote branch") def relpath(self, path): if not path.startswith(self.baseurl): raise BzrError('path %r is not under base URL %r' % (path, self.baseurl)) pl = len(self.baseurl) return path[pl:].lstrip('/') def get_revision(self, revision_id): from revision import Revision revf = get_url(self.baseurl + '/.bzr/revision-store/' + revision_id, True) r = Revision.read_xml(revf) if r.revision_id != revision_id: raise BzrCheckError('revision stored as {%s} actually contains {%s}' % (revision_id, r.revision_id)) return r class RemoteStore(object): def __init__(self, baseurl): self._baseurl = baseurl def _path(self, name): if '/' in name: raise ValueError('invalid store id', name) return self._baseurl + '/' + name def __getitem__(self, fileid): p = self._path(fileid) return get_url(p, compressed=True) def simple_walk(): """For experimental purposes, traverse many parts of a remote branch""" from revision import Revision from branch import Branch from inventory import Inventory got_invs = {} got_texts = {} print 'read history' history = get_url('/.bzr/revision-history').readlines() num_revs = len(history) for i, rev_id in enumerate(history): rev_id = rev_id.rstrip() print 'read revision %d/%d' % (i, num_revs) # python gzip needs a seekable file (!!) but the HTTP response # isn't, so we need to buffer it rev_f = get_url('/.bzr/revision-store/%s' % rev_id, compressed=True) rev = Revision.read_xml(rev_f) print rev.message inv_id = rev.inventory_id if inv_id not in got_invs: print 'get inventory %s' % inv_id inv_f = get_url('/.bzr/inventory-store/%s' % inv_id, compressed=True) inv = Inventory.read_xml(inv_f) print '%4d inventory entries' % len(inv) for path, ie in inv.iter_entries(): text_id = ie.text_id if text_id == None: continue if text_id in got_texts: continue print ' fetch %s text {%s}' % (path, text_id) text_f = get_url('/.bzr/text-store/%s' % text_id, compressed=True) got_texts[text_id] = True got_invs.add[inv_id] = True print '----' def try_me(): BASE_URL = 'http://bazaar-ng.org/bzr/bzr.dev/' b = RemoteBranch(BASE_URL) ## print '\n'.join(b.revision_history()) from log import show_log show_log(b) if __name__ == '__main__': try_me() M 644 inline bzrlib/revfile.py data 15771 #! /usr/bin/env python # (C) 2005 Canonical Ltd # based on an idea by Matt Mackall # modified to squish into bzr by Martin Pool # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Packed file revision storage. A Revfile holds the text history of a particular source file, such as Makefile. It can represent a tree of text versions for that file, allowing for microbranches within a single repository. This is stored on disk as two files: an index file, and a data file. The index file is short and always read completely into memory; the data file is much longer and only the relevant bits of it, identified by the index file, need to be read. Each text version is identified by the SHA-1 of the full text of that version. It also has a sequence number within the file. The index file has a short header and then a sequence of fixed-length records: * byte[20] SHA-1 of text (as binary, not hex) * uint32 sequence number this is based on, or -1 for full text * uint32 flags: 1=zlib compressed * uint32 offset in text file of start * uint32 length of compressed delta in text file * uint32[3] reserved total 48 bytes. The header is also 48 bytes for tidyness and easy calculation. Both the index and the text are only ever appended to; a consequence is that sequence numbers are stable references. But not every repository in the world will assign the same sequence numbers, therefore the SHA-1 is the only universally unique reference. The iter method here will generally read through the whole index file in one go. With readahead in the kernel and python/libc (typically 128kB) this means that there should be no seeks and often only one read() call to get everything into memory. """ # TODO: Something like pread() would make this slightly simpler and # perhaps more efficient. # TODO: Could also try to mmap things... Might be faster for the # index in particular? # TODO: Some kind of faster lookup of SHAs? The bad thing is that probably means # rewriting existing records, which is not so nice. # TODO: Something to check that regions identified in the index file # completely butt up and do not overlap. Strictly it's not a problem # if there are gaps and that can happen if we're interrupted while # writing to the datafile. Overlapping would be very bad though. import sys, zlib, struct, mdiff, stat, os, sha from binascii import hexlify, unhexlify factor = 10 _RECORDSIZE = 48 _HEADER = "bzr revfile v1\n" _HEADER = _HEADER + ('\xff' * (_RECORDSIZE - len(_HEADER))) _NO_RECORD = 0xFFFFFFFFL # fields in the index record I_SHA = 0 I_BASE = 1 I_FLAGS = 2 I_OFFSET = 3 I_LEN = 4 FL_GZIP = 1 # maximum number of patches in a row before recording a whole text. CHAIN_LIMIT = 50 class RevfileError(Exception): pass class LimitHitException(Exception): pass class Revfile(object): def __init__(self, basename, mode): # TODO: Lock file while open # TODO: advise of random access self.basename = basename if mode not in ['r', 'w']: raise RevfileError("invalid open mode %r" % mode) self.mode = mode idxname = basename + '.irev' dataname = basename + '.drev' idx_exists = os.path.exists(idxname) data_exists = os.path.exists(dataname) if idx_exists != data_exists: raise RevfileError("half-assed revfile") if not idx_exists: if mode == 'r': raise RevfileError("Revfile %r does not exist" % basename) self.idxfile = open(idxname, 'w+b') self.datafile = open(dataname, 'w+b') print 'init empty file' self.idxfile.write(_HEADER) self.idxfile.flush() else: if mode == 'r': diskmode = 'rb' else: diskmode = 'r+b' self.idxfile = open(idxname, diskmode) self.datafile = open(dataname, diskmode) h = self.idxfile.read(_RECORDSIZE) if h != _HEADER: raise RevfileError("bad header %r in index of %r" % (h, self.basename)) def _check_index(self, idx): if idx < 0 or idx > len(self): raise RevfileError("invalid index %r" % idx) def _check_write(self): if self.mode != 'w': raise RevfileError("%r is open readonly" % self.basename) def find_sha(self, s): assert isinstance(s, str) assert len(s) == 20 for idx, idxrec in enumerate(self): if idxrec[I_SHA] == s: return idx else: return _NO_RECORD def _add_compressed(self, text_sha, data, base, compress): # well, maybe compress flags = 0 if compress: data_len = len(data) if data_len > 50: # don't do compression if it's too small; it's unlikely to win # enough to be worthwhile compr_data = zlib.compress(data) compr_len = len(compr_data) if compr_len < data_len: data = compr_data flags = FL_GZIP ##print '- compressed %d -> %d, %.1f%%' \ ## % (data_len, compr_len, float(compr_len)/float(data_len) * 100.0) return self._add_raw(text_sha, data, base, flags) def _add_raw(self, text_sha, data, base, flags): """Add pre-processed data, can be either full text or delta. This does the compression if that makes sense.""" idx = len(self) self.datafile.seek(0, 2) # to end self.idxfile.seek(0, 2) assert self.idxfile.tell() == _RECORDSIZE * (idx + 1) data_offset = self.datafile.tell() assert isinstance(data, str) # not unicode or anything weird self.datafile.write(data) self.datafile.flush() assert isinstance(text_sha, str) entry = text_sha entry += struct.pack(">IIII12x", base, flags, data_offset, len(data)) assert len(entry) == _RECORDSIZE self.idxfile.write(entry) self.idxfile.flush() return idx def _add_full_text(self, text, text_sha, compress): """Add a full text to the file. This is not compressed against any reference version. Returns the index for that text.""" return self._add_compressed(text_sha, text, _NO_RECORD, compress) def _add_delta(self, text, text_sha, base, compress): """Add a text stored relative to a previous text.""" self._check_index(base) try: base_text = self.get(base, recursion_limit=CHAIN_LIMIT) except LimitHitException: return self._add_full_text(text, text_sha, compress) data = mdiff.bdiff(base_text, text) # If the delta is larger than the text, we might as well just # store the text. (OK, the delta might be more compressible, # but the overhead of applying it probably still makes it # bad, and I don't want to compress both of them to find out.) if len(data) >= len(text): return self._add_full_text(text, text_sha, compress) else: return self._add_compressed(text_sha, data, base, compress) def add(self, text, base=_NO_RECORD, compress=True): """Add a new text to the revfile. If the text is already present them its existing id is returned and the file is not changed. If compress is true then gzip compression will be used if it reduces the size. If a base index is specified, that text *may* be used for delta compression of the new text. Delta compression will only be used if it would be a size win and if the existing base is not at too long of a delta chain already. """ self._check_write() text_sha = sha.new(text).digest() idx = self.find_sha(text_sha) if idx != _NO_RECORD: # TODO: Optional paranoid mode where we read out that record and make sure # it's the same, in case someone ever breaks SHA-1. return idx # already present if base == _NO_RECORD: return self._add_full_text(text, text_sha, compress) else: return self._add_delta(text, text_sha, base, compress) def get(self, idx, recursion_limit=None): """Retrieve text of a previous revision. If recursion_limit is an integer then walk back at most that many revisions and then raise LimitHitException, indicating that we ought to record a new file text instead of another delta. Don't use this when trying to get out an existing revision.""" idxrec = self[idx] base = idxrec[I_BASE] if base == _NO_RECORD: text = self._get_full_text(idx, idxrec) else: text = self._get_patched(idx, idxrec, recursion_limit) if sha.new(text).digest() != idxrec[I_SHA]: raise RevfileError("corrupt SHA-1 digest on record %d" % idx) return text def _get_raw(self, idx, idxrec): flags = idxrec[I_FLAGS] if flags & ~FL_GZIP: raise RevfileError("unsupported index flags %#x on index %d" % (flags, idx)) l = idxrec[I_LEN] if l == 0: return '' self.datafile.seek(idxrec[I_OFFSET]) data = self.datafile.read(l) if len(data) != l: raise RevfileError("short read %d of %d " "getting text for record %d in %r" % (len(data), l, idx, self.basename)) if flags & FL_GZIP: data = zlib.decompress(data) return data def _get_full_text(self, idx, idxrec): assert idxrec[I_BASE] == _NO_RECORD text = self._get_raw(idx, idxrec) return text def _get_patched(self, idx, idxrec, recursion_limit): base = idxrec[I_BASE] assert base >= 0 assert base < idx # no loops! if recursion_limit == None: sub_limit = None else: sub_limit = recursion_limit - 1 if sub_limit < 0: raise LimitHitException() base_text = self.get(base, sub_limit) patch = self._get_raw(idx, idxrec) text = mdiff.bpatch(base_text, patch) return text def __len__(self): """Return number of revisions.""" l = os.fstat(self.idxfile.fileno())[stat.ST_SIZE] if l % _RECORDSIZE: raise RevfileError("bad length %d on index of %r" % (l, self.basename)) if l < _RECORDSIZE: raise RevfileError("no header present in index of %r" % (self.basename)) return int(l / _RECORDSIZE) - 1 def __getitem__(self, idx): """Index by sequence id returns the index field""" ## TODO: Can avoid seek if we just moved there... self._seek_index(idx) idxrec = self._read_next_index() if idxrec == None: raise IndexError() else: return idxrec def _seek_index(self, idx): if idx < 0: raise RevfileError("invalid index %r" % idx) self.idxfile.seek((idx + 1) * _RECORDSIZE) def __iter__(self): """Read back all index records. Do not seek the index file while this is underway!""" sys.stderr.write(" ** iter called ** \n") self._seek_index(0) while True: idxrec = self._read_next_index() if not idxrec: break yield idxrec def _read_next_index(self): rec = self.idxfile.read(_RECORDSIZE) if not rec: return None elif len(rec) != _RECORDSIZE: raise RevfileError("short read of %d bytes getting index %d from %r" % (len(rec), idx, self.basename)) return struct.unpack(">20sIIII12x", rec) def dump(self, f=sys.stdout): f.write('%-8s %-40s %-8s %-8s %-8s %-8s\n' % tuple('idx sha1 base flags offset len'.split())) f.write('-------- ---------------------------------------- ') f.write('-------- -------- -------- --------\n') for i, rec in enumerate(self): f.write("#%-7d %40s " % (i, hexlify(rec[0]))) if rec[1] == _NO_RECORD: f.write("(none) ") else: f.write("#%-7d " % rec[1]) f.write("%8x %8d %8d\n" % (rec[2], rec[3], rec[4])) def total_text_size(self): """Return the sum of sizes of all file texts. This is how much space they would occupy if they were stored without delta and gzip compression. As a side effect this completely validates the Revfile, checking that all texts can be reproduced with the correct SHA-1.""" t = 0L for idx in range(len(self)): t += len(self.get(idx)) return t def main(argv): try: cmd = argv[1] except IndexError: sys.stderr.write("usage: revfile dump\n" " revfile add\n" " revfile add-delta BASE\n" " revfile get IDX\n" " revfile find-sha HEX\n" " revfile total-text-size\n" " revfile last\n") return 1 def rw(): return Revfile('testrev', 'w') def ro(): return Revfile('testrev', 'r') if cmd == 'add': print rw().add(sys.stdin.read()) elif cmd == 'add-delta': print rw().add(sys.stdin.read(), int(argv[2])) elif cmd == 'dump': ro().dump() elif cmd == 'get': try: idx = int(argv[2]) except IndexError: sys.stderr.write("usage: revfile get IDX\n") return 1 if idx < 0 or idx >= len(r): sys.stderr.write("invalid index %r\n" % idx) return 1 sys.stdout.write(ro().get(idx)) elif cmd == 'find-sha': try: s = unhexlify(argv[2]) except IndexError: sys.stderr.write("usage: revfile find-sha HEX\n") return 1 idx = ro().find_sha(s) if idx == _NO_RECORD: sys.stderr.write("no such record\n") return 1 else: print idx elif cmd == 'total-text-size': print ro().total_text_size() elif cmd == 'last': print len(ro())-1 else: sys.stderr.write("unknown command %r\n" % cmd) return 1 if __name__ == '__main__': import sys sys.exit(main(sys.argv) or 0) M 644 inline bzrlib/store.py data 5394 # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Stores are the main data-storage mechanism for Bazaar-NG. A store is a simple write-once container indexed by a universally unique ID, which is typically the SHA-1 of the content.""" __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import os, tempfile, types, osutils, gzip, errno from stat import ST_SIZE from StringIO import StringIO from trace import mutter ###################################################################### # stores class StoreError(Exception): pass class ImmutableStore(object): """Store that holds files indexed by unique names. Files can be added, but not modified once they are in. Typically the hash is used as the name, or something else known to be unique, such as a UUID. >>> st = ImmutableScratchStore() >>> st.add(StringIO('hello'), 'aa') >>> 'aa' in st True >>> 'foo' in st False You are not allowed to add an id that is already present. Entries can be retrieved as files, which may then be read. >>> st.add(StringIO('goodbye'), '123123') >>> st['123123'].read() 'goodbye' TODO: Atomic add by writing to a temporary file and renaming. TODO: Perhaps automatically transform to/from XML in a method? Would just need to tell the constructor what class to use... TODO: Even within a simple disk store like this, we could gzip the files. But since many are less than one disk block, that might not help a lot. """ def __init__(self, basedir): """ImmutableStore constructor.""" self._basedir = basedir def _path(self, id): assert '/' not in id return os.path.join(self._basedir, id) def __repr__(self): return "%s(%r)" % (self.__class__.__name__, self._basedir) def add(self, f, fileid, compressed=True): """Add contents of a file into the store. f -- An open file, or file-like object.""" # FIXME: Only works on smallish files # TODO: Can be optimized by copying at the same time as # computing the sum. mutter("add store entry %r" % (fileid)) if isinstance(f, types.StringTypes): content = f else: content = f.read() p = self._path(fileid) if os.access(p, os.F_OK) or os.access(p + '.gz', os.F_OK): bailout("store %r already contains id %r" % (self._basedir, fileid)) if compressed: f = gzip.GzipFile(p + '.gz', 'wb') os.chmod(p + '.gz', 0444) else: f = file(p, 'wb') os.chmod(p, 0444) f.write(content) f.close() def __contains__(self, fileid): """""" p = self._path(fileid) return (os.access(p, os.R_OK) or os.access(p + '.gz', os.R_OK)) # TODO: Guard against the same thing being stored twice, compressed and uncompresse def __iter__(self): for f in os.listdir(self._basedir): if f[-3:] == '.gz': # TODO: case-insensitive? yield f[:-3] else: yield f def __len__(self): return len(os.listdir(self._basedir)) def __getitem__(self, fileid): """Returns a file reading from a particular entry.""" p = self._path(fileid) try: return gzip.GzipFile(p + '.gz', 'rb') except IOError, e: if e.errno == errno.ENOENT: return file(p, 'rb') else: raise e def total_size(self): """Return (count, bytes) This is the (compressed) size stored on disk, not the size of the content.""" total = 0 count = 0 for fid in self: count += 1 p = self._path(fid) try: total += os.stat(p)[ST_SIZE] except OSError: total += os.stat(p + '.gz')[ST_SIZE] return count, total class ImmutableScratchStore(ImmutableStore): """Self-destructing test subclass of ImmutableStore. The Store only exists for the lifetime of the Python object. Obviously you should not put anything precious in it. """ def __init__(self): ImmutableStore.__init__(self, tempfile.mkdtemp()) def __del__(self): for f in os.listdir(self._basedir): fpath = os.path.join(self._basedir, f) # needed on windows, and maybe some other filesystems os.chmod(fpath, 0600) os.remove(fpath) os.rmdir(self._basedir) mutter("%r destroyed" % self) M 644 inline bzrlib/tree.py data 7753 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tree classes, representing directory at point in time. """ from sets import Set import os.path, os, fnmatch from osutils import pumpfile, filesize, quotefn, sha_file, \ joinpath, splitpath, appendpath, isdir, isfile, file_kind, fingerprint_file import errno from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE from inventory import Inventory from trace import mutter, note from errors import bailout import branch import bzrlib class Tree(object): """Abstract file tree. There are several subclasses: * `WorkingTree` exists as files on disk editable by the user. * `RevisionTree` is a tree as recorded at some point in the past. * `EmptyTree` Trees contain an `Inventory` object, and also know how to retrieve file texts mentioned in the inventory, either from a working directory or from a store. It is possible for trees to contain files that are not described in their inventory or vice versa; for this use `filenames()`. Trees can be compared, etc, regardless of whether they are working trees or versioned trees. """ def has_filename(self, filename): """True if the tree has given filename.""" raise NotImplementedError() def has_id(self, file_id): return self.inventory.has_id(file_id) __contains__ = has_id def __iter__(self): return iter(self.inventory) def id2path(self, file_id): return self.inventory.id2path(file_id) def _get_inventory(self): return self._inventory inventory = property(_get_inventory, doc="Inventory of this Tree") def _check_retrieved(self, ie, f): fp = fingerprint_file(f) f.seek(0) if ie.text_size != None: if ie.text_size != fp['size']: bailout("mismatched size for file %r in %r" % (ie.file_id, self._store), ["inventory expects %d bytes" % ie.text_size, "file is actually %d bytes" % fp['size'], "store is probably damaged/corrupt"]) if ie.text_sha1 != fp['sha1']: bailout("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store), ["inventory expects %s" % ie.text_sha1, "file is actually %s" % fp['sha1'], "store is probably damaged/corrupt"]) def print_file(self, fileid): """Print file with id `fileid` to stdout.""" import sys pumpfile(self.get_file(fileid), sys.stdout) def export(self, dest): """Export this tree to a new directory. `dest` should not exist, and will be created holding the contents of this tree. TODO: To handle subdirectories we need to create the directories first. :note: If the export fails, the destination directory will be left in a half-assed state. """ os.mkdir(dest) mutter('export version %r' % self) inv = self.inventory for dp, ie in inv.iter_entries(): kind = ie.kind fullpath = appendpath(dest, dp) if kind == 'directory': os.mkdir(fullpath) elif kind == 'file': pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb')) else: bailout("don't know how to export {%s} of kind %r" % (ie.file_id, kind)) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) class RevisionTree(Tree): """Tree viewing a previous revision. File text can be retrieved from the text store. TODO: Some kind of `__repr__` method, but a good one probably means knowing the branch and revision number, or at least passing a description to the constructor. """ def __init__(self, store, inv): self._store = store self._inventory = inv def get_file(self, file_id): ie = self._inventory[file_id] f = self._store[ie.text_id] mutter(" get fileid{%s} from %r" % (file_id, self)) self._check_retrieved(ie, f) return f def get_file_size(self, file_id): return self._inventory[file_id].text_size def get_file_sha1(self, file_id): ie = self._inventory[file_id] return ie.text_sha1 def has_filename(self, filename): return bool(self.inventory.path2id(filename)) def list_files(self): # The only files returned by this are those from the version for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id class EmptyTree(Tree): def __init__(self): self._inventory = Inventory() def has_filename(self, filename): return False def list_files(self): if False: # just to make it a generator yield None ###################################################################### # diff # TODO: Merge these two functions into a single one that can operate # on either a whole tree or a set of files. # TODO: Return the diff in order by filename, not by category or in # random order. Can probably be done by lock-stepping through the # filenames from both trees. def file_status(filename, old_tree, new_tree): """Return single-letter status, old and new names for a file. The complexity here is in deciding how to represent renames; many complex cases are possible. """ old_inv = old_tree.inventory new_inv = new_tree.inventory new_id = new_inv.path2id(filename) old_id = old_inv.path2id(filename) if not new_id and not old_id: # easy: doesn't exist in either; not versioned at all if new_tree.is_ignored(filename): return 'I', None, None else: return '?', None, None elif new_id: # There is now a file of this name, great. pass else: # There is no longer a file of this name, but we can describe # what happened to the file that used to have # this name. There are two possibilities: either it was # deleted entirely, or renamed. assert old_id if new_inv.has_id(old_id): return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id) else: return 'D', old_inv.id2path(old_id), None # if the file_id is new in this revision, it is added if new_id and not old_inv.has_id(new_id): return 'A' # if there used to be a file of this name, but that ID has now # disappeared, it is deleted if old_id and not new_inv.has_id(old_id): return 'D' return 'wtf?' def find_renames(old_inv, new_inv): for file_id in old_inv: if file_id not in new_inv: continue old_name = old_inv.id2path(file_id) new_name = new_inv.id2path(file_id) if old_name != new_name: yield (old_name, new_name) commit refs/heads/master mark :559 committer Martin Pool 1117074135 +1000 data 97 - AtomicFile must cope without destination file existing on win32 (patch from John A Meinel) from :558 M 644 inline bzrlib/atomicfile.py data 2477 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA class AtomicFile(object): """A file that does an atomic-rename to move into place. This also causes hardlinks to break when it's written out. Open this as for a regular file, then use commit() to move into place or abort() to cancel. An encoding can be specified; otherwise the default is ascii. """ def __init__(self, filename, mode='wb', encoding=None): if mode != 'wb' and mode != 'wt': raise ValueError("invalid AtomicFile mode %r" % mode) import os, socket self.tmpfilename = '%s.%d.%s.tmp' % (filename, os.getpid(), socket.gethostname()) self.realfilename = filename self.f = open(self.tmpfilename, mode) if encoding: import codecs self.f = codecs.EncodedFile(self.f, encoding) self.write = self.f.write self.closed = property(self.f.closed) def commit(self): """Close the file and move to final name.""" import sys, os self.f.close() if sys.platform == 'win32': # windows cannot rename over an existing file try: os.remove(self.realfilename) except OSError, e: import errno if e.errno != errno.ENOENT: raise os.rename(self.tmpfilename, self.realfilename) def abort(self): """Discard temporary file without committing changes.""" import os self.f.close() os.remove(self.tmpfilename) def close(self): """Discard the file unless already committed.""" if not self.closed: self.abort() commit refs/heads/master mark :560 committer Martin Pool 1117074442 +1000 data 52 - fix testbzr for win32 (patch from John A Meinel) from :559 M 644 inline testbzr data 10861 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. usage: testbzr [-p PYTHON] [BZR] By default this tests the copy of bzr found in the same directory as testbzr, or the first one found on the $PATH. A copy of bzr may be given on the command line to override this, for example when applying a new test suite to an old copy of bzr or vice versa. testbzr normally invokes bzr using the same version of python as it would normally use to run -- that is, the system default python, unless that is older than 2.3. The -p option allows specification of a different Python interpreter, such as when testing that bzr still works on python2.3. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" OVERRIDE_PYTHON = None LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = BZRPATH if OVERRIDE_PYTHON: cmd.insert(0, OVERRIDE_PYTHON) logfile.write('$ %r\n' % cmd) return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) start_dir = os.getcwd() logfile = open(LOGFILENAME, 'wt', buffering=1) try: from getopt import getopt opts, args = getopt(sys.argv[1:], 'p:') for option, value in opts: if option == '-p': OVERRIDE_PYTHON = value mypath = os.path.abspath(sys.argv[0]) print '%-30s %s' % ('running tests from', mypath) global BZRPATH if args: BZRPATH = args[1] else: BZRPATH = os.path.join(os.path.split(mypath)[0], 'bzr') print '%-30s %s' % ('against bzr', BZRPATH) print '%-30s %s' % ('in directory', os.getcwd()) print '%-30s %s' % ('with python', (OVERRIDE_PYTHON or '(default)')) print print backtick([BZRPATH, 'version']) runcmd(['mkdir', TESTDIR]) cd(TESTDIR) test_root = os.getcwd() progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("internal tests") runcmd("bzr selftest") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) runcmd("bzr diff --message foo", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == 'unknown:\n test.txt\n' out = backtick("bzr status --all") assert out == "unknown:\n test.txt\n" out = backtick("bzr status test.txt --all") assert out == "unknown:\n test.txt\n" f = file('test2.txt', 'wt') f.write('goodbye cruel world...\n') f.close() out = backtick("bzr status test.txt") assert out == "unknown:\n test.txt\n" out = backtick("bzr status") assert out == ("unknown:\n" " test.txt\n" " test2.txt\n") os.unlink('test2.txt') progress("command aliases") out = backtick("bzr st --all") assert out == ("unknown:\n" " test.txt\n") out = backtick("bzr stat") assert out == ("unknown:\n" " test.txt\n") progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == ("added:\n" " test.txt\n") progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == os.path.join("sub2", "hello.txt\n") assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') assert backtick('bzr relpath sub2/hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') runcmd('bzr log') runcmd('bzr log -v') progress("file with spaces in name") mkdir('sub directory') file('sub directory/file with spaces ', 'wt').write('see how this works\n') runcmd('bzr add .') runcmd('bzr diff') runcmd('bzr commit -m add-spaces') runcmd('bzr check') runcmd('bzr log') runcmd('bzr log --forward') runcmd('bzr info') cd('..') cd('..') progress('ignore patterns') mkdir('ignorebranch') cd('ignorebranch') runcmd('bzr init') assert backtick('bzr unknowns') == '' file('foo.tmp', 'wt').write('tmp files are ignored') assert backtick('bzr unknowns') == '' file('foo.c', 'wt').write('int main() {}') assert backtick('bzr unknowns') == 'foo.c\n' runcmd('bzr add foo.c') assert backtick('bzr unknowns') == '' # 'ignore' works when creating the .bzignore file file('foo.blah', 'wt').write('blah') assert backtick('bzr unknowns') == 'foo.blah\n' runcmd('bzr ignore *.blah') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\n' # 'ignore' works when then .bzrignore file already exists file('garh', 'wt').write('garh') assert backtick('bzr unknowns') == 'garh\n' runcmd('bzr ignore garh') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\ngarh\n' progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) logfile.close() sys.stdout.writelines(file(os.path.join(start_dir, LOGFILENAME), 'rt').readlines()[-50:]) sys.exit(1) commit refs/heads/master mark :561 committer Martin Pool 1117077053 +1000 data 83 - add failing test for problem with stat after remove, reported by William Dodé from :560 M 644 inline testbzr data 11586 #! /usr/bin/python # -*- coding: utf-8 -*- # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. usage: testbzr [-p PYTHON] [BZR] By default this tests the copy of bzr found in the same directory as testbzr, or the first one found on the $PATH. A copy of bzr may be given on the command line to override this, for example when applying a new test suite to an old copy of bzr or vice versa. testbzr normally invokes bzr using the same version of python as it would normally use to run -- that is, the system default python, unless that is older than 2.3. The -p option allows specification of a different Python interpreter, such as when testing that bzr still works on python2.3. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" OVERRIDE_PYTHON = None LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = BZRPATH if OVERRIDE_PYTHON: cmd.insert(0, OVERRIDE_PYTHON) logfile.write('$ %r\n' % cmd) return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) start_dir = os.getcwd() logfile = open(LOGFILENAME, 'wt', buffering=1) try: from getopt import getopt opts, args = getopt(sys.argv[1:], 'p:') for option, value in opts: if option == '-p': OVERRIDE_PYTHON = value mypath = os.path.abspath(sys.argv[0]) print '%-30s %s' % ('running tests from', mypath) global BZRPATH if args: BZRPATH = args[1] else: BZRPATH = os.path.join(os.path.split(mypath)[0], 'bzr') print '%-30s %s' % ('against bzr', BZRPATH) print '%-30s %s' % ('in directory', os.getcwd()) print '%-30s %s' % ('with python', (OVERRIDE_PYTHON or '(default)')) print print backtick([BZRPATH, 'version']) runcmd(['mkdir', TESTDIR]) cd(TESTDIR) test_root = os.getcwd() progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("internal tests") runcmd("bzr selftest") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) runcmd("bzr diff --message foo", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == 'unknown:\n test.txt\n' out = backtick("bzr status --all") assert out == "unknown:\n test.txt\n" out = backtick("bzr status test.txt --all") assert out == "unknown:\n test.txt\n" f = file('test2.txt', 'wt') f.write('goodbye cruel world...\n') f.close() out = backtick("bzr status test.txt") assert out == "unknown:\n test.txt\n" out = backtick("bzr status") assert out == ("unknown:\n" " test.txt\n" " test2.txt\n") os.unlink('test2.txt') progress("command aliases") out = backtick("bzr st --all") assert out == ("unknown:\n" " test.txt\n") out = backtick("bzr stat") assert out == ("unknown:\n" " test.txt\n") progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == ("added:\n" " test.txt\n") progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == os.path.join("sub2", "hello.txt\n") assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') assert backtick('bzr relpath sub2/hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') runcmd('bzr log') runcmd('bzr log -v') progress("file with spaces in name") mkdir('sub directory') file('sub directory/file with spaces ', 'wt').write('see how this works\n') runcmd('bzr add .') runcmd('bzr diff') runcmd('bzr commit -m add-spaces') runcmd('bzr check') runcmd('bzr log') runcmd('bzr log --forward') runcmd('bzr info') cd('..') cd('..') progress('status after remove') mkdir('status-after-remove') # see mail from William Dodé, 2005-05-25 # $ bzr init; touch a; bzr add a; bzr commit -m "add a" # * looking for changes... # added a # * commited r1 # $ bzr remove a # $ bzr status # bzr: local variable 'kind' referenced before assignment # at /vrac/python/bazaar-ng/bzrlib/diff.py:286 in compare_trees() # see ~/.bzr.log for debug information cd('status-after-remove') runcmd('bzr init') file('a', 'w').write('foo') runcmd('bzr add a') runcmd(['bzr', 'commit', '-m', 'add a']) runcmd('bzr remove a') runcmd('bzr status') cd('..') progress('ignore patterns') mkdir('ignorebranch') cd('ignorebranch') runcmd('bzr init') assert backtick('bzr unknowns') == '' file('foo.tmp', 'wt').write('tmp files are ignored') assert backtick('bzr unknowns') == '' file('foo.c', 'wt').write('int main() {}') assert backtick('bzr unknowns') == 'foo.c\n' runcmd('bzr add foo.c') assert backtick('bzr unknowns') == '' # 'ignore' works when creating the .bzignore file file('foo.blah', 'wt').write('blah') assert backtick('bzr unknowns') == 'foo.blah\n' runcmd('bzr ignore *.blah') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\n' # 'ignore' works when then .bzrignore file already exists file('garh', 'wt').write('garh') assert backtick('bzr unknowns') == 'garh\n' runcmd('bzr ignore garh') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\ngarh\n' progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) logfile.close() sys.stdout.writelines(file(os.path.join(start_dir, LOGFILENAME), 'rt').readlines()[-50:]) sys.exit(1) commit refs/heads/master mark :562 committer Martin Pool 1117120400 +1000 data 46 - bug fix for printing logs containing unicode from :561 M 644 inline bzrlib/commands.py data 36577 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date from bzrlib import merge def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0], lock_mode='r') file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.', lock_mode='r') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0], lock_mode='r') file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.', lock_mode='r') show_diff(b, revision, specific_files=file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. TODO: Option to limit range. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename, lock_mode='r') fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.', lock_mode='r') file_id = None mutter('encoding log as %r' % bzrlib.user_encoding) outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout) show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures return 1 else: print return 0 class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): merge.merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'forward': None, 'message': unicode, 'profile': None, 'revision': int, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :563 committer Martin Pool 1117123322 +1000 data 138 - AtomicFile emits a warning if it is gc'd without being closed - also use codecs.open rather than EncodedFile - more checks in AtomicFile from :562 M 644 inline bzrlib/atomicfile.py data 2940 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from warnings import warn class AtomicFile(object): """A file that does an atomic-rename to move into place. This also causes hardlinks to break when it's written out. Open this as for a regular file, then use commit() to move into place or abort() to cancel. An encoding can be specified; otherwise the default is ascii. """ def __init__(self, filename, mode='wb', encoding=None): if mode != 'wb' and mode != 'wt': raise ValueError("invalid AtomicFile mode %r" % mode) import os, socket self.tmpfilename = '%s.%d.%s.tmp' % (filename, os.getpid(), socket.gethostname()) self.realfilename = filename if encoding: import codecs self.f = codecs.open(self.tmpfilename, mode, encoding) else: self.f = open(self.tmpfilename, mode) self.write = self.f.write self.closed = False def __repr__(self): return '%s(%r)' % (self.__class__.__name__, self.realfilename) def commit(self): """Close the file and move to final name.""" import sys, os if self.closed: raise Exception('%r is already closed' % self) self.f.close() self.closed = True if sys.platform == 'win32': # windows cannot rename over an existing file try: os.remove(self.realfilename) except OSError, e: import errno if e.errno != errno.ENOENT: raise os.rename(self.tmpfilename, self.realfilename) def abort(self): """Discard temporary file without committing changes.""" import os if self.closed: raise Exception('%r is already closed' % self) self.f.close() self.closed = True os.remove(self.tmpfilename) def close(self): """Discard the file unless already committed.""" if not self.closed: self.abort() def __del__(self): if not self.closed: warn("%r leaked" % self) commit refs/heads/master mark :564 committer Martin Pool 1117123773 +1000 data 105 - Set Branch.base in class def to avoid it being undefined when introspecting half-constructed branches from :563 M 644 inline bzrlib/branch.py data 26892 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import bailout, BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch(object): """Branch holding a history of revisions. base Base directory of the branch. """ _lockmode = None base = None def __init__(self, base, init=False, find_root=True, lock_mode='w'): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): bailout("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.lock(lock_mode) self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def lock(self, mode='w'): """Lock the on-disk branch, excluding other processes.""" try: import fcntl, errno if mode == 'w': lm = fcntl.LOCK_EX om = os.O_WRONLY | os.O_CREAT elif mode == 'r': lm = fcntl.LOCK_SH om = os.O_RDONLY else: raise BzrError("invalid locking mode %r" % mode) try: lockfile = os.open(self.controlfilename('branch-lock'), om) except OSError, e: if e.errno == errno.ENOENT: # might not exist on branches from <0.0.4 self.controlfile('branch-lock', 'w').close() lockfile = os.open(self.controlfilename('branch-lock'), om) else: raise e fcntl.lockf(lockfile, lm) def unlock(): fcntl.lockf(lockfile, fcntl.LOCK_UN) os.close(lockfile) self._lockmode = None self.unlock = unlock self._lockmode = mode except ImportError: warning("please write a locking method for platform %r" % sys.platform) def unlock(): self._lockmode = None self.unlock = unlock self._lockmode = mode def _need_readlock(self): if self._lockmode not in ['r', 'w']: raise BzrError('need read lock on branch, only have %r' % self._lockmode) def _need_writelock(self): if self._lockmode not in ['w']: raise BzrError('need write lock on branch, only have %r' % self._lockmode) def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): bailout("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: bailout('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" self._need_readlock() before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self._need_writelock() ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ self._need_writelock() # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): bailout("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: bailout("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': bailout('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" self._need_readlock() tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: bailout("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability self._need_writelock() if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: bailout("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" self._need_readlock() r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" self._need_readlock() i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" self._need_readlock() if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self._need_readlock() return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def commit(self, *args, **kw): """Deprecated""" from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. self._need_readlock() if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self._need_writelock() tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): bailout("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): bailout("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: bailout("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): bailout("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': bailout("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: bailout("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self._need_writelock() ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): bailout("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): bailout("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': bailout("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): bailout("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): bailout("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: bailout("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): bailout("destination %r already exists" % dest_path) if f_id in to_idpath: bailout("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :565 committer Martin Pool 1117123785 +1000 data 37 - more invariant checks in AtomicFile from :564 M 644 inline bzrlib/atomicfile.py data 3010 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from warnings import warn class AtomicFile(object): """A file that does an atomic-rename to move into place. This also causes hardlinks to break when it's written out. Open this as for a regular file, then use commit() to move into place or abort() to cancel. An encoding can be specified; otherwise the default is ascii. """ def __init__(self, filename, mode='wb', encoding=None): if mode != 'wb' and mode != 'wt': raise ValueError("invalid AtomicFile mode %r" % mode) import os, socket self.tmpfilename = '%s.%d.%s.tmp' % (filename, os.getpid(), socket.gethostname()) self.realfilename = filename if encoding: import codecs self.f = codecs.open(self.tmpfilename, mode, encoding) else: self.f = open(self.tmpfilename, mode) self.write = self.f.write self.closed = False def __repr__(self): return '%s(%r)' % (self.__class__.__name__, self.realfilename) def commit(self): """Close the file and move to final name.""" import sys, os if self.closed: raise Exception('%r is already closed' % self) self.closed = True self.f.close() self.f = None if sys.platform == 'win32': # windows cannot rename over an existing file try: os.remove(self.realfilename) except OSError, e: import errno if e.errno != errno.ENOENT: raise os.rename(self.tmpfilename, self.realfilename) def abort(self): """Discard temporary file without committing changes.""" import os if self.closed: raise Exception('%r is already closed' % self) self.closed = True self.f.close() self.f = None os.remove(self.tmpfilename) def close(self): """Discard the file unless already committed.""" if not self.closed: self.abort() def __del__(self): if not self.closed: warn("%r leaked" % self) commit refs/heads/master mark :566 committer Martin Pool 1117126613 +1000 data 74 - fix bug in reporting diffs between trees where files have been deleted from :565 M 644 inline bzrlib/diff.py data 10303 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from trace import mutter from errors import BzrError def _diff_one(oldlines, newlines, to_file, **kw): import difflib # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, **kw) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') to_file.writelines(ud) if nonl: print >>to_file, "\\ No newline at end of file" print >>to_file def show_diff(b, revision, specific_files): import sys if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() show_diff_trees(old_tree, new_tree, sys.stdout, specific_files) def show_diff_trees(old_tree, new_tree, to_file, specific_files=None): """Show in text form the changes from one tree to another. to_files If set, include only changes to these files. """ # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. delta = compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=specific_files) for path, file_id, kind in delta.removed: print '*** removed %s %r' % (kind, path) if kind == 'file': _diff_one(old_tree.get_file(file_id).readlines(), [], to_file, fromfile=old_label + path, tofile=DEVNULL) for path, file_id, kind in delta.added: print '*** added %s %r' % (kind, path) if kind == 'file': _diff_one([], new_tree.get_file(file_id).readlines(), to_file, fromfile=DEVNULL, tofile=new_label + path) for old_path, new_path, file_id, kind, text_modified in delta.renamed: print '*** renamed %s %r => %r' % (kind, old_path, new_path) if text_modified: _diff_one(old_tree.get_file(file_id).readlines(), new_tree.get_file(file_id).readlines(), to_file, fromfile=old_label + old_path, tofile=new_label + new_path) for path, file_id, kind in delta.modified: print '*** modified %s %r' % (kind, path) if kind == 'file': _diff_one(old_tree.get_file(file_id).readlines(), new_tree.get_file(file_id).readlines(), to_file, fromfile=old_label + path, tofile=new_label + path) class TreeDelta(object): """Describes changes from one tree to another. Contains four lists: added (path, id, kind) removed (path, id, kind) renamed (oldpath, newpath, id, kind, text_modified) modified (path, id, kind) unchanged (path, id, kind) Each id is listed only once. Files that are both modified and renamed are listed only in renamed, with the text_modified flag true. The lists are normally sorted when the delta is created. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] self.unchanged = [] def touches_file_id(self, file_id): """Return True if file_id is modified by this delta.""" for l in self.added, self.removed, self.modified: for v in l: if v[1] == file_id: return True for v in self.renamed: if v[2] == file_id: return True return False def show(self, to_file, show_ids=False, show_unchanged=False): def show_list(files): for path, fid, kind in files: if kind == 'directory': path += '/' elif kind == 'symlink': path += '@' if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if self.removed: print >>to_file, 'removed:' show_list(self.removed) if self.added: print >>to_file, 'added:' show_list(self.added) if self.renamed: print >>to_file, 'renamed:' for oldpath, newpath, fid, kind, text_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if self.modified: print >>to_file, 'modified:' show_list(self.modified) if show_unchanged and self.unchanged: print >>to_file, 'unchanged:' show_list(self.unchanged) def compare_trees(old_tree, new_tree, want_unchanged, specific_files=None): """Describe changes from one tree to another. Returns a TreeDelta with details of added, modified, renamed, and deleted entries. The root entry is specifically exempt. This only considers versioned files. want_unchanged If true, also list files unchanged from one version to the next. specific_files If true, only check for changes to specified names or files within them. """ from osutils import is_inside_any old_inv = old_tree.inventory new_inv = new_tree.inventory delta = TreeDelta() mutter('start compare_trees') # TODO: match for specific files can be rather smarter by finding # the IDs of those files up front and then considering only that. for file_id in old_tree: if file_id in new_tree: kind = old_inv.get_file_kind(file_id) assert kind == new_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ 'invalid file kind %r' % kind if kind == 'root_directory': continue old_path = old_inv.id2path(file_id) new_path = new_inv.id2path(file_id) if specific_files: if (not is_inside_any(specific_files, old_path) and not is_inside_any(specific_files, new_path)): continue if kind == 'file': old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False # TODO: Can possibly avoid calculating path strings if the # two files are unchanged and their names and parents are # the same and the parents are unchanged all the way up. # May not be worthwhile. if old_path != new_path: delta.renamed.append((old_path, new_path, file_id, kind, text_modified)) elif text_modified: delta.modified.append((new_path, file_id, kind)) elif want_unchanged: delta.unchanged.append((new_path, file_id, kind)) else: kind = old_inv.get_file_kind(file_id) old_path = old_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, old_path): continue delta.removed.append((old_path, file_id, kind)) mutter('start looking for new files') for file_id in new_inv: if file_id in old_inv: continue new_path = new_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, new_path): continue kind = new_inv.get_file_kind(file_id) delta.added.append((new_path, file_id, kind)) delta.removed.sort() delta.added.sort() delta.renamed.sort() delta.modified.sort() delta.unchanged.sort() return delta commit refs/heads/master mark :567 committer Martin Pool 1117126689 +1000 data 59 - New form 'bzr log -r FROM:TO' patch from John A Meinel from :566 M 644 inline NEWS data 7786 bzr-0.0.5 NOT RELEASED YET CHANGES: * ``bzr`` with no command now shows help rather than giving an error. Suggested by Michael Ellerman. * ``bzr status`` output format changed, because svn-style output doesn't really match the model of bzr. Now files are grouped by status and can be shown with their IDs. ``bzr status --all`` shows all versioned files and unknown files but not ignored files. * ``bzr log`` runs from most-recent to least-recent, the reverse of the previous order. The previous behaviour can be obtained with the ``--forward`` option. ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New ``bzr ignore PATTERN`` command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. File may be in a branch other than the working directory. * ``bzr log`` and ``bzr root`` can be given an http URL instead of a filename. * Commands can now be defined by external programs or scripts in a directory on $BZRPATH. * New "stat cache" avoids reading the contents of files if they haven't changed since the previous time. * If the Python interpreter is too old, try to find a better one or give an error. Based on a patch from Fredrik Lundh. * New optional parameter ``bzr info [BRANCH]``. * New form ``bzr commit SELECTED`` to commit only selected files. * New form ``bzr log -r FROM:TO`` shows changes in selected range; contributed by John A Meinel. BUG FIXES: * Fixed diff format so that added and removed files will be handled properly by patch. Fix from Lalo Martins. * Various fixes for files whose names contain spaces or other metacharacters. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. * testbzr requires python2.4, but can be used to test bzr running under a different version. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. * New developer commands ``added``, ``modified``. PORTABILITY: * Cope on Windows on python2.3 by using the weaker random seed. 2.4 is now only recommended. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 38054 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date from bzrlib import merge def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0], lock_mode='r') file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.', lock_mode='r') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision'] aliases = ['di'] def run(self, revision=None, file_list=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0], lock_mode='r') file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.', lock_mode='r') show_diff(b, revision, specific_files=file_list) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename, lock_mode='r') fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.', lock_mode='r') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout) show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures return 1 else: print return 0 class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): merge.merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'help': None, 'file': unicode, 'forward': None, 'message': unicode, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/log.py data 7501 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Code to show logs of changes. Various flavors of log can be produced: * for one file, or the whole tree, and (not done yet) for files in a given directory * in "verbose" mode with a description of what changed from one version to the next * with file-ids and revision-ids shown * from last to first or (not anymore) from first to last; the default is "reversed" because it shows the likely most relevant and interesting information first * (not yet) in XML format """ from trace import mutter def find_touching_revisions(branch, file_id): """Yield a description of revisions which affect the file_id. Each returned element is (revno, revision_id, description) This is the list of revisions where the file is either added, modified, renamed or deleted. TODO: Perhaps some way to limit this to only particular revisions, or to traverse a non-mainline set of revisions? """ last_ie = None last_path = None revno = 1 for revision_id in branch.revision_history(): this_inv = branch.get_revision_inventory(revision_id) if file_id in this_inv: this_ie = this_inv[file_id] this_path = this_inv.id2path(file_id) else: this_ie = this_path = None # now we know how it was last time, and how it is in this revision. # are those two states effectively the same or not? if not this_ie and not last_ie: # not present in either pass elif this_ie and not last_ie: yield revno, revision_id, "added " + this_path elif not this_ie and last_ie: # deleted here yield revno, revision_id, "deleted " + last_path elif this_path != last_path: yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path)) elif (this_ie.text_size != last_ie.text_size or this_ie.text_sha1 != last_ie.text_sha1): yield revno, revision_id, "modified " + this_path last_ie = this_ie last_path = this_path revno += 1 def show_log(branch, specific_fileid=None, show_timezone='original', verbose=False, show_ids=False, to_file=None, direction='reverse', start_revision=None, end_revision=None): """Write out human-readable log of commits to this branch. specific_fileid If true, list only the commits affecting the specified file, rather than all commits. show_timezone 'original' (committer's timezone), 'utc' (universal time), or 'local' (local user's timezone) verbose If true show added/changed/deleted/renamed files. show_ids If true, show revision and file ids. to_file File to send log to; by default stdout. direction 'reverse' (default) is latest to earliest; 'forward' is earliest to latest. start_revision If not None, only show revisions >= start_revision end_revision If not None, only show revisions <= end_revision """ from osutils import format_date from errors import BzrCheckError from textui import show_status if specific_fileid: mutter('get log for file_id %r' % specific_fileid) if to_file == None: import sys to_file = sys.stdout which_revs = branch.enum_history(direction) if not (verbose or specific_fileid): # no need to know what changed between revisions with_deltas = deltas_for_log_dummy(branch, which_revs) elif direction == 'reverse': with_deltas = deltas_for_log_reverse(branch, which_revs) else: raise NotImplementedError("sorry, verbose forward logs not done yet") for revno, rev, delta in with_deltas: if specific_fileid: if not delta.touches_file_id(specific_fileid): continue if start_revision is not None and revno < start_revision: continue if end_revision is not None and revno > end_revision: continue if not verbose: # although we calculated it, throw it away without display delta = None show_one_log(revno, rev, delta, show_ids, to_file, show_timezone) def deltas_for_log_dummy(branch, which_revs): for revno, revision_id in which_revs: yield revno, branch.get_revision(revision_id), None def deltas_for_log_reverse(branch, which_revs): """Compute deltas for display in reverse log. Given a sequence of (revno, revision_id) pairs, return (revno, rev, delta). The delta is from the given revision to the next one in the sequence, which makes sense if the log is being displayed from newest to oldest. """ from tree import EmptyTree from diff import compare_trees last_revno = last_revision_id = last_tree = None for revno, revision_id in which_revs: this_tree = branch.revision_tree(revision_id) this_revision = branch.get_revision(revision_id) if last_revno: yield last_revno, last_revision, compare_trees(this_tree, last_tree, False) last_revno = revno last_revision = this_revision last_tree = this_tree if last_revno: this_tree = EmptyTree() yield last_revno, last_revision, compare_trees(this_tree, last_tree, False) def junk(): precursor = None if verbose: from tree import EmptyTree prev_tree = EmptyTree() for revno, revision_id in which_revs: precursor = revision_id if revision_id != rev.revision_id: raise BzrCheckError("retrieved wrong revision: %r" % (revision_id, rev.revision_id)) if verbose: this_tree = branch.revision_tree(revision_id) delta = compare_trees(prev_tree, this_tree, want_unchanged=False) prev_tree = this_tree else: delta = None def show_one_log(revno, rev, delta, show_ids, to_file, show_timezone): from osutils import format_date print >>to_file, '-' * 60 print >>to_file, 'revno:', revno if show_ids: print >>to_file, 'revision-id:', rev.revision_id print >>to_file, 'committer:', rev.committer print >>to_file, 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) print >>to_file, 'message:' if not rev.message: print >>to_file, ' (no message)' else: for l in rev.message.split('\n'): print >>to_file, ' ' + l if delta != None: delta.show(to_file, show_ids) commit refs/heads/master mark :568 committer Martin Pool 1117158573 +1000 data 107 - start adding support for showing diffs by calling out to an external program rather than using difflib from :567 M 644 inline bzrlib/diff.py data 11774 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from trace import mutter from errors import BzrError def internal_diff(old_label, oldlines, new_label, newlines, to_file): import difflib # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, fromfile=old_label, tofile=new_label) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') to_file.writelines(ud) if nonl: print >>to_file, "\\ No newline at end of file" print >>to_file def external_diff(old_label, oldlines, new_label, newlines, to_file): """Display a diff by calling out to the external diff program.""" import sys if to_file != sys.stdout: raise NotImplementedError("sorry, can't send external diff other than to stdout yet", to_file) from tempfile import NamedTemporaryFile from os import system oldtmpf = NamedTemporaryFile() newtmpf = NamedTemporaryFile() try: # TODO: perhaps a special case for comparing to or from the empty # sequence; can just use /dev/null on Unix # TODO: if either of the files being compared already exists as a # regular named file (e.g. in the working directory) then we can # compare directly to that, rather than copying it. # TODO: Set the labels appropriately oldtmpf.writelines(oldlines) newtmpf.writelines(newlines) oldtmpf.flush() newtmpf.flush() system('diff -u --label %s %s --label %s %s' % (old_label, oldtmpf.name, new_label, newtmpf.name)) finally: oldtmpf.close() # and delete newtmpf.close() def diff_file(old_label, oldlines, new_label, newlines, to_file): if True: differ = external_diff else: differ = internal_diff differ(old_label, oldlines, new_label, newlines, to_file) def show_diff(b, revision, specific_files): import sys if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() show_diff_trees(old_tree, new_tree, sys.stdout, specific_files) def show_diff_trees(old_tree, new_tree, to_file, specific_files=None): """Show in text form the changes from one tree to another. to_files If set, include only changes to these files. """ # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. delta = compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=specific_files) for path, file_id, kind in delta.removed: print '*** removed %s %r' % (kind, path) if kind == 'file': diff_file(old_label + path, old_tree.get_file(file_id).readlines(), DEVNULL, [], to_file) for path, file_id, kind in delta.added: print '*** added %s %r' % (kind, path) if kind == 'file': diff_file(DEVNULL, [], new_label + path, new_tree.get_file(file_id).readlines(), to_file) for old_path, new_path, file_id, kind, text_modified in delta.renamed: print '*** renamed %s %r => %r' % (kind, old_path, new_path) if text_modified: diff_file(old_label + old_path, old_tree.get_file(file_id).readlines(), new_label + new_path, new_tree.get_file(file_id).readlines(), to_file) for path, file_id, kind in delta.modified: print '*** modified %s %r' % (kind, path) if kind == 'file': diff_file(old_label + path, old_tree.get_file(file_id).readlines(), new_label + path, new_tree.get_file(file_id).readlines(), to_file) class TreeDelta(object): """Describes changes from one tree to another. Contains four lists: added (path, id, kind) removed (path, id, kind) renamed (oldpath, newpath, id, kind, text_modified) modified (path, id, kind) unchanged (path, id, kind) Each id is listed only once. Files that are both modified and renamed are listed only in renamed, with the text_modified flag true. The lists are normally sorted when the delta is created. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] self.unchanged = [] def touches_file_id(self, file_id): """Return True if file_id is modified by this delta.""" for l in self.added, self.removed, self.modified: for v in l: if v[1] == file_id: return True for v in self.renamed: if v[2] == file_id: return True return False def show(self, to_file, show_ids=False, show_unchanged=False): def show_list(files): for path, fid, kind in files: if kind == 'directory': path += '/' elif kind == 'symlink': path += '@' if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if self.removed: print >>to_file, 'removed:' show_list(self.removed) if self.added: print >>to_file, 'added:' show_list(self.added) if self.renamed: print >>to_file, 'renamed:' for oldpath, newpath, fid, kind, text_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if self.modified: print >>to_file, 'modified:' show_list(self.modified) if show_unchanged and self.unchanged: print >>to_file, 'unchanged:' show_list(self.unchanged) def compare_trees(old_tree, new_tree, want_unchanged, specific_files=None): """Describe changes from one tree to another. Returns a TreeDelta with details of added, modified, renamed, and deleted entries. The root entry is specifically exempt. This only considers versioned files. want_unchanged If true, also list files unchanged from one version to the next. specific_files If true, only check for changes to specified names or files within them. """ from osutils import is_inside_any old_inv = old_tree.inventory new_inv = new_tree.inventory delta = TreeDelta() mutter('start compare_trees') # TODO: match for specific files can be rather smarter by finding # the IDs of those files up front and then considering only that. for file_id in old_tree: if file_id in new_tree: kind = old_inv.get_file_kind(file_id) assert kind == new_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ 'invalid file kind %r' % kind if kind == 'root_directory': continue old_path = old_inv.id2path(file_id) new_path = new_inv.id2path(file_id) if specific_files: if (not is_inside_any(specific_files, old_path) and not is_inside_any(specific_files, new_path)): continue if kind == 'file': old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False # TODO: Can possibly avoid calculating path strings if the # two files are unchanged and their names and parents are # the same and the parents are unchanged all the way up. # May not be worthwhile. if old_path != new_path: delta.renamed.append((old_path, new_path, file_id, kind, text_modified)) elif text_modified: delta.modified.append((new_path, file_id, kind)) elif want_unchanged: delta.unchanged.append((new_path, file_id, kind)) else: kind = old_inv.get_file_kind(file_id) old_path = old_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, old_path): continue delta.removed.append((old_path, file_id, kind)) mutter('start looking for new files') for file_id in new_inv: if file_id in old_inv: continue new_path = new_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, new_path): continue kind = new_inv.get_file_kind(file_id) delta.added.append((new_path, file_id, kind)) delta.removed.sort() delta.added.sort() delta.renamed.sort() delta.modified.sort() delta.unchanged.sort() return delta commit refs/heads/master mark :569 committer Martin Pool 1117158628 +1000 data 36 - still use internal diff by default from :568 M 644 inline bzrlib/diff.py data 11775 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from trace import mutter from errors import BzrError def internal_diff(old_label, oldlines, new_label, newlines, to_file): import difflib # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, fromfile=old_label, tofile=new_label) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') to_file.writelines(ud) if nonl: print >>to_file, "\\ No newline at end of file" print >>to_file def external_diff(old_label, oldlines, new_label, newlines, to_file): """Display a diff by calling out to the external diff program.""" import sys if to_file != sys.stdout: raise NotImplementedError("sorry, can't send external diff other than to stdout yet", to_file) from tempfile import NamedTemporaryFile from os import system oldtmpf = NamedTemporaryFile() newtmpf = NamedTemporaryFile() try: # TODO: perhaps a special case for comparing to or from the empty # sequence; can just use /dev/null on Unix # TODO: if either of the files being compared already exists as a # regular named file (e.g. in the working directory) then we can # compare directly to that, rather than copying it. # TODO: Set the labels appropriately oldtmpf.writelines(oldlines) newtmpf.writelines(newlines) oldtmpf.flush() newtmpf.flush() system('diff -u --label %s %s --label %s %s' % (old_label, oldtmpf.name, new_label, newtmpf.name)) finally: oldtmpf.close() # and delete newtmpf.close() def diff_file(old_label, oldlines, new_label, newlines, to_file): if False: differ = external_diff else: differ = internal_diff differ(old_label, oldlines, new_label, newlines, to_file) def show_diff(b, revision, specific_files): import sys if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() show_diff_trees(old_tree, new_tree, sys.stdout, specific_files) def show_diff_trees(old_tree, new_tree, to_file, specific_files=None): """Show in text form the changes from one tree to another. to_files If set, include only changes to these files. """ # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. delta = compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=specific_files) for path, file_id, kind in delta.removed: print '*** removed %s %r' % (kind, path) if kind == 'file': diff_file(old_label + path, old_tree.get_file(file_id).readlines(), DEVNULL, [], to_file) for path, file_id, kind in delta.added: print '*** added %s %r' % (kind, path) if kind == 'file': diff_file(DEVNULL, [], new_label + path, new_tree.get_file(file_id).readlines(), to_file) for old_path, new_path, file_id, kind, text_modified in delta.renamed: print '*** renamed %s %r => %r' % (kind, old_path, new_path) if text_modified: diff_file(old_label + old_path, old_tree.get_file(file_id).readlines(), new_label + new_path, new_tree.get_file(file_id).readlines(), to_file) for path, file_id, kind in delta.modified: print '*** modified %s %r' % (kind, path) if kind == 'file': diff_file(old_label + path, old_tree.get_file(file_id).readlines(), new_label + path, new_tree.get_file(file_id).readlines(), to_file) class TreeDelta(object): """Describes changes from one tree to another. Contains four lists: added (path, id, kind) removed (path, id, kind) renamed (oldpath, newpath, id, kind, text_modified) modified (path, id, kind) unchanged (path, id, kind) Each id is listed only once. Files that are both modified and renamed are listed only in renamed, with the text_modified flag true. The lists are normally sorted when the delta is created. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] self.unchanged = [] def touches_file_id(self, file_id): """Return True if file_id is modified by this delta.""" for l in self.added, self.removed, self.modified: for v in l: if v[1] == file_id: return True for v in self.renamed: if v[2] == file_id: return True return False def show(self, to_file, show_ids=False, show_unchanged=False): def show_list(files): for path, fid, kind in files: if kind == 'directory': path += '/' elif kind == 'symlink': path += '@' if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if self.removed: print >>to_file, 'removed:' show_list(self.removed) if self.added: print >>to_file, 'added:' show_list(self.added) if self.renamed: print >>to_file, 'renamed:' for oldpath, newpath, fid, kind, text_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if self.modified: print >>to_file, 'modified:' show_list(self.modified) if show_unchanged and self.unchanged: print >>to_file, 'unchanged:' show_list(self.unchanged) def compare_trees(old_tree, new_tree, want_unchanged, specific_files=None): """Describe changes from one tree to another. Returns a TreeDelta with details of added, modified, renamed, and deleted entries. The root entry is specifically exempt. This only considers versioned files. want_unchanged If true, also list files unchanged from one version to the next. specific_files If true, only check for changes to specified names or files within them. """ from osutils import is_inside_any old_inv = old_tree.inventory new_inv = new_tree.inventory delta = TreeDelta() mutter('start compare_trees') # TODO: match for specific files can be rather smarter by finding # the IDs of those files up front and then considering only that. for file_id in old_tree: if file_id in new_tree: kind = old_inv.get_file_kind(file_id) assert kind == new_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ 'invalid file kind %r' % kind if kind == 'root_directory': continue old_path = old_inv.id2path(file_id) new_path = new_inv.id2path(file_id) if specific_files: if (not is_inside_any(specific_files, old_path) and not is_inside_any(specific_files, new_path)): continue if kind == 'file': old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False # TODO: Can possibly avoid calculating path strings if the # two files are unchanged and their names and parents are # the same and the parents are unchanged all the way up. # May not be worthwhile. if old_path != new_path: delta.renamed.append((old_path, new_path, file_id, kind, text_modified)) elif text_modified: delta.modified.append((new_path, file_id, kind)) elif want_unchanged: delta.unchanged.append((new_path, file_id, kind)) else: kind = old_inv.get_file_kind(file_id) old_path = old_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, old_path): continue delta.removed.append((old_path, file_id, kind)) mutter('start looking for new files') for file_id in new_inv: if file_id in old_inv: continue new_path = new_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, new_path): continue kind = new_inv.get_file_kind(file_id) delta.added.append((new_path, file_id, kind)) delta.removed.sort() delta.added.sort() delta.renamed.sort() delta.modified.sort() delta.unchanged.sort() return delta commit refs/heads/master mark :570 committer Martin Pool 1117158782 +1000 data 3 doc from :569 M 644 inline TODO data 11179 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * ``bzr log DIR`` should give changes to any files within DIR. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. * Maybe also store directories in the statcache so that we can quickly identify that they still exist. * Diff should show timestamps; for files from the working directory we can use the file itself; for files from a revision we should use the commit time of the revision. Medium things ------------- * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. We should be able to just get the id for the selected files, look up their location and diff just those files. No need to traverse the entire inventories. * ``bzr status DIR`` or ``bzr diff DIR`` should report on all changes under that directory. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from ElementTree to an object when it is read in, but rather wait until the program actually wants to know about that node. * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. - Selected-file commit - Impossible selected-file commit: adding things in non-versioned directories, crossing renames, etc. * Write a reproducible benchmark, perhaps importing various kernel versions. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Paranoid mode where we never trust SHA-1 matches. * Don't commit if there are no changes unless forced. * --dry-run mode for commit? (Or maybe just run with check-command=false?) * Generally, be a bit more verbose unless --silent is specified. * Function that finds all changes to files under a given directory; perhaps log should use this if a directory is given. * XML attributes might have trouble with filenames containing \n and \r. Do we really want to support this? I think perhaps not. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. Possibly this should be done by splitting the commit function into several parts (under a single interface). It is already rather large. Decomposition: - find tree modifications and prepare in-memory inventory - export that inventory to a temporary directory - run the test in that temporary directory - if that succeeded, continue to actually finish the commit What should be done with the text of modified files while this is underway? I don't think we want to count on holding them in memory and we can't trust the working files to stay in one place so I suppose we need to move them into the text store, or otherwise into a temporary directory. If the commit does not actually complete, we would rather the content was not left behind in the stores. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` M 644 inline bzrlib/diff.py data 11729 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from trace import mutter from errors import BzrError def internal_diff(old_label, oldlines, new_label, newlines, to_file): import difflib # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, fromfile=old_label, tofile=new_label) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') to_file.writelines(ud) if nonl: print >>to_file, "\\ No newline at end of file" print >>to_file def external_diff(old_label, oldlines, new_label, newlines, to_file): """Display a diff by calling out to the external diff program.""" import sys if to_file != sys.stdout: raise NotImplementedError("sorry, can't send external diff other than to stdout yet", to_file) from tempfile import NamedTemporaryFile from os import system oldtmpf = NamedTemporaryFile() newtmpf = NamedTemporaryFile() try: # TODO: perhaps a special case for comparing to or from the empty # sequence; can just use /dev/null on Unix # TODO: if either of the files being compared already exists as a # regular named file (e.g. in the working directory) then we can # compare directly to that, rather than copying it. oldtmpf.writelines(oldlines) newtmpf.writelines(newlines) oldtmpf.flush() newtmpf.flush() system('diff -u --label %s %s --label %s %s' % (old_label, oldtmpf.name, new_label, newtmpf.name)) finally: oldtmpf.close() # and delete newtmpf.close() def diff_file(old_label, oldlines, new_label, newlines, to_file): if False: differ = external_diff else: differ = internal_diff differ(old_label, oldlines, new_label, newlines, to_file) def show_diff(b, revision, specific_files): import sys if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() show_diff_trees(old_tree, new_tree, sys.stdout, specific_files) def show_diff_trees(old_tree, new_tree, to_file, specific_files=None): """Show in text form the changes from one tree to another. to_files If set, include only changes to these files. """ # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. delta = compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=specific_files) for path, file_id, kind in delta.removed: print '*** removed %s %r' % (kind, path) if kind == 'file': diff_file(old_label + path, old_tree.get_file(file_id).readlines(), DEVNULL, [], to_file) for path, file_id, kind in delta.added: print '*** added %s %r' % (kind, path) if kind == 'file': diff_file(DEVNULL, [], new_label + path, new_tree.get_file(file_id).readlines(), to_file) for old_path, new_path, file_id, kind, text_modified in delta.renamed: print '*** renamed %s %r => %r' % (kind, old_path, new_path) if text_modified: diff_file(old_label + old_path, old_tree.get_file(file_id).readlines(), new_label + new_path, new_tree.get_file(file_id).readlines(), to_file) for path, file_id, kind in delta.modified: print '*** modified %s %r' % (kind, path) if kind == 'file': diff_file(old_label + path, old_tree.get_file(file_id).readlines(), new_label + path, new_tree.get_file(file_id).readlines(), to_file) class TreeDelta(object): """Describes changes from one tree to another. Contains four lists: added (path, id, kind) removed (path, id, kind) renamed (oldpath, newpath, id, kind, text_modified) modified (path, id, kind) unchanged (path, id, kind) Each id is listed only once. Files that are both modified and renamed are listed only in renamed, with the text_modified flag true. The lists are normally sorted when the delta is created. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] self.unchanged = [] def touches_file_id(self, file_id): """Return True if file_id is modified by this delta.""" for l in self.added, self.removed, self.modified: for v in l: if v[1] == file_id: return True for v in self.renamed: if v[2] == file_id: return True return False def show(self, to_file, show_ids=False, show_unchanged=False): def show_list(files): for path, fid, kind in files: if kind == 'directory': path += '/' elif kind == 'symlink': path += '@' if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if self.removed: print >>to_file, 'removed:' show_list(self.removed) if self.added: print >>to_file, 'added:' show_list(self.added) if self.renamed: print >>to_file, 'renamed:' for oldpath, newpath, fid, kind, text_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if self.modified: print >>to_file, 'modified:' show_list(self.modified) if show_unchanged and self.unchanged: print >>to_file, 'unchanged:' show_list(self.unchanged) def compare_trees(old_tree, new_tree, want_unchanged, specific_files=None): """Describe changes from one tree to another. Returns a TreeDelta with details of added, modified, renamed, and deleted entries. The root entry is specifically exempt. This only considers versioned files. want_unchanged If true, also list files unchanged from one version to the next. specific_files If true, only check for changes to specified names or files within them. """ from osutils import is_inside_any old_inv = old_tree.inventory new_inv = new_tree.inventory delta = TreeDelta() mutter('start compare_trees') # TODO: match for specific files can be rather smarter by finding # the IDs of those files up front and then considering only that. for file_id in old_tree: if file_id in new_tree: kind = old_inv.get_file_kind(file_id) assert kind == new_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ 'invalid file kind %r' % kind if kind == 'root_directory': continue old_path = old_inv.id2path(file_id) new_path = new_inv.id2path(file_id) if specific_files: if (not is_inside_any(specific_files, old_path) and not is_inside_any(specific_files, new_path)): continue if kind == 'file': old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False # TODO: Can possibly avoid calculating path strings if the # two files are unchanged and their names and parents are # the same and the parents are unchanged all the way up. # May not be worthwhile. if old_path != new_path: delta.renamed.append((old_path, new_path, file_id, kind, text_modified)) elif text_modified: delta.modified.append((new_path, file_id, kind)) elif want_unchanged: delta.unchanged.append((new_path, file_id, kind)) else: kind = old_inv.get_file_kind(file_id) old_path = old_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, old_path): continue delta.removed.append((old_path, file_id, kind)) mutter('start looking for new files') for file_id in new_inv: if file_id in old_inv: continue new_path = new_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, new_path): continue kind = new_inv.get_file_kind(file_id) delta.added.append((new_path, file_id, kind)) delta.removed.sort() delta.added.sort() delta.renamed.sort() delta.modified.sort() delta.unchanged.sort() return delta commit refs/heads/master mark :571 committer Martin Pool 1117162670 +1000 data 78 - new --diff-options to pass options through to external diff and turn it on from :570 M 644 inline NEWS data 7901 bzr-0.0.5 NOT RELEASED YET CHANGES: * ``bzr`` with no command now shows help rather than giving an error. Suggested by Michael Ellerman. * ``bzr status`` output format changed, because svn-style output doesn't really match the model of bzr. Now files are grouped by status and can be shown with their IDs. ``bzr status --all`` shows all versioned files and unknown files but not ignored files. * ``bzr log`` runs from most-recent to least-recent, the reverse of the previous order. The previous behaviour can be obtained with the ``--forward`` option. ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New ``bzr ignore PATTERN`` command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. File may be in a branch other than the working directory. * ``bzr log`` and ``bzr root`` can be given an http URL instead of a filename. * Commands can now be defined by external programs or scripts in a directory on $BZRPATH. * New "stat cache" avoids reading the contents of files if they haven't changed since the previous time. * If the Python interpreter is too old, try to find a better one or give an error. Based on a patch from Fredrik Lundh. * New optional parameter ``bzr info [BRANCH]``. * New form ``bzr commit SELECTED`` to commit only selected files. * New form ``bzr log -r FROM:TO`` shows changes in selected range; contributed by John A Meinel. * New option ``bzr diff --diff-options 'OPTS'`` allows passing options through to an external GNU diff. BUG FIXES: * Fixed diff format so that added and removed files will be handled properly by patch. Fix from Lalo Martins. * Various fixes for files whose names contain spaces or other metacharacters. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. * testbzr requires python2.4, but can be used to test bzr running under a different version. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. * New developer commands ``added``, ``modified``. PORTABILITY: * Cope on Windows on python2.3 by using the weaker random seed. 2.4 is now only recommended. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 38178 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, time, os.path import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date from bzrlib import merge def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0], lock_mode='r') file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.', lock_mode='r') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0], lock_mode='r') file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.', lock_mode='r') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename, lock_mode='r') fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.', lock_mode='r') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout) show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures return 1 else: print return 0 class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): merge.merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'forward': None, 'message': unicode, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/diff.py data 13372 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from trace import mutter from errors import BzrError def internal_diff(old_label, oldlines, new_label, newlines, to_file): import difflib # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, fromfile=old_label, tofile=new_label) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') to_file.writelines(ud) if nonl: print >>to_file, "\\ No newline at end of file" print >>to_file def external_diff(old_label, oldlines, new_label, newlines, to_file, diff_opts): """Display a diff by calling out to the external diff program.""" import sys if to_file != sys.stdout: raise NotImplementedError("sorry, can't send external diff other than to stdout yet", to_file) from tempfile import NamedTemporaryFile import os oldtmpf = NamedTemporaryFile() newtmpf = NamedTemporaryFile() try: # TODO: perhaps a special case for comparing to or from the empty # sequence; can just use /dev/null on Unix # TODO: if either of the files being compared already exists as a # regular named file (e.g. in the working directory) then we can # compare directly to that, rather than copying it. oldtmpf.writelines(oldlines) newtmpf.writelines(newlines) oldtmpf.flush() newtmpf.flush() if not diff_opts: diff_opts = [] diffcmd = ['diff', '--label', old_label, oldtmpf.name, '--label', new_label, newtmpf.name] # diff only allows one style to be specified; they don't override. # note that some of these take optargs, and the optargs can be # directly appended to the options. # this is only an approximate parser; it doesn't properly understand # the grammar. for s in ['-c', '-u', '-C', '-U', '-e', '--ed', '-q', '--brief', '--normal', '-n', '--rcs', '-y', '--side-by-side', '-D', '--ifdef']: for j in diff_opts: if j.startswith(s): break else: continue break else: diffcmd.append('-u') if diff_opts: diffcmd.extend(diff_opts) rc = os.spawnvp(os.P_WAIT, 'diff', diffcmd) if rc != 0 and rc != 1: # returns 1 if files differ; that's OK if rc < 0: msg = 'signal %d' % (-rc) else: msg = 'exit code %d' % rc raise BzrError('external diff failed with %s; command: %r' % (rc, diffcmd)) finally: oldtmpf.close() # and delete newtmpf.close() def show_diff(b, revision, specific_files, external_diff_options=None): import sys if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() show_diff_trees(old_tree, new_tree, sys.stdout, specific_files, external_diff_options) def show_diff_trees(old_tree, new_tree, to_file, specific_files=None, external_diff_options=None): """Show in text form the changes from one tree to another. to_files If set, include only changes to these files. external_diff_options If set, use an external GNU diff and pass these options. """ # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. if external_diff_options: assert isinstance(external_diff_options, basestring) opts = external_diff_options.split() def diff_file(olab, olines, nlab, nlines, to_file): external_diff(olab, olines, nlab, nlines, to_file, opts) else: diff_file = internal_diff delta = compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=specific_files) for path, file_id, kind in delta.removed: print '*** removed %s %r' % (kind, path) if kind == 'file': diff_file(old_label + path, old_tree.get_file(file_id).readlines(), DEVNULL, [], to_file) for path, file_id, kind in delta.added: print '*** added %s %r' % (kind, path) if kind == 'file': diff_file(DEVNULL, [], new_label + path, new_tree.get_file(file_id).readlines(), to_file) for old_path, new_path, file_id, kind, text_modified in delta.renamed: print '*** renamed %s %r => %r' % (kind, old_path, new_path) if text_modified: diff_file(old_label + old_path, old_tree.get_file(file_id).readlines(), new_label + new_path, new_tree.get_file(file_id).readlines(), to_file) for path, file_id, kind in delta.modified: print '*** modified %s %r' % (kind, path) if kind == 'file': diff_file(old_label + path, old_tree.get_file(file_id).readlines(), new_label + path, new_tree.get_file(file_id).readlines(), to_file) class TreeDelta(object): """Describes changes from one tree to another. Contains four lists: added (path, id, kind) removed (path, id, kind) renamed (oldpath, newpath, id, kind, text_modified) modified (path, id, kind) unchanged (path, id, kind) Each id is listed only once. Files that are both modified and renamed are listed only in renamed, with the text_modified flag true. The lists are normally sorted when the delta is created. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] self.unchanged = [] def touches_file_id(self, file_id): """Return True if file_id is modified by this delta.""" for l in self.added, self.removed, self.modified: for v in l: if v[1] == file_id: return True for v in self.renamed: if v[2] == file_id: return True return False def show(self, to_file, show_ids=False, show_unchanged=False): def show_list(files): for path, fid, kind in files: if kind == 'directory': path += '/' elif kind == 'symlink': path += '@' if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if self.removed: print >>to_file, 'removed:' show_list(self.removed) if self.added: print >>to_file, 'added:' show_list(self.added) if self.renamed: print >>to_file, 'renamed:' for oldpath, newpath, fid, kind, text_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if self.modified: print >>to_file, 'modified:' show_list(self.modified) if show_unchanged and self.unchanged: print >>to_file, 'unchanged:' show_list(self.unchanged) def compare_trees(old_tree, new_tree, want_unchanged, specific_files=None): """Describe changes from one tree to another. Returns a TreeDelta with details of added, modified, renamed, and deleted entries. The root entry is specifically exempt. This only considers versioned files. want_unchanged If true, also list files unchanged from one version to the next. specific_files If true, only check for changes to specified names or files within them. """ from osutils import is_inside_any old_inv = old_tree.inventory new_inv = new_tree.inventory delta = TreeDelta() mutter('start compare_trees') # TODO: match for specific files can be rather smarter by finding # the IDs of those files up front and then considering only that. for file_id in old_tree: if file_id in new_tree: kind = old_inv.get_file_kind(file_id) assert kind == new_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ 'invalid file kind %r' % kind if kind == 'root_directory': continue old_path = old_inv.id2path(file_id) new_path = new_inv.id2path(file_id) if specific_files: if (not is_inside_any(specific_files, old_path) and not is_inside_any(specific_files, new_path)): continue if kind == 'file': old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False # TODO: Can possibly avoid calculating path strings if the # two files are unchanged and their names and parents are # the same and the parents are unchanged all the way up. # May not be worthwhile. if old_path != new_path: delta.renamed.append((old_path, new_path, file_id, kind, text_modified)) elif text_modified: delta.modified.append((new_path, file_id, kind)) elif want_unchanged: delta.unchanged.append((new_path, file_id, kind)) else: kind = old_inv.get_file_kind(file_id) old_path = old_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, old_path): continue delta.removed.append((old_path, file_id, kind)) mutter('start looking for new files') for file_id in new_inv: if file_id in old_inv: continue new_path = new_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, new_path): continue kind = new_inv.get_file_kind(file_id) delta.added.append((new_path, file_id, kind)) delta.removed.sort() delta.added.sort() delta.renamed.sort() delta.modified.sort() delta.unchanged.sort() return delta commit refs/heads/master mark :572 committer Martin Pool 1117165472 +1000 data 14 - trim imports from :571 M 644 inline bzrlib/commands.py data 38195 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date from bzrlib import merge def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0], lock_mode='r') file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.', lock_mode='r') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0], lock_mode='r') file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.', lock_mode='r') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename, lock_mode='r') fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.', lock_mode='r') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout) show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import codecs, os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures return 1 else: print return 0 class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): merge.merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'forward': None, 'message': unicode, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :573 committer Martin Pool 1117166825 +1000 data 30 - new exception NotBranchError from :572 M 644 inline bzrlib/errors.py data 1339 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " ###################################################################### # exceptions class BzrError(StandardError): pass class BzrCheckError(BzrError): pass class BzrCommandError(BzrError): # Error from malformed user command pass class NotBranchError(BzrError): """Specified path is not in a branch""" pass def bailout(msg, explanation=[]): ex = BzrError(msg, explanation) import trace trace._tracefile.write('* raising %s\n' % ex) raise ex commit refs/heads/master mark :574 committer Martin Pool 1117166912 +1000 data 4 todo from :573 M 644 inline TODO data 11235 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * ``bzr log DIR`` should give changes to any files within DIR. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. * Maybe also store directories in the statcache so that we can quickly identify that they still exist. * Diff should show timestamps; for files from the working directory we can use the file itself; for files from a revision we should use the commit time of the revision. Medium things ------------- * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. We should be able to just get the id for the selected files, look up their location and diff just those files. No need to traverse the entire inventories. * ``bzr status DIR`` or ``bzr diff DIR`` should report on all changes under that directory. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from ElementTree to an object when it is read in, but rather wait until the program actually wants to know about that node. * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. - Selected-file commit - Impossible selected-file commit: adding things in non-versioned directories, crossing renames, etc. * Write a reproducible benchmark, perhaps importing various kernel versions. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Paranoid mode where we never trust SHA-1 matches. * Don't commit if there are no changes unless forced. * --dry-run mode for commit? (Or maybe just run with check-command=false?) * Generally, be a bit more verbose unless --silent is specified. * Function that finds all changes to files under a given directory; perhaps log should use this if a directory is given. * XML attributes might have trouble with filenames containing \n and \r. Do we really want to support this? I think perhaps not. * Remember execute bits, so that exports will work OK. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. Possibly this should be done by splitting the commit function into several parts (under a single interface). It is already rather large. Decomposition: - find tree modifications and prepare in-memory inventory - export that inventory to a temporary directory - run the test in that temporary directory - if that succeeded, continue to actually finish the commit What should be done with the text of modified files while this is underway? I don't think we want to count on holding them in memory and we can't trust the working files to stay in one place so I suppose we need to move them into the text store, or otherwise into a temporary directory. If the commit does not actually complete, we would rather the content was not left behind in the stores. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :575 committer Martin Pool 1117166927 +1000 data 17 - cleanup imports from :574 M 644 inline bzrlib/commands.py data 38315 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date from bzrlib import merge def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0], lock_mode='r') file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.', lock_mode='r') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0], lock_mode='r') file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.', lock_mode='r') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename, lock_mode='r') fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.', lock_mode='r') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout) show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures return 1 else: print return 0 class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): merge.merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'forward': None, 'message': unicode, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :576 committer Martin Pool 1117166970 +1000 data 118 - raise exceptions rather than using bailout() - use warnings.warning for problems that need to be fixed by developers from :575 M 644 inline bzrlib/branch.py data 27236 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch(object): """Branch holding a history of revisions. base Base directory of the branch. """ _lockmode = None base = None def __init__(self, base, init=False, find_root=True, lock_mode='w'): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): from errors import NotBranchError raise NotBranchError("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.lock(lock_mode) self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def lock(self, mode='w'): """Lock the on-disk branch, excluding other processes.""" try: import fcntl, errno if mode == 'w': lm = fcntl.LOCK_EX om = os.O_WRONLY | os.O_CREAT elif mode == 'r': lm = fcntl.LOCK_SH om = os.O_RDONLY else: raise BzrError("invalid locking mode %r" % mode) try: lockfile = os.open(self.controlfilename('branch-lock'), om) except OSError, e: if e.errno == errno.ENOENT: # might not exist on branches from <0.0.4 self.controlfile('branch-lock', 'w').close() lockfile = os.open(self.controlfilename('branch-lock'), om) else: raise e fcntl.lockf(lockfile, lm) def unlock(): fcntl.lockf(lockfile, fcntl.LOCK_UN) os.close(lockfile) self._lockmode = None self.unlock = unlock self._lockmode = mode except ImportError: import warnings warnings.warning("please write a locking method for platform %r" % sys.platform) def unlock(): self._lockmode = None self.unlock = unlock self._lockmode = mode def _need_readlock(self): if self._lockmode not in ['r', 'w']: raise BzrError('need read lock on branch, only have %r' % self._lockmode) def _need_writelock(self): if self._lockmode not in ['w']: raise BzrError('need write lock on branch, only have %r' % self._lockmode) def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): from errors import NotBranchError raise NotBranchError("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: raise BzrError('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" self._need_readlock() before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self._need_writelock() ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ self._need_writelock() # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): raise BzrError("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: raise BzrError("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" self._need_readlock() tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: raise BzrError("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability self._need_writelock() if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: raise BzrError("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" self._need_readlock() r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" self._need_readlock() i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" self._need_readlock() if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self._need_readlock() return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def commit(self, *args, **kw): """Deprecated""" from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. self._need_readlock() if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self._need_writelock() tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): raise BzrError("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): raise BzrError("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: raise BzrError("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): raise BzrError("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': raise BzrError("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self._need_writelock() ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): raise BzrError("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): raise BzrError("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': raise BzrError("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): raise BzrError("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): raise BzrError("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: raise BzrError("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): raise BzrError("destination %r already exists" % dest_path) if f_id in to_idpath: raise BzrError("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :577 committer Martin Pool 1117170623 +1000 data 38 - merge portable lock module from John from :576 M 644 inline bzrlib/lock.py data 6012 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Locking wrappers. This only does local locking using OS locks for now. This module causes two methods, lock() and unlock() to be defined in any way that works on the current platform. It is not specified whether these locks are reentrant (i.e. can be taken repeatedly by a single process) or whether they exclude different threads in a single process. Eventually we may need to use some kind of lock representation that will work on a dumb filesystem without actual locking primitives.""" import sys, os import bzrlib from trace import mutter, note, warning class LockError(Exception): """All exceptions from the lock/unlock functions should be from this exception class. They will be translated as necessary. The original exception is available as e.original_error """ def __init__(self, e=None): self.original_error = e if e: Exception.__init__(self, e) else: Exception.__init__(self) try: import fcntl LOCK_SH = fcntl.LOCK_SH LOCK_EX = fcntl.LOCK_EX LOCK_NB = fcntl.LOCK_NB def lock(f, flags): try: fcntl.flock(f, flags) except Exception, e: raise LockError(e) def unlock(f): try: fcntl.flock(f, fcntl.LOCK_UN) except Exception, e: raise LockError(e) except ImportError: try: import win32con, win32file, pywintypes LOCK_SH = 0 # the default LOCK_EX = win32con.LOCKFILE_EXCLUSIVE_LOCK LOCK_NB = win32con.LOCKFILE_FAIL_IMMEDIATELY def lock(f, flags): try: if type(f) == file: hfile = win32file._get_osfhandle(f.fileno()) else: hfile = win32file._get_osfhandle(f) overlapped = pywintypes.OVERLAPPED() win32file.LockFileEx(hfile, flags, 0, 0x7fff0000, overlapped) except Exception, e: raise LockError(e) def unlock(f): try: if type(f) == file: hfile = win32file._get_osfhandle(f.fileno()) else: hfile = win32file._get_osfhandle(f) overlapped = pywintypes.OVERLAPPED() win32file.UnlockFileEx(hfile, 0, 0x7fff0000, overlapped) except Exception, e: raise LockError(e) except ImportError: try: import msvcrt # Unfortunately, msvcrt.locking() doesn't distinguish between # read locks and write locks. Also, the way the combinations # work to get non-blocking is not the same, so we # have to write extra special functions here. LOCK_SH = 1 LOCK_EX = 2 LOCK_NB = 4 def lock(f, flags): try: # Unfortunately, msvcrt.LK_RLCK is equivalent to msvcrt.LK_LOCK # according to the comments, LK_RLCK is open the lock for writing. # Unfortunately, msvcrt.locking() also has the side effect that it # will only block for 10 seconds at most, and then it will throw an # exception, this isn't terrible, though. if type(f) == file: fpos = f.tell() fn = f.fileno() f.seek(0) else: fn = f fpos = os.lseek(fn, 0,0) os.lseek(fn, 0,0) if flags & LOCK_SH: if flags & LOCK_NB: lock_mode = msvcrt.LK_NBLCK else: lock_mode = msvcrt.LK_LOCK elif flags & LOCK_EX: if flags & LOCK_NB: lock_mode = msvcrt.LK_NBRLCK else: lock_mode = msvcrt.LK_RLCK else: raise ValueError('Invalid lock mode: %r' % flags) try: msvcrt.locking(fn, lock_mode, -1) finally: os.lseek(fn, fpos, 0) except Exception, e: raise LockError(e) def unlock(f): try: if type(f) == file: fpos = f.tell() fn = f.fileno() f.seek(0) else: fn = f fpos = os.lseek(fn, 0,0) os.lseek(fn, 0,0) try: msvcrt.locking(fn, msvcrt.LK_UNLCK, -1) finally: os.lseek(fn, fpos, 0) except Exception, e: raise LockError(e) except ImportError: from warnings import Warning warning("please write a locking method for platform %r" % sys.platform) # Creating no-op lock/unlock for now def lock(f, flags): pass def unlock(f): pass M 644 inline bzrlib/branch.py data 27279 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import BzrError from textui import show_status import lock BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch(object): """Branch holding a history of revisions. base Base directory of the branch. """ _lockmode = None base = None def __init__(self, base, init=False, find_root=True, lock_mode='w'): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): from errors import NotBranchError raise NotBranchError("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self._lockfile = None self.lock(lock_mode) self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def lock(self, mode='w'): """Lock the on-disk branch, excluding other processes.""" try: import fcntl, errno if mode == 'w': lm = fcntl.LOCK_EX om = os.O_WRONLY | os.O_CREAT elif mode == 'r': lm = fcntl.LOCK_SH om = os.O_RDONLY else: raise BzrError("invalid locking mode %r" % mode) try: lockfile = os.open(self.controlfilename('branch-lock'), om) except OSError, e: if e.errno == errno.ENOENT: # might not exist on branches from <0.0.4 self.controlfile('branch-lock', 'w').close() lockfile = os.open(self.controlfilename('branch-lock'), om) else: raise e fcntl.lockf(lockfile, lm) def unlock(): fcntl.lockf(lockfile, fcntl.LOCK_UN) os.close(lockfile) self._lockmode = None self.unlock = unlock self._lockmode = mode except ImportError: import warnings warnings.warning("please write a locking method for platform %r" % sys.platform) def unlock(): self._lockmode = None self.unlock = unlock self._lockmode = mode def _need_readlock(self): if self._lockmode not in ['r', 'w']: raise BzrError('need read lock on branch, only have %r' % self._lockmode) def _need_writelock(self): if self._lockmode not in ['w']: raise BzrError('need write lock on branch, only have %r' % self._lockmode) def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): from errors import NotBranchError raise NotBranchError("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: raise BzrError('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" self._need_readlock() before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self._need_writelock() ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ self._need_writelock() # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): raise BzrError("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: raise BzrError("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" self._need_readlock() tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: raise BzrError("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability self._need_writelock() if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: raise BzrError("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" self._need_readlock() r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" self._need_readlock() i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" self._need_readlock() if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self._need_readlock() return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def commit(self, *args, **kw): """Deprecated""" from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. self._need_readlock() if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self._need_writelock() tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): raise BzrError("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): raise BzrError("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: raise BzrError("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): raise BzrError("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': raise BzrError("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self._need_writelock() ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): raise BzrError("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): raise BzrError("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': raise BzrError("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): raise BzrError("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): raise BzrError("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: raise BzrError("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): raise BzrError("destination %r already exists" % dest_path) if f_id in to_idpath: raise BzrError("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :578 committer Martin Pool 1117171437 +1000 data 99 - start to move toward Branch.lock and unlock methods, rather than setting it in the constructor from :577 M 644 inline bzrlib/branch.py data 26932 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch(object): """Branch holding a history of revisions. base Base directory of the branch. _lock_mode None, or a duple with 'r' or 'w' for the first element and a positive count for the second. _lockfile Open file used for locking. """ base = None _lock_mode = None def __init__(self, base, init=False, find_root=True, lock_mode='w'): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): from errors import NotBranchError raise NotBranchError("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self._lockfile = self.controlfile('branch-lock', 'wb') self.lock(lock_mode) self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def __del__(self): if self._lock_mode: from warnings import warn warn("branch %r was not explicitly unlocked" % self) self.unlock() def lock(self, mode): if self._lock_mode: raise BzrError('branch %r is already locked: %r' % (self, self._lock_mode)) from bzrlib.lock import lock, LOCK_SH, LOCK_EX if mode == 'r': m = LOCK_SH elif mode == 'w': m = LOCK_EX else: raise ValueError('invalid lock mode %r' % mode) lock(self._lockfile, m) self._lock_mode = (mode, 1) def unlock(self): if not self._lock_mode: raise BzrError('branch %r is not locked' % (self)) from bzrlib.lock import unlock unlock(self._lockfile) self._lock_mode = None def _need_readlock(self): if not self._lock_mode: raise BzrError('need read lock on branch, only have %r' % self._lockmode) def _need_writelock(self): if (self._lock_mode == None) or (self._lock_mode[0] != 'w'): raise BzrError('need write lock on branch, only have %r' % self._lockmode) def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): from errors import NotBranchError raise NotBranchError("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: raise BzrError('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" self._need_readlock() before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self._need_writelock() ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ self._need_writelock() # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): raise BzrError("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: raise BzrError("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" self._need_readlock() tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: raise BzrError("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability self._need_writelock() if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: raise BzrError("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" self._need_readlock() r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" self._need_readlock() i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" self._need_readlock() if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self._need_readlock() return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def commit(self, *args, **kw): """Deprecated""" from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. self._need_readlock() if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self._need_writelock() tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): raise BzrError("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): raise BzrError("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: raise BzrError("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): raise BzrError("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': raise BzrError("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self._need_writelock() ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): raise BzrError("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): raise BzrError("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': raise BzrError("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): raise BzrError("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): raise BzrError("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: raise BzrError("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): raise BzrError("destination %r already exists" % dest_path) if f_id in to_idpath: raise BzrError("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :579 committer Martin Pool 1117416863 +1000 data 4 todo from :578 M 644 inline TODO data 11384 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * ``bzr log DIR`` should give changes to any files within DIR. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. * Maybe also store directories in the statcache so that we can quickly identify that they still exist. * Diff should show timestamps; for files from the working directory we can use the file itself; for files from a revision we should use the commit time of the revision. Medium things ------------- * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. We should be able to just get the id for the selected files, look up their location and diff just those files. No need to traverse the entire inventories. * ``bzr status DIR`` or ``bzr diff DIR`` should report on all changes under that directory. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from ElementTree to an object when it is read in, but rather wait until the program actually wants to know about that node. * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. - Selected-file commit - Impossible selected-file commit: adding things in non-versioned directories, crossing renames, etc. * Write a reproducible benchmark, perhaps importing various kernel versions. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Paranoid mode where we never trust SHA-1 matches. * Don't commit if there are no changes unless forced. * --dry-run mode for commit? (Or maybe just run with check-command=false?) * Generally, be a bit more verbose unless --silent is specified. * Function that finds all changes to files under a given directory; perhaps log should use this if a directory is given. * XML attributes might have trouble with filenames containing \n and \r. Do we really want to support this? I think perhaps not. * Remember execute bits, so that exports will work OK. * Unify smart_add and plain Branch.add(); perhaps smart_add should just build a list of files to add and pass that to the regular add function. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. Possibly this should be done by splitting the commit function into several parts (under a single interface). It is already rather large. Decomposition: - find tree modifications and prepare in-memory inventory - export that inventory to a temporary directory - run the test in that temporary directory - if that succeeded, continue to actually finish the commit What should be done with the text of modified files while this is underway? I don't think we want to count on holding them in memory and we can't trust the working files to stay in one place so I suppose we need to move them into the text store, or otherwise into a temporary directory. If the commit does not actually complete, we would rather the content was not left behind in the stores. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :580 committer Martin Pool 1117417072 +1000 data 412 - Use explicit lock methods on a branch, rather than doing it implicitly from the Branch constructor and relying on destroying the branch to release the lock. - New with_readlock, _with_writelock decorators for branch methods. - Branch locks can now be taken several times by a single caller, but they forbid upgrading or downgrading. - Don't need assertions about whether the branch is locked anymore. from :579 M 644 inline bzrlib/branch.py data 27103 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def with_writelock(method): """Method decorator for functions run with the branch locked.""" def d(self, *a, **k): # called with self set to the branch self.lock('w') try: return method(self, *a, **k) finally: self.unlock() return d def with_readlock(method): def d(self, *a, **k): self.lock('r') try: return method(self, *a, **k) finally: self.unlock() return d def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch(object): """Branch holding a history of revisions. base Base directory of the branch. _lock_mode None, or 'r' or 'w' _lock_count If _lock_mode is true, a positive count of the number of times the lock has been taken. _lockfile Open file used for locking. """ base = None _lock_mode = None _lock_count = None def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): from errors import NotBranchError raise NotBranchError("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self._lockfile = self.controlfile('branch-lock', 'wb') self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def __del__(self): if self._lock_mode: from warnings import warn warn("branch %r was not explicitly unlocked" % self) self.unlock() def lock(self, mode): if self._lock_mode: if mode == 'w' and cur_lm == 'r': raise BzrError("can't upgrade to a write lock") assert self._lock_count >= 1 self._lock_count += 1 else: from bzrlib.lock import lock, LOCK_SH, LOCK_EX if mode == 'r': m = LOCK_SH elif mode == 'w': m = LOCK_EX else: raise ValueError('invalid lock mode %r' % mode) lock(self._lockfile, m) self._lock_mode = mode self._lock_count = 1 def unlock(self): if not self._lock_mode: raise BzrError('branch %r is not locked' % (self)) if self._lock_count > 1: self._lock_count -= 1 else: assert self._lock_count == 1 from bzrlib.lock import unlock unlock(self._lockfile) self._lock_mode = self._lock_count = None def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): from errors import NotBranchError raise NotBranchError("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: raise BzrError('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) @with_readlock def read_working_inventory(self): """Read the working inventory.""" before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") @with_writelock def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Option to specify file id. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): raise BzrError("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: raise BzrError("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: raise BzrError("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) @with_writelock def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: raise BzrError("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) @with_readlock def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def commit(self, *args, **kw): """Deprecated""" from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) @with_writelock def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): raise BzrError("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): raise BzrError("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: raise BzrError("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): raise BzrError("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': raise BzrError("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) @with_writelock def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): raise BzrError("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): raise BzrError("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': raise BzrError("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): raise BzrError("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): raise BzrError("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: raise BzrError("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): raise BzrError("destination %r already exists" % dest_path) if f_id in to_idpath: raise BzrError("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/commands.py data 38210 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date from bzrlib import merge def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout) show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures return 1 else: print return 0 class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): merge.merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) ###################################################################### # main routine # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'forward': None, 'message': unicode, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/commit.py data 9014 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def commit(branch, message, timestamp=None, timezone=None, committer=None, verbose=True, specific_files=None, rev_id=None): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. specific_files If true, commit only those files. rev_id If set, use this as the new revision id. Useful for test or import commands that need to tightly control what revisions are assigned. If you duplicate a revision id that exists elsewhere it is your own fault. If null (default), a time/random revision id is generated. """ import os, time, tempfile from inventory import Inventory from osutils import isdir, isfile, sha_string, quotefn, \ local_time_offset, username, kind_marker, is_inside_any from branch import gen_file_id from errors import BzrError from revision import Revision from trace import mutter, note branch.lock('w') try: # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_tree = branch.working_tree() work_inv = work_tree.inventory inv = Inventory() basis = branch.basis_tree() basis_inv = basis.inventory missing_ids = [] if verbose: note('looking for changes...') for path, entry in work_inv.iter_entries(): ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = branch.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if specific_files and not is_inside_any(specific_files, path): if basis_inv.has_id(file_id): # carry over with previous state inv.add(basis_inv[file_id].copy()) else: # omit this from committed inventory pass continue if not work_tree.has_id(file_id): if verbose: print('deleted %s%s' % (path, kind_marker(entry.kind))) mutter(" file is missing, removing from inventory") missing_ids.append(file_id) continue inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: raise BzrError("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): raise BzrError("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): raise BzrError("%s is entered as file but is not a file" % quotefn(p)) new_sha1 = work_tree.get_file_sha1(file_id) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and old_ie.text_sha1 == new_sha1): ## assert content == basis.get_file(file_id).read() entry.text_id = old_ie.text_id entry.text_sha1 = new_sha1 entry.text_size = old_ie.text_size mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: content = file(p, 'rb').read() # calculate the sha again, just in case the file contents # changed since we updated the cache entry.text_sha1 = sha_string(content) entry.text_size = len(content) entry.text_id = gen_file_id(entry.name) branch.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: print('added %s' % path) elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): print('modified %s' % path) else: print('renamed %s' % path) for file_id in missing_ids: # Any files that have been deleted are now removed from the # working inventory. Files that were not selected for commit # are left as they were in the working inventory and ommitted # from the revision inventory. # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itbranch. if work_inv.has_id(file_id): del work_inv[file_id] if rev_id is None: rev_id = _gen_revision_id(time.time()) inv_id = rev_id inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) branch.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) branch._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = branch.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) branch.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (branch.revno() + 1)) branch.append_revision(rev_id) if verbose: note("commited r%d" % branch.revno()) finally: branch.unlock() def _gen_revision_id(when): """Return new revision-id.""" from binascii import hexlify from osutils import rand_bytes, compact_date, user_email s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s M 644 inline bzrlib/remotebranch.py data 6873 #! /usr/bin/env python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Proxy object for access to remote branches. At the moment remote branches are only for HTTP and only for read access. """ import gzip from cStringIO import StringIO import urllib2 from errors import BzrError, BzrCheckError from branch import Branch, BZR_BRANCH_FORMAT from trace import mutter # velocitynet.com.au transparently proxies connections and thereby # breaks keep-alive -- sucks! ENABLE_URLGRABBER = True if ENABLE_URLGRABBER: import urlgrabber import urlgrabber.keepalive urlgrabber.keepalive.DEBUG = 0 def get_url(path, compressed=False): try: url = path if compressed: url += '.gz' mutter("grab url %s" % url) url_f = urlgrabber.urlopen(url, keepalive=1, close_connection=0) if not compressed: return url_f else: return gzip.GzipFile(fileobj=StringIO(url_f.read())) except urllib2.URLError, e: raise BzrError("remote fetch failed: %r: %s" % (url, e)) else: def get_url(url, compressed=False): import urllib2 if compressed: url += '.gz' mutter("get_url %s" % url) url_f = urllib2.urlopen(url) if compressed: return gzip.GzipFile(fileobj=StringIO(url_f.read())) else: return url_f def _find_remote_root(url): """Return the prefix URL that corresponds to the branch root.""" orig_url = url while True: try: ff = get_url(url + '/.bzr/branch-format') fmt = ff.read() ff.close() fmt = fmt.rstrip('\r\n') if fmt != BZR_BRANCH_FORMAT.rstrip('\r\n'): raise BzrError("sorry, branch format %r not supported at url %s" % (fmt, url)) return url except urllib2.URLError: pass try: idx = url.rindex('/') except ValueError: raise BzrError('no branch root found for URL %s' % orig_url) url = url[:idx] class RemoteBranch(Branch): def __init__(self, baseurl, find_root=True): """Create new proxy for a remote branch.""" if lock_mode not in ('', 'r'): raise BzrError('lock mode %r is not supported for remote branches' % lock_mode) if find_root: self.baseurl = _find_remote_root(baseurl) else: self.baseurl = baseurl self._check_format() self.inventory_store = RemoteStore(baseurl + '/.bzr/inventory-store/') self.text_store = RemoteStore(baseurl + '/.bzr/text-store/') def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.baseurl) __repr__ = __str__ def controlfile(self, filename, mode): if mode not in ('rb', 'rt', 'r'): raise BzrError("file mode %r not supported for remote branches" % mode) return get_url(self.baseurl + '/.bzr/' + filename, False) def lock(self, mode): if mode != 'r': raise BzrError('lock mode %r not supported for remote branch %r' % (mode, self)) def unlock(self): pass def relpath(self, path): if not path.startswith(self.baseurl): raise BzrError('path %r is not under base URL %r' % (path, self.baseurl)) pl = len(self.baseurl) return path[pl:].lstrip('/') def get_revision(self, revision_id): from revision import Revision revf = get_url(self.baseurl + '/.bzr/revision-store/' + revision_id, True) r = Revision.read_xml(revf) if r.revision_id != revision_id: raise BzrCheckError('revision stored as {%s} actually contains {%s}' % (revision_id, r.revision_id)) return r class RemoteStore(object): def __init__(self, baseurl): self._baseurl = baseurl def _path(self, name): if '/' in name: raise ValueError('invalid store id', name) return self._baseurl + '/' + name def __getitem__(self, fileid): p = self._path(fileid) return get_url(p, compressed=True) def simple_walk(): """For experimental purposes, traverse many parts of a remote branch""" from revision import Revision from branch import Branch from inventory import Inventory got_invs = {} got_texts = {} print 'read history' history = get_url('/.bzr/revision-history').readlines() num_revs = len(history) for i, rev_id in enumerate(history): rev_id = rev_id.rstrip() print 'read revision %d/%d' % (i, num_revs) # python gzip needs a seekable file (!!) but the HTTP response # isn't, so we need to buffer it rev_f = get_url('/.bzr/revision-store/%s' % rev_id, compressed=True) rev = Revision.read_xml(rev_f) print rev.message inv_id = rev.inventory_id if inv_id not in got_invs: print 'get inventory %s' % inv_id inv_f = get_url('/.bzr/inventory-store/%s' % inv_id, compressed=True) inv = Inventory.read_xml(inv_f) print '%4d inventory entries' % len(inv) for path, ie in inv.iter_entries(): text_id = ie.text_id if text_id == None: continue if text_id in got_texts: continue print ' fetch %s text {%s}' % (path, text_id) text_f = get_url('/.bzr/text-store/%s' % text_id, compressed=True) got_texts[text_id] = True got_invs.add[inv_id] = True print '----' def try_me(): BASE_URL = 'http://bazaar-ng.org/bzr/bzr.dev/' b = RemoteBranch(BASE_URL) ## print '\n'.join(b.revision_history()) from log import show_log show_log(b) if __name__ == '__main__': try_me() M 644 inline bzrlib/status.py data 1931 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def show_status(branch, show_unchanged=False, specific_files=None, show_ids=False): """Display single-line status for non-ignored working files. show_all If true, show unmodified files too. specific_files If set, only show the status of files in this list. """ import sys from bzrlib.diff import compare_trees branch.lock('r') try: old = branch.basis_tree() new = branch.working_tree() delta = compare_trees(old, new, want_unchanged=show_unchanged, specific_files=specific_files) delta.show(sys.stdout, show_ids=show_ids, show_unchanged=show_unchanged) unknowns = new.unknowns() done_header = False for path in unknowns: # FIXME: Should also match if the unknown file is within a # specified directory. if specific_files: if path not in specific_files: continue if not done_header: print 'unknown:' done_header = True print ' ', path finally: branch.unlock() commit refs/heads/master mark :581 committer Martin Pool 1117417153 +1000 data 69 - make sure any bzr output is flushed before running external diff from :580 M 644 inline bzrlib/diff.py data 13460 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from trace import mutter from errors import BzrError def internal_diff(old_label, oldlines, new_label, newlines, to_file): import difflib # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, fromfile=old_label, tofile=new_label) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') to_file.writelines(ud) if nonl: print >>to_file, "\\ No newline at end of file" print >>to_file def external_diff(old_label, oldlines, new_label, newlines, to_file, diff_opts): """Display a diff by calling out to the external diff program.""" import sys if to_file != sys.stdout: raise NotImplementedError("sorry, can't send external diff other than to stdout yet", to_file) # make sure our own output is properly ordered before the diff to_file.flush() from tempfile import NamedTemporaryFile import os oldtmpf = NamedTemporaryFile() newtmpf = NamedTemporaryFile() try: # TODO: perhaps a special case for comparing to or from the empty # sequence; can just use /dev/null on Unix # TODO: if either of the files being compared already exists as a # regular named file (e.g. in the working directory) then we can # compare directly to that, rather than copying it. oldtmpf.writelines(oldlines) newtmpf.writelines(newlines) oldtmpf.flush() newtmpf.flush() if not diff_opts: diff_opts = [] diffcmd = ['diff', '--label', old_label, oldtmpf.name, '--label', new_label, newtmpf.name] # diff only allows one style to be specified; they don't override. # note that some of these take optargs, and the optargs can be # directly appended to the options. # this is only an approximate parser; it doesn't properly understand # the grammar. for s in ['-c', '-u', '-C', '-U', '-e', '--ed', '-q', '--brief', '--normal', '-n', '--rcs', '-y', '--side-by-side', '-D', '--ifdef']: for j in diff_opts: if j.startswith(s): break else: continue break else: diffcmd.append('-u') if diff_opts: diffcmd.extend(diff_opts) rc = os.spawnvp(os.P_WAIT, 'diff', diffcmd) if rc != 0 and rc != 1: # returns 1 if files differ; that's OK if rc < 0: msg = 'signal %d' % (-rc) else: msg = 'exit code %d' % rc raise BzrError('external diff failed with %s; command: %r' % (rc, diffcmd)) finally: oldtmpf.close() # and delete newtmpf.close() def show_diff(b, revision, specific_files, external_diff_options=None): import sys if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() show_diff_trees(old_tree, new_tree, sys.stdout, specific_files, external_diff_options) def show_diff_trees(old_tree, new_tree, to_file, specific_files=None, external_diff_options=None): """Show in text form the changes from one tree to another. to_files If set, include only changes to these files. external_diff_options If set, use an external GNU diff and pass these options. """ # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. if external_diff_options: assert isinstance(external_diff_options, basestring) opts = external_diff_options.split() def diff_file(olab, olines, nlab, nlines, to_file): external_diff(olab, olines, nlab, nlines, to_file, opts) else: diff_file = internal_diff delta = compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=specific_files) for path, file_id, kind in delta.removed: print '*** removed %s %r' % (kind, path) if kind == 'file': diff_file(old_label + path, old_tree.get_file(file_id).readlines(), DEVNULL, [], to_file) for path, file_id, kind in delta.added: print '*** added %s %r' % (kind, path) if kind == 'file': diff_file(DEVNULL, [], new_label + path, new_tree.get_file(file_id).readlines(), to_file) for old_path, new_path, file_id, kind, text_modified in delta.renamed: print '*** renamed %s %r => %r' % (kind, old_path, new_path) if text_modified: diff_file(old_label + old_path, old_tree.get_file(file_id).readlines(), new_label + new_path, new_tree.get_file(file_id).readlines(), to_file) for path, file_id, kind in delta.modified: print '*** modified %s %r' % (kind, path) if kind == 'file': diff_file(old_label + path, old_tree.get_file(file_id).readlines(), new_label + path, new_tree.get_file(file_id).readlines(), to_file) class TreeDelta(object): """Describes changes from one tree to another. Contains four lists: added (path, id, kind) removed (path, id, kind) renamed (oldpath, newpath, id, kind, text_modified) modified (path, id, kind) unchanged (path, id, kind) Each id is listed only once. Files that are both modified and renamed are listed only in renamed, with the text_modified flag true. The lists are normally sorted when the delta is created. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] self.unchanged = [] def touches_file_id(self, file_id): """Return True if file_id is modified by this delta.""" for l in self.added, self.removed, self.modified: for v in l: if v[1] == file_id: return True for v in self.renamed: if v[2] == file_id: return True return False def show(self, to_file, show_ids=False, show_unchanged=False): def show_list(files): for path, fid, kind in files: if kind == 'directory': path += '/' elif kind == 'symlink': path += '@' if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if self.removed: print >>to_file, 'removed:' show_list(self.removed) if self.added: print >>to_file, 'added:' show_list(self.added) if self.renamed: print >>to_file, 'renamed:' for oldpath, newpath, fid, kind, text_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if self.modified: print >>to_file, 'modified:' show_list(self.modified) if show_unchanged and self.unchanged: print >>to_file, 'unchanged:' show_list(self.unchanged) def compare_trees(old_tree, new_tree, want_unchanged, specific_files=None): """Describe changes from one tree to another. Returns a TreeDelta with details of added, modified, renamed, and deleted entries. The root entry is specifically exempt. This only considers versioned files. want_unchanged If true, also list files unchanged from one version to the next. specific_files If true, only check for changes to specified names or files within them. """ from osutils import is_inside_any old_inv = old_tree.inventory new_inv = new_tree.inventory delta = TreeDelta() mutter('start compare_trees') # TODO: match for specific files can be rather smarter by finding # the IDs of those files up front and then considering only that. for file_id in old_tree: if file_id in new_tree: kind = old_inv.get_file_kind(file_id) assert kind == new_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ 'invalid file kind %r' % kind if kind == 'root_directory': continue old_path = old_inv.id2path(file_id) new_path = new_inv.id2path(file_id) if specific_files: if (not is_inside_any(specific_files, old_path) and not is_inside_any(specific_files, new_path)): continue if kind == 'file': old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False # TODO: Can possibly avoid calculating path strings if the # two files are unchanged and their names and parents are # the same and the parents are unchanged all the way up. # May not be worthwhile. if old_path != new_path: delta.renamed.append((old_path, new_path, file_id, kind, text_modified)) elif text_modified: delta.modified.append((new_path, file_id, kind)) elif want_unchanged: delta.unchanged.append((new_path, file_id, kind)) else: kind = old_inv.get_file_kind(file_id) old_path = old_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, old_path): continue delta.removed.append((old_path, file_id, kind)) mutter('start looking for new files') for file_id in new_inv: if file_id in old_inv: continue new_path = new_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, new_path): continue kind = new_inv.get_file_kind(file_id) delta.added.append((new_path, file_id, kind)) delta.removed.sort() delta.added.sort() delta.renamed.sort() delta.modified.sort() delta.unchanged.sort() return delta commit refs/heads/master mark :582 committer Martin Pool 1117417608 +1000 data 7 cleanup from :581 M 644 inline bzrlib/commands.py data 38123 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date from bzrlib import merge def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout) show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures return 1 else: print return 0 class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): merge.merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'forward': None, 'message': unicode, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'm': 'message', 'F': 'file', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :583 committer Martin Pool 1117417657 +1000 data 33 - add -h as short name for --help from :582 M 644 inline bzrlib/commands.py data 38161 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date from bzrlib import merge def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout) show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures return 1 else: print return 0 class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): merge.merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'forward': None, 'message': unicode, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :584 committer Martin Pool 1117417780 +1000 data 51 - make relpath and revision-history hidden commands from :583 M 644 inline bzrlib/commands.py data 38197 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date from bzrlib import merge def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision'] def run(self, revision=None): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): print '%-50s %s' % (entry.file_id, path) class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout) show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures return 1 else: print return 0 class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): merge.merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'forward': None, 'message': unicode, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :585 committer Martin Pool 1117418224 +1000 data 39 - make RemoteBranch.__str__ more robust from :584 M 644 inline bzrlib/remotebranch.py data 6912 #! /usr/bin/env python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Proxy object for access to remote branches. At the moment remote branches are only for HTTP and only for read access. """ import gzip from cStringIO import StringIO import urllib2 from errors import BzrError, BzrCheckError from branch import Branch, BZR_BRANCH_FORMAT from trace import mutter # velocitynet.com.au transparently proxies connections and thereby # breaks keep-alive -- sucks! ENABLE_URLGRABBER = True if ENABLE_URLGRABBER: import urlgrabber import urlgrabber.keepalive urlgrabber.keepalive.DEBUG = 0 def get_url(path, compressed=False): try: url = path if compressed: url += '.gz' mutter("grab url %s" % url) url_f = urlgrabber.urlopen(url, keepalive=1, close_connection=0) if not compressed: return url_f else: return gzip.GzipFile(fileobj=StringIO(url_f.read())) except urllib2.URLError, e: raise BzrError("remote fetch failed: %r: %s" % (url, e)) else: def get_url(url, compressed=False): import urllib2 if compressed: url += '.gz' mutter("get_url %s" % url) url_f = urllib2.urlopen(url) if compressed: return gzip.GzipFile(fileobj=StringIO(url_f.read())) else: return url_f def _find_remote_root(url): """Return the prefix URL that corresponds to the branch root.""" orig_url = url while True: try: ff = get_url(url + '/.bzr/branch-format') fmt = ff.read() ff.close() fmt = fmt.rstrip('\r\n') if fmt != BZR_BRANCH_FORMAT.rstrip('\r\n'): raise BzrError("sorry, branch format %r not supported at url %s" % (fmt, url)) return url except urllib2.URLError: pass try: idx = url.rindex('/') except ValueError: raise BzrError('no branch root found for URL %s' % orig_url) url = url[:idx] class RemoteBranch(Branch): def __init__(self, baseurl, find_root=True): """Create new proxy for a remote branch.""" if lock_mode not in ('', 'r'): raise BzrError('lock mode %r is not supported for remote branches' % lock_mode) if find_root: self.baseurl = _find_remote_root(baseurl) else: self.baseurl = baseurl self._check_format() self.inventory_store = RemoteStore(baseurl + '/.bzr/inventory-store/') self.text_store = RemoteStore(baseurl + '/.bzr/text-store/') def __str__(self): b = getattr(self, 'baseurl', 'undefined') return '%s(%r)' % (self.__class__.__name__, b) __repr__ = __str__ def controlfile(self, filename, mode): if mode not in ('rb', 'rt', 'r'): raise BzrError("file mode %r not supported for remote branches" % mode) return get_url(self.baseurl + '/.bzr/' + filename, False) def lock(self, mode): if mode != 'r': raise BzrError('lock mode %r not supported for remote branch %r' % (mode, self)) def unlock(self): pass def relpath(self, path): if not path.startswith(self.baseurl): raise BzrError('path %r is not under base URL %r' % (path, self.baseurl)) pl = len(self.baseurl) return path[pl:].lstrip('/') def get_revision(self, revision_id): from revision import Revision revf = get_url(self.baseurl + '/.bzr/revision-store/' + revision_id, True) r = Revision.read_xml(revf) if r.revision_id != revision_id: raise BzrCheckError('revision stored as {%s} actually contains {%s}' % (revision_id, r.revision_id)) return r class RemoteStore(object): def __init__(self, baseurl): self._baseurl = baseurl def _path(self, name): if '/' in name: raise ValueError('invalid store id', name) return self._baseurl + '/' + name def __getitem__(self, fileid): p = self._path(fileid) return get_url(p, compressed=True) def simple_walk(): """For experimental purposes, traverse many parts of a remote branch""" from revision import Revision from branch import Branch from inventory import Inventory got_invs = {} got_texts = {} print 'read history' history = get_url('/.bzr/revision-history').readlines() num_revs = len(history) for i, rev_id in enumerate(history): rev_id = rev_id.rstrip() print 'read revision %d/%d' % (i, num_revs) # python gzip needs a seekable file (!!) but the HTTP response # isn't, so we need to buffer it rev_f = get_url('/.bzr/revision-store/%s' % rev_id, compressed=True) rev = Revision.read_xml(rev_f) print rev.message inv_id = rev.inventory_id if inv_id not in got_invs: print 'get inventory %s' % inv_id inv_f = get_url('/.bzr/inventory-store/%s' % inv_id, compressed=True) inv = Inventory.read_xml(inv_f) print '%4d inventory entries' % len(inv) for path, ie in inv.iter_entries(): text_id = ie.text_id if text_id == None: continue if text_id in got_texts: continue print ' fetch %s text {%s}' % (path, text_id) text_f = get_url('/.bzr/text-store/%s' % text_id, compressed=True) got_texts[text_id] = True got_invs.add[inv_id] = True print '----' def try_me(): BASE_URL = 'http://bazaar-ng.org/bzr/bzr.dev/' b = RemoteBranch(BASE_URL) ## print '\n'.join(b.revision_history()) from log import show_log show_log(b) if __name__ == '__main__': try_me() commit refs/heads/master mark :586 committer Martin Pool 1117418557 +1000 data 4 todo from :585 M 644 inline TODO data 11584 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * ``bzr log DIR`` should give changes to any files within DIR. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. Perhaps ~/.bzr/http-cache. Baz has a fairly simple cache under ~/.arch-cache, containing revision information encoded almost as a bunch of archives. Perhaps we could simply store full paths. * Maybe also store directories in the statcache so that we can quickly identify that they still exist. * Diff should show timestamps; for files from the working directory we can use the file itself; for files from a revision we should use the commit time of the revision. Medium things ------------- * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. We should be able to just get the id for the selected files, look up their location and diff just those files. No need to traverse the entire inventories. * ``bzr status DIR`` or ``bzr diff DIR`` should report on all changes under that directory. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from ElementTree to an object when it is read in, but rather wait until the program actually wants to know about that node. * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. - Selected-file commit - Impossible selected-file commit: adding things in non-versioned directories, crossing renames, etc. * Write a reproducible benchmark, perhaps importing various kernel versions. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Paranoid mode where we never trust SHA-1 matches. * Don't commit if there are no changes unless forced. * --dry-run mode for commit? (Or maybe just run with check-command=false?) * Generally, be a bit more verbose unless --silent is specified. * Function that finds all changes to files under a given directory; perhaps log should use this if a directory is given. * XML attributes might have trouble with filenames containing \n and \r. Do we really want to support this? I think perhaps not. * Remember execute bits, so that exports will work OK. * Unify smart_add and plain Branch.add(); perhaps smart_add should just build a list of files to add and pass that to the regular add function. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. Possibly this should be done by splitting the commit function into several parts (under a single interface). It is already rather large. Decomposition: - find tree modifications and prepare in-memory inventory - export that inventory to a temporary directory - run the test in that temporary directory - if that succeeded, continue to actually finish the commit What should be done with the text of modified files while this is underway? I don't think we want to count on holding them in memory and we can't trust the working files to stay in one place so I suppose we need to move them into the text store, or otherwise into a temporary directory. If the commit does not actually complete, we would rather the content was not left behind in the stores. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :587 committer Martin Pool 1117418566 +1000 data 32 - fix up locking on RemoteBranch from :586 M 644 inline bzrlib/remotebranch.py data 6753 #! /usr/bin/env python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Proxy object for access to remote branches. At the moment remote branches are only for HTTP and only for read access. """ import gzip from cStringIO import StringIO import urllib2 from errors import BzrError, BzrCheckError from branch import Branch, BZR_BRANCH_FORMAT from trace import mutter # velocitynet.com.au transparently proxies connections and thereby # breaks keep-alive -- sucks! ENABLE_URLGRABBER = True if ENABLE_URLGRABBER: import urlgrabber import urlgrabber.keepalive urlgrabber.keepalive.DEBUG = 0 def get_url(path, compressed=False): try: url = path if compressed: url += '.gz' mutter("grab url %s" % url) url_f = urlgrabber.urlopen(url, keepalive=1, close_connection=0) if not compressed: return url_f else: return gzip.GzipFile(fileobj=StringIO(url_f.read())) except urllib2.URLError, e: raise BzrError("remote fetch failed: %r: %s" % (url, e)) else: def get_url(url, compressed=False): import urllib2 if compressed: url += '.gz' mutter("get_url %s" % url) url_f = urllib2.urlopen(url) if compressed: return gzip.GzipFile(fileobj=StringIO(url_f.read())) else: return url_f def _find_remote_root(url): """Return the prefix URL that corresponds to the branch root.""" orig_url = url while True: try: ff = get_url(url + '/.bzr/branch-format') fmt = ff.read() ff.close() fmt = fmt.rstrip('\r\n') if fmt != BZR_BRANCH_FORMAT.rstrip('\r\n'): raise BzrError("sorry, branch format %r not supported at url %s" % (fmt, url)) return url except urllib2.URLError: pass try: idx = url.rindex('/') except ValueError: raise BzrError('no branch root found for URL %s' % orig_url) url = url[:idx] class RemoteBranch(Branch): def __init__(self, baseurl, find_root=True): """Create new proxy for a remote branch.""" if find_root: self.baseurl = _find_remote_root(baseurl) else: self.baseurl = baseurl self._check_format() self.inventory_store = RemoteStore(baseurl + '/.bzr/inventory-store/') self.text_store = RemoteStore(baseurl + '/.bzr/text-store/') def __str__(self): b = getattr(self, 'baseurl', 'undefined') return '%s(%r)' % (self.__class__.__name__, b) __repr__ = __str__ def controlfile(self, filename, mode): if mode not in ('rb', 'rt', 'r'): raise BzrError("file mode %r not supported for remote branches" % mode) return get_url(self.baseurl + '/.bzr/' + filename, False) def lock(self, mode): if mode != 'r': raise BzrError('lock mode %r not supported for remote branch %r' % (mode, self)) def unlock(self): pass def relpath(self, path): if not path.startswith(self.baseurl): raise BzrError('path %r is not under base URL %r' % (path, self.baseurl)) pl = len(self.baseurl) return path[pl:].lstrip('/') def get_revision(self, revision_id): from revision import Revision revf = get_url(self.baseurl + '/.bzr/revision-store/' + revision_id, True) r = Revision.read_xml(revf) if r.revision_id != revision_id: raise BzrCheckError('revision stored as {%s} actually contains {%s}' % (revision_id, r.revision_id)) return r class RemoteStore(object): def __init__(self, baseurl): self._baseurl = baseurl def _path(self, name): if '/' in name: raise ValueError('invalid store id', name) return self._baseurl + '/' + name def __getitem__(self, fileid): p = self._path(fileid) return get_url(p, compressed=True) def simple_walk(): """For experimental purposes, traverse many parts of a remote branch""" from revision import Revision from branch import Branch from inventory import Inventory got_invs = {} got_texts = {} print 'read history' history = get_url('/.bzr/revision-history').readlines() num_revs = len(history) for i, rev_id in enumerate(history): rev_id = rev_id.rstrip() print 'read revision %d/%d' % (i, num_revs) # python gzip needs a seekable file (!!) but the HTTP response # isn't, so we need to buffer it rev_f = get_url('/.bzr/revision-store/%s' % rev_id, compressed=True) rev = Revision.read_xml(rev_f) print rev.message inv_id = rev.inventory_id if inv_id not in got_invs: print 'get inventory %s' % inv_id inv_f = get_url('/.bzr/inventory-store/%s' % inv_id, compressed=True) inv = Inventory.read_xml(inv_f) print '%4d inventory entries' % len(inv) for path, ie in inv.iter_entries(): text_id = ie.text_id if text_id == None: continue if text_id in got_texts: continue print ' fetch %s text {%s}' % (path, text_id) text_f = get_url('/.bzr/text-store/%s' % text_id, compressed=True) got_texts[text_id] = True got_invs.add[inv_id] = True print '----' def try_me(): BASE_URL = 'http://bazaar-ng.org/bzr/bzr.dev/' b = RemoteBranch(BASE_URL) ## print '\n'.join(b.revision_history()) from log import show_log show_log(b) if __name__ == '__main__': try_me() commit refs/heads/master mark :588 committer Martin Pool 1117419660 +1000 data 53 - change inventory command to not show ids by default from :587 M 644 inline NEWS data 8062 bzr-0.0.5 NOT RELEASED YET CHANGES: * ``bzr`` with no command now shows help rather than giving an error. Suggested by Michael Ellerman. * ``bzr status`` output format changed, because svn-style output doesn't really match the model of bzr. Now files are grouped by status and can be shown with their IDs. ``bzr status --all`` shows all versioned files and unknown files but not ignored files. * ``bzr log`` runs from most-recent to least-recent, the reverse of the previous order. The previous behaviour can be obtained with the ``--forward`` option. * ``bzr inventory`` by default shows only filenames, and also ids if ``--show-ids`` is given, in which case the id is the second field. ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New ``bzr ignore PATTERN`` command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. File may be in a branch other than the working directory. * ``bzr log`` and ``bzr root`` can be given an http URL instead of a filename. * Commands can now be defined by external programs or scripts in a directory on $BZRPATH. * New "stat cache" avoids reading the contents of files if they haven't changed since the previous time. * If the Python interpreter is too old, try to find a better one or give an error. Based on a patch from Fredrik Lundh. * New optional parameter ``bzr info [BRANCH]``. * New form ``bzr commit SELECTED`` to commit only selected files. * New form ``bzr log -r FROM:TO`` shows changes in selected range; contributed by John A Meinel. * New option ``bzr diff --diff-options 'OPTS'`` allows passing options through to an external GNU diff. BUG FIXES: * Fixed diff format so that added and removed files will be handled properly by patch. Fix from Lalo Martins. * Various fixes for files whose names contain spaces or other metacharacters. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. * testbzr requires python2.4, but can be used to test bzr running under a different version. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. * New developer commands ``added``, ``modified``. PORTABILITY: * Cope on Windows on python2.3 by using the weaker random seed. 2.4 is now only recommended. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 38299 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date from bzrlib import merge def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout) show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures return 1 else: print return 0 class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): merge.merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'forward': None, 'message': unicode, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :589 committer Martin Pool 1117421013 +1000 data 53 - Add script with example of how to upload over rsync from :588 M 644 inline contrib/upload-bzr.dev data 737 #! /bin/sh -ex # example of how to upload a bzr tree using rsync # --include-from is used to make sure that only versioned files and # control files are copied. We use includes/excludes rather than # --files-from so that we can delete any files from the destination # that are no longer present on the source. cd ~/work/bzr bzr inventory | rsync -avvP \ . \ escudero.ubuntu.com:/srv/www.bazaar-ng.org/rsync/bzr/bzr.dev/ \ --include-from - \ --include .bzr \ --include '.bzr/**' \ --exclude-from .rsyncexclude \ --exclude-from .bzrignore \ --exclude \* \ --exclude '.*' \ --delete-excluded --delete \ commit refs/heads/master mark :590 committer Martin Pool 1117421148 +1000 data 32 - rsync upload should be quieter from :589 M 644 inline contrib/upload-bzr.dev data 735 #! /bin/sh -ex # example of how to upload a bzr tree using rsync # --include-from is used to make sure that only versioned files and # control files are copied. We use includes/excludes rather than # --files-from so that we can delete any files from the destination # that are no longer present on the source. cd ~/work/bzr bzr inventory | rsync -av \ . \ escudero.ubuntu.com:/srv/www.bazaar-ng.org/rsync/bzr/bzr.dev/ \ --include-from - \ --include .bzr \ --include '.bzr/**' \ --exclude-from .rsyncexclude \ --exclude-from .bzrignore \ --exclude \* \ --exclude '.*' \ --delete-excluded --delete \ commit refs/heads/master mark :591 committer Martin Pool 1117421979 +1000 data 14 - trim imports from :590 M 644 inline bzrlib/commands.py data 38307 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn, pumpfile, isdir, isfile from bzrlib.tree import RevisionTree, EmptyTree, Tree from bzrlib.revision import Revision from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout) show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures return 1 else: print return 0 class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'forward': None, 'message': unicode, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :592 committer Martin Pool 1117422185 +1000 data 19 - trim imports more from :591 M 644 inline bzrlib/commands.py data 38176 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): bzrlib.add.smart_add(file_list, verbose) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout) show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures return 1 else: print return 0 class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'forward': None, 'message': unicode, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :593 committer Martin Pool 1117422303 +1000 data 4 todo from :592 M 644 inline TODO data 11730 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * ``bzr log DIR`` should give changes to any files within DIR. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. Perhaps ~/.bzr/http-cache. Baz has a fairly simple cache under ~/.arch-cache, containing revision information encoded almost as a bunch of archives. Perhaps we could simply store full paths. * Maybe also store directories in the statcache so that we can quickly identify that they still exist. * Diff should show timestamps; for files from the working directory we can use the file itself; for files from a revision we should use the commit time of the revision. * Perhaps split command infrastructure from the actual command definitions. * Cleaner support for negative boolean options like --no-recurse. Medium things ------------- * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. We should be able to just get the id for the selected files, look up their location and diff just those files. No need to traverse the entire inventories. * ``bzr status DIR`` or ``bzr diff DIR`` should report on all changes under that directory. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from ElementTree to an object when it is read in, but rather wait until the program actually wants to know about that node. * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. - Selected-file commit - Impossible selected-file commit: adding things in non-versioned directories, crossing renames, etc. * Write a reproducible benchmark, perhaps importing various kernel versions. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Paranoid mode where we never trust SHA-1 matches. * Don't commit if there are no changes unless forced. * --dry-run mode for commit? (Or maybe just run with check-command=false?) * Generally, be a bit more verbose unless --silent is specified. * Function that finds all changes to files under a given directory; perhaps log should use this if a directory is given. * XML attributes might have trouble with filenames containing \n and \r. Do we really want to support this? I think perhaps not. * Remember execute bits, so that exports will work OK. * Unify smart_add and plain Branch.add(); perhaps smart_add should just build a list of files to add and pass that to the regular add function. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. Possibly this should be done by splitting the commit function into several parts (under a single interface). It is already rather large. Decomposition: - find tree modifications and prepare in-memory inventory - export that inventory to a temporary directory - run the test in that temporary directory - if that succeeded, continue to actually finish the commit What should be done with the text of modified files while this is underway? I don't think we want to count on holding them in memory and we can't trust the working files to stay in one place so I suppose we need to move them into the text store, or otherwise into a temporary directory. If the commit does not actually complete, we would rather the content was not left behind in the stores. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :594 committer Martin Pool 1117422836 +1000 data 41 - add --no-recurse option for add command from :593 M 644 inline NEWS data 8213 bzr-0.0.5 NOT RELEASED YET CHANGES: * ``bzr`` with no command now shows help rather than giving an error. Suggested by Michael Ellerman. * ``bzr status`` output format changed, because svn-style output doesn't really match the model of bzr. Now files are grouped by status and can be shown with their IDs. ``bzr status --all`` shows all versioned files and unknown files but not ignored files. * ``bzr log`` runs from most-recent to least-recent, the reverse of the previous order. The previous behaviour can be obtained with the ``--forward`` option. * ``bzr inventory`` by default shows only filenames, and also ids if ``--show-ids`` is given, in which case the id is the second field. ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New ``bzr ignore PATTERN`` command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. File may be in a branch other than the working directory. * ``bzr log`` and ``bzr root`` can be given an http URL instead of a filename. * Commands can now be defined by external programs or scripts in a directory on $BZRPATH. * New "stat cache" avoids reading the contents of files if they haven't changed since the previous time. * If the Python interpreter is too old, try to find a better one or give an error. Based on a patch from Fredrik Lundh. * New optional parameter ``bzr info [BRANCH]``. * New form ``bzr commit SELECTED`` to commit only selected files. * New form ``bzr log -r FROM:TO`` shows changes in selected range; contributed by John A Meinel. * New option ``bzr diff --diff-options 'OPTS'`` allows passing options through to an external GNU diff. * New option ``bzr add --no-recurse`` to add a directory but not their contents. BUG FIXES: * Fixed diff format so that added and removed files will be handled properly by patch. Fix from Lalo Martins. * Various fixes for files whose names contain spaces or other metacharacters. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. * testbzr requires python2.4, but can be used to test bzr running under a different version. * Tests added for many other changes in this release. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. * New developer commands ``added``, ``modified``. PORTABILITY: * Cope on Windows on python2.3 by using the weaker random seed. 2.4 is now only recommended. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 38260 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout) show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures return 1 else: print return 0 class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :595 committer Martin Pool 1117422847 +1000 data 28 - tests for add --no-recurse from :594 M 644 inline testbzr data 12198 #! /usr/bin/python # -*- coding: utf-8 -*- # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. usage: testbzr [-p PYTHON] [BZR] By default this tests the copy of bzr found in the same directory as testbzr, or the first one found on the $PATH. A copy of bzr may be given on the command line to override this, for example when applying a new test suite to an old copy of bzr or vice versa. testbzr normally invokes bzr using the same version of python as it would normally use to run -- that is, the system default python, unless that is older than 2.3. The -p option allows specification of a different Python interpreter, such as when testing that bzr still works on python2.3. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" OVERRIDE_PYTHON = None LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = BZRPATH if OVERRIDE_PYTHON: cmd.insert(0, OVERRIDE_PYTHON) logfile.write('$ %r\n' % cmd) return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) start_dir = os.getcwd() logfile = open(LOGFILENAME, 'wt', buffering=1) try: from getopt import getopt opts, args = getopt(sys.argv[1:], 'p:') for option, value in opts: if option == '-p': OVERRIDE_PYTHON = value mypath = os.path.abspath(sys.argv[0]) print '%-30s %s' % ('running tests from', mypath) global BZRPATH if args: BZRPATH = args[1] else: BZRPATH = os.path.join(os.path.split(mypath)[0], 'bzr') print '%-30s %s' % ('against bzr', BZRPATH) print '%-30s %s' % ('in directory', os.getcwd()) print '%-30s %s' % ('with python', (OVERRIDE_PYTHON or '(default)')) print print backtick([BZRPATH, 'version']) runcmd(['mkdir', TESTDIR]) cd(TESTDIR) test_root = os.getcwd() progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("internal tests") runcmd("bzr selftest") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) runcmd("bzr diff --message foo", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == 'unknown:\n test.txt\n' out = backtick("bzr status --all") assert out == "unknown:\n test.txt\n" out = backtick("bzr status test.txt --all") assert out == "unknown:\n test.txt\n" f = file('test2.txt', 'wt') f.write('goodbye cruel world...\n') f.close() out = backtick("bzr status test.txt") assert out == "unknown:\n test.txt\n" out = backtick("bzr status") assert out == ("unknown:\n" " test.txt\n" " test2.txt\n") os.unlink('test2.txt') progress("command aliases") out = backtick("bzr st --all") assert out == ("unknown:\n" " test.txt\n") out = backtick("bzr stat") assert out == ("unknown:\n" " test.txt\n") progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == ("added:\n" " test.txt\n") progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == os.path.join("sub2", "hello.txt\n") assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') assert backtick('bzr relpath sub2/hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') runcmd('bzr log') runcmd('bzr log -v') progress("file with spaces in name") mkdir('sub directory') file('sub directory/file with spaces ', 'wt').write('see how this works\n') runcmd('bzr add .') runcmd('bzr diff') runcmd('bzr commit -m add-spaces') runcmd('bzr check') runcmd('bzr log') runcmd('bzr log --forward') runcmd('bzr info') cd('..') cd('..') progress('status after remove') mkdir('status-after-remove') # see mail from William Dodé, 2005-05-25 # $ bzr init; touch a; bzr add a; bzr commit -m "add a" # * looking for changes... # added a # * commited r1 # $ bzr remove a # $ bzr status # bzr: local variable 'kind' referenced before assignment # at /vrac/python/bazaar-ng/bzrlib/diff.py:286 in compare_trees() # see ~/.bzr.log for debug information cd('status-after-remove') runcmd('bzr init') file('a', 'w').write('foo') runcmd('bzr add a') runcmd(['bzr', 'commit', '-m', 'add a']) runcmd('bzr remove a') runcmd('bzr status') cd('..') progress('ignore patterns') mkdir('ignorebranch') cd('ignorebranch') runcmd('bzr init') assert backtick('bzr unknowns') == '' file('foo.tmp', 'wt').write('tmp files are ignored') assert backtick('bzr unknowns') == '' file('foo.c', 'wt').write('int main() {}') assert backtick('bzr unknowns') == 'foo.c\n' runcmd('bzr add foo.c') assert backtick('bzr unknowns') == '' # 'ignore' works when creating the .bzignore file file('foo.blah', 'wt').write('blah') assert backtick('bzr unknowns') == 'foo.blah\n' runcmd('bzr ignore *.blah') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\n' # 'ignore' works when then .bzrignore file already exists file('garh', 'wt').write('garh') assert backtick('bzr unknowns') == 'garh\n' runcmd('bzr ignore garh') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\ngarh\n' cd('..') progress("recursive and non-recursive add") mkdir('no-recurse') cd('no-recurse') runcmd('bzr init') mkdir('foo') fp = os.path.join('foo', 'test.txt') f = file(fp, 'w') f.write('hello!\n') f.close() runcmd('bzr add --no-recurse foo') runcmd('bzr file-id foo') runcmd('bzr file-id ' + fp, 1) # not versioned yet runcmd('bzr commit -m add-dir-only') runcmd('bzr file-id ' + fp, 1) # still not versioned runcmd('bzr add foo') runcmd('bzr file-id ' + fp) runcmd('bzr commit -m add-sub-file') cd('..') progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) logfile.close() sys.stdout.writelines(file(os.path.join(start_dir, LOGFILENAME), 'rt').readlines()[-50:]) sys.exit(1) commit refs/heads/master mark :596 committer Martin Pool 1117423366 +1000 data 3 doc from :595 M 644 inline bzrlib/branch.py data 27352 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def with_writelock(method): """Method decorator for functions run with the branch locked.""" def d(self, *a, **k): # called with self set to the branch self.lock('w') try: return method(self, *a, **k) finally: self.unlock() return d def with_readlock(method): def d(self, *a, **k): self.lock('r') try: return method(self, *a, **k) finally: self.unlock() return d def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch(object): """Branch holding a history of revisions. base Base directory of the branch. _lock_mode None, or 'r' or 'w' _lock_count If _lock_mode is true, a positive count of the number of times the lock has been taken. _lockfile Open file used for locking. """ base = None _lock_mode = None _lock_count = None def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): from errors import NotBranchError raise NotBranchError("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self._lockfile = self.controlfile('branch-lock', 'wb') self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def __del__(self): if self._lock_mode: from warnings import warn warn("branch %r was not explicitly unlocked" % self) self.unlock() def lock(self, mode): if self._lock_mode: if mode == 'w' and cur_lm == 'r': raise BzrError("can't upgrade to a write lock") assert self._lock_count >= 1 self._lock_count += 1 else: from bzrlib.lock import lock, LOCK_SH, LOCK_EX if mode == 'r': m = LOCK_SH elif mode == 'w': m = LOCK_EX else: raise ValueError('invalid lock mode %r' % mode) lock(self._lockfile, m) self._lock_mode = mode self._lock_count = 1 def unlock(self): if not self._lock_mode: raise BzrError('branch %r is not locked' % (self)) if self._lock_count > 1: self._lock_count -= 1 else: assert self._lock_count == 1 from bzrlib.lock import unlock unlock(self._lockfile) self._lock_mode = self._lock_count = None def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" rp = os.path.realpath(path) # FIXME: windows if not rp.startswith(self.base): from errors import NotBranchError raise NotBranchError("path %r is not within branch %r" % (rp, self.base)) rp = rp[len(self.base):] rp = rp.lstrip(os.sep) return rp def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: raise BzrError('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) @with_readlock def read_working_inventory(self): """Read the working inventory.""" before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") @with_writelock def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. files List of paths to add, relative to the base of the tree. ids If set, use these instead of automatically generated ids. Must be the same length as the list of files, but may contain None for ids that are to be autogenerated. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): raise BzrError("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: raise BzrError("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: raise BzrError("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) @with_writelock def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: raise BzrError("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) @with_readlock def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def commit(self, *args, **kw): """Deprecated""" from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) @with_writelock def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): raise BzrError("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): raise BzrError("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: raise BzrError("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): raise BzrError("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': raise BzrError("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) @with_writelock def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): raise BzrError("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): raise BzrError("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': raise BzrError("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): raise BzrError("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): raise BzrError("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: raise BzrError("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): raise BzrError("destination %r already exists" % dest_path) if f_id in to_idpath: raise BzrError("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :597 committer Martin Pool 1117423911 +1000 data 63 - tidy up add code - always show progress messages while adding from :596 M 644 inline bzrlib/add.py data 2986 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, sys import bzrlib from errors import bailout from trace import mutter, note def smart_add(file_list, verbose=True, recurse=True): """Add files to version, optionall recursing into directories. This is designed more towards DWIM for humans than API simplicity. For the specific behaviour see the help for cmd_add(). """ from bzrlib.osutils import quotefn, kind_marker assert file_list user_list = file_list[:] assert not isinstance(file_list, basestring) b = bzrlib.branch.Branch(file_list[0], find_root=True) inv = b.read_working_inventory() tree = b.working_tree() count = 0 for f in file_list: rf = b.relpath(f) af = b.abspath(rf) kind = bzrlib.osutils.file_kind(af) if kind != 'file' and kind != 'directory': if f not in user_list: print "Skipping %s (can't add file of kind '%s')" % (f, kind) continue bailout("can't add file of kind %r" % kind) bzrlib.mutter("smart add of %r, abs=%r" % (f, af)) if bzrlib.branch.is_control_file(af): bailout("cannot add control file %r" % af) versioned = (inv.path2id(rf) != None) if rf == '': mutter("branch root doesn't need to be added") elif versioned: mutter("%r is already versioned" % f) else: file_id = bzrlib.branch.gen_file_id(rf) inv.add_path(rf, kind=kind, file_id=file_id) bzrlib.mutter("added %r kind %r file_id={%s}" % (rf, kind, file_id)) count += 1 print 'added', quotefn(f) if kind == 'directory' and recurse: for subf in os.listdir(af): subp = os.path.join(rf, subf) if subf == bzrlib.BZRDIR: mutter("skip control directory %r" % subp) elif tree.is_ignored(subp): mutter("skip ignored sub-file %r" % subp) else: mutter("queue to add sub-file %r" % subp) file_list.append(b.abspath(subp)) if count > 0: if verbose: note('added %d' % count) b._write_inventory(inv) commit refs/heads/master mark :598 committer Martin Pool 1117424120 +1000 data 3 doc from :597 M 644 inline bzrlib/add.py data 2987 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, sys import bzrlib from errors import bailout from trace import mutter, note def smart_add(file_list, verbose=True, recurse=True): """Add files to version, optionally recursing into directories. This is designed more towards DWIM for humans than API simplicity. For the specific behaviour see the help for cmd_add(). """ from bzrlib.osutils import quotefn, kind_marker assert file_list user_list = file_list[:] assert not isinstance(file_list, basestring) b = bzrlib.branch.Branch(file_list[0], find_root=True) inv = b.read_working_inventory() tree = b.working_tree() count = 0 for f in file_list: rf = b.relpath(f) af = b.abspath(rf) kind = bzrlib.osutils.file_kind(af) if kind != 'file' and kind != 'directory': if f not in user_list: print "Skipping %s (can't add file of kind '%s')" % (f, kind) continue bailout("can't add file of kind %r" % kind) bzrlib.mutter("smart add of %r, abs=%r" % (f, af)) if bzrlib.branch.is_control_file(af): bailout("cannot add control file %r" % af) versioned = (inv.path2id(rf) != None) if rf == '': mutter("branch root doesn't need to be added") elif versioned: mutter("%r is already versioned" % f) else: file_id = bzrlib.branch.gen_file_id(rf) inv.add_path(rf, kind=kind, file_id=file_id) bzrlib.mutter("added %r kind %r file_id={%s}" % (rf, kind, file_id)) count += 1 print 'added', quotefn(f) if kind == 'directory' and recurse: for subf in os.listdir(af): subp = os.path.join(rf, subf) if subf == bzrlib.BZRDIR: mutter("skip control directory %r" % subp) elif tree.is_ignored(subp): mutter("skip ignored sub-file %r" % subp) else: mutter("queue to add sub-file %r" % subp) file_list.append(b.abspath(subp)) if count > 0: if verbose: note('added %d' % count) b._write_inventory(inv) commit refs/heads/master mark :599 committer Martin Pool 1117429841 +1000 data 39 - better error reporting from smart_add from :598 M 644 inline bzrlib/add.py data 3076 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, sys import bzrlib from trace import mutter, note def smart_add(file_list, verbose=True, recurse=True): """Add files to version, optionally recursing into directories. This is designed more towards DWIM for humans than API simplicity. For the specific behaviour see the help for cmd_add(). """ from bzrlib.osutils import quotefn, kind_marker from bzrlib.errors import BadFileKindError, ForbiddenFileError assert file_list user_list = file_list[:] assert not isinstance(file_list, basestring) b = bzrlib.branch.Branch(file_list[0], find_root=True) inv = b.read_working_inventory() tree = b.working_tree() count = 0 for f in file_list: rf = b.relpath(f) af = b.abspath(rf) kind = bzrlib.osutils.file_kind(af) if kind != 'file' and kind != 'directory': if f in user_list: raise BadFileKindError("cannot add %s of type %s" % (f, kind)) else: print "skipping %s (can't add file of kind '%s')" % (f, kind) continue bzrlib.mutter("smart add of %r, abs=%r" % (f, af)) if bzrlib.branch.is_control_file(af): raise ForbiddenFileError('cannot add control file %s' % f) versioned = (inv.path2id(rf) != None) if rf == '': mutter("branch root doesn't need to be added") elif versioned: mutter("%r is already versioned" % f) else: file_id = bzrlib.branch.gen_file_id(rf) inv.add_path(rf, kind=kind, file_id=file_id) bzrlib.mutter("added %r kind %r file_id={%s}" % (rf, kind, file_id)) count += 1 print 'added', quotefn(f) if kind == 'directory' and recurse: for subf in os.listdir(af): subp = os.path.join(rf, subf) if subf == bzrlib.BZRDIR: mutter("skip control directory %r" % subp) elif tree.is_ignored(subp): mutter("skip ignored sub-file %r" % subp) else: mutter("queue to add sub-file %r" % subp) file_list.append(b.abspath(subp)) if count > 0: if verbose: note('added %d' % count) b._write_inventory(inv) M 644 inline bzrlib/errors.py data 1601 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " ###################################################################### # exceptions class BzrError(StandardError): pass class BzrCheckError(BzrError): pass class BzrCommandError(BzrError): # Error from malformed user command pass class NotBranchError(BzrError): """Specified path is not in a branch""" pass class BadFileKindError(BzrError): """Specified file is of a kind that cannot be added. (For example a symlink or device file.)""" pass class ForbiddenFileError(BzrError): """Cannot operate on a file because it is a control file.""" pass def bailout(msg, explanation=[]): ex = BzrError(msg, explanation) import trace trace._tracefile.write('* raising %s\n' % ex) raise ex commit refs/heads/master mark :600 committer Martin Pool 1117435530 +1000 data 95 - Better Branch.relpath that doesn't match on paths that have only a string prefix in common from :599 M 644 inline bzrlib/branch.py data 27867 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def with_writelock(method): """Method decorator for functions run with the branch locked.""" def d(self, *a, **k): # called with self set to the branch self.lock('w') try: return method(self, *a, **k) finally: self.unlock() return d def with_readlock(method): def d(self, *a, **k): self.lock('r') try: return method(self, *a, **k) finally: self.unlock() return d def _relpath(base, path): """Return path relative to base, or raise exception. The path may be either an absolute path or a path relative to the current working directory. Lifted out of Branch.relpath for ease of testing. os.path.commonprefix (python2.4) has a bad bug that it works just on string prefixes, assuming that '/u' is a prefix of '/u2'. This avoids that problem.""" rp = os.path.abspath(path) s = [] head = rp while len(head) >= len(base): if head == base: break head, tail = os.path.split(head) if tail: s.insert(0, tail) else: from errors import NotBranchError raise NotBranchError("path %r is not within branch %r" % (rp, base)) return os.sep.join(s) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch(object): """Branch holding a history of revisions. base Base directory of the branch. _lock_mode None, or 'r' or 'w' _lock_count If _lock_mode is true, a positive count of the number of times the lock has been taken. _lockfile Open file used for locking. """ base = None _lock_mode = None _lock_count = None def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): from errors import NotBranchError raise NotBranchError("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self._lockfile = self.controlfile('branch-lock', 'wb') self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def __del__(self): if self._lock_mode: from warnings import warn warn("branch %r was not explicitly unlocked" % self) self.unlock() def lock(self, mode): if self._lock_mode: if mode == 'w' and cur_lm == 'r': raise BzrError("can't upgrade to a write lock") assert self._lock_count >= 1 self._lock_count += 1 else: from bzrlib.lock import lock, LOCK_SH, LOCK_EX if mode == 'r': m = LOCK_SH elif mode == 'w': m = LOCK_EX else: raise ValueError('invalid lock mode %r' % mode) lock(self._lockfile, m) self._lock_mode = mode self._lock_count = 1 def unlock(self): if not self._lock_mode: raise BzrError('branch %r is not locked' % (self)) if self._lock_count > 1: self._lock_count -= 1 else: assert self._lock_count == 1 from bzrlib.lock import unlock unlock(self._lockfile) self._lock_mode = self._lock_count = None def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" return _relpath(self.base, path) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: raise BzrError('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) @with_readlock def read_working_inventory(self): """Read the working inventory.""" before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") @with_writelock def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. files List of paths to add, relative to the base of the tree. ids If set, use these instead of automatically generated ids. Must be the same length as the list of files, but may contain None for ids that are to be autogenerated. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): raise BzrError("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: raise BzrError("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: raise BzrError("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) @with_writelock def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: raise BzrError("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) @with_readlock def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def commit(self, *args, **kw): """Deprecated""" from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) @with_writelock def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): raise BzrError("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): raise BzrError("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: raise BzrError("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): raise BzrError("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': raise BzrError("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) @with_writelock def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): raise BzrError("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): raise BzrError("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': raise BzrError("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): raise BzrError("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): raise BzrError("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: raise BzrError("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): raise BzrError("destination %r already exists" % dest_path) if f_id in to_idpath: raise BzrError("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :601 committer Martin Pool 1117435655 +1000 data 41 - whitebox tests for branch path handling from :600 M 644 inline bzrlib/whitebox.py data 2197 #! /usr/bin/python from bzrlib.branch import ScratchBranch from bzrlib.errors import NotBranchError from unittest import TestCase import os, unittest def Reporter(TestResult): def startTest(self, test): super(Reporter, self).startTest(test) print test.id(), def stopTest(self, test): print class BranchPathTestCase(TestCase): """test for branch path lookups Branch.relpath and bzrlib.branch._relpath do a simple but subtle job: given a path (either relative to cwd or absolute), work out if it is inside a branch and return the path relative to the base. """ def runTest(self): from bzrlib.branch import _relpath import tempfile, shutil savedir = os.getcwdu() dtmp = tempfile.mkdtemp() def rp(p): return _relpath(dtmp, p) try: # check paths inside dtmp while standing outside it self.assertEqual(rp(os.path.join(dtmp, 'foo')), 'foo') # root = nothing self.assertEqual(rp(dtmp), '') self.assertRaises(NotBranchError, rp, '/etc') # now some near-miss operations -- note that # os.path.commonprefix gets these wrong! self.assertRaises(NotBranchError, rp, dtmp.rstrip('\\/') + '2') self.assertRaises(NotBranchError, rp, dtmp.rstrip('\\/') + '2/foo') # now operations based on relpath of files in current # directory, or nearby os.chdir(dtmp) self.assertEqual(rp('foo/bar/quux'), 'foo/bar/quux') self.assertEqual(rp('foo'), 'foo') self.assertEqual(rp('./foo'), 'foo') self.assertEqual(rp(os.path.abspath('foo')), 'foo') self.assertRaises(NotBranchError, rp, '../foo') finally: os.chdir(savedir) shutil.rmtree(dtmp) if __name__ == '__main__': unittest.main() commit refs/heads/master mark :602 committer Martin Pool 1117507519 +1000 data 125 - new statcache format: use nul field separators rather than unicode escaping to avoid troubles with backslashes in paths. from :601 M 644 inline bzrlib/statcache.py data 8829 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import stat, os, sha, time from trace import mutter from errors import BzrError, BzrCheckError """File stat cache to speed up tree comparisons. This module basically gives a quick way to find the SHA-1 and related information of a file in the working directory, without actually reading and hashing the whole file. Implementation ============== Users of this module should not need to know about how this is implemented, and in particular should not depend on the particular data which is stored or its format. This is done by maintaining a cache indexed by a file fingerprint of (path, size, mtime, ctime, ino, dev) pointing to the SHA-1. If the fingerprint has changed, we assume the file content has not changed either and the SHA-1 is therefore the same. If any of the fingerprint fields have changed then the file content *may* have changed, or it may not have. We need to reread the file contents to make sure, but this is not visible to the user or higher-level code (except as a delay of course). The mtime and ctime are stored with nanosecond fields, but not all filesystems give this level of precision. There is therefore a possible race: the file might be modified twice within a second without changing the size or mtime, and a SHA-1 cached from the first version would be wrong. We handle this by not recording a cached hash for any files which were modified in the current second and that therefore have the chance to change again before the second is up. The only known hole in this design is if the system clock jumps backwards crossing invocations of bzr. Please don't do that; use ntp to gradually adjust your clock or don't use bzr over the step. At the moment this is stored in a simple textfile; it might be nice to use a tdb instead to allow faster lookup by file-id. The cache is represented as a map from file_id to a tuple of (file_id, sha1, path, size, mtime, ctime, ino, dev). The SHA-1 is stored in memory as a hexdigest. This version of the file on disk has one line per record, and fields separated by \0 records. """ # order of fields returned by fingerprint() FP_SIZE = 0 FP_MTIME = 1 FP_CTIME = 2 FP_INO = 3 FP_DEV = 4 # order of fields in the statcache file and in the in-memory map SC_FILE_ID = 0 SC_SHA1 = 1 SC_PATH = 2 SC_SIZE = 3 SC_MTIME = 4 SC_CTIME = 5 SC_INO = 6 SC_DEV = 7 CACHE_HEADER = "### bzr statcache v4" def fingerprint(abspath): try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) def _write_cache(basedir, entries): from atomicfile import AtomicFile cachefn = os.path.join(basedir, '.bzr', 'stat-cache') outf = AtomicFile(cachefn, 'wb') try: outf.write(CACHE_HEADER + '\n') for entry in entries: if len(entry) != 8: raise ValueError("invalid statcache entry tuple %r" % entry) outf.write(entry[0].encode('utf-8')) # file id outf.write('\0') outf.write(entry[1]) # hex sha1 outf.write('\0') outf.write(entry[2].encode('utf-8')) # name for nf in entry[3:]: outf.write('\0%d' % nf) outf.write('\n') outf.commit() finally: if not outf.closed: outf.abort() def _try_write_cache(basedir, entries): try: return _write_cache(basedir, entries) except IOError, e: mutter("cannot update statcache in %s: %s" % (basedir, e)) except OSError, e: mutter("cannot update statcache in %s: %s" % (basedir, e)) def load_cache(basedir): import re cache = {} seen_paths = {} from bzrlib.trace import warning sha_re = re.compile(r'[a-f0-9]{40}') try: cachefn = os.path.join(basedir, '.bzr', 'stat-cache') cachefile = open(cachefn, 'rb') except IOError: return cache line1 = cachefile.readline().rstrip('\r\n') if line1 != CACHE_HEADER: mutter('cache header marker not found at top of %s; discarding cache' % cachefn) return cache for l in cachefile: f = l.split('\0') file_id = f[0].decode('utf-8') if file_id in cache: warning("duplicated file_id in cache: {%s}" % file_id) text_sha = f[1] if len(text_sha) != 40 or not sha_re.match(text_sha): raise BzrCheckError("invalid file SHA-1 in cache: %r" % text_sha) path = f[2].decode('utf-8') if path in seen_paths: warning("duplicated path in cache: %r" % path) seen_paths[path] = True entry = (file_id, text_sha, path) + tuple([long(x) for x in f[3:]]) if len(entry) != 8: raise ValueError("invalid statcache entry tuple %r" % entry) cache[file_id] = entry return cache def _files_from_inventory(inv): for path, ie in inv.iter_entries(): if ie.kind != 'file': continue yield ie.file_id, path def update_cache(basedir, inv, flush=False): """Update and return the cache for the branch. The returned cache may contain entries that have not been written to disk for files recently touched. flush -- discard any previous cache and recalculate from scratch. """ # load the existing cache; use information there to find a list of # files ordered by inode, which is alleged to be the fastest order # to stat the files. to_update = _files_from_inventory(inv) assert isinstance(flush, bool) if flush: cache = {} else: cache = load_cache(basedir) by_inode = [] without_inode = [] for file_id, path in to_update: if file_id in cache: by_inode.append((cache[file_id][SC_INO], file_id, path)) else: without_inode.append((file_id, path)) by_inode.sort() to_update = [a[1:] for a in by_inode] + without_inode stat_cnt = missing_cnt = new_cnt = hardcheck = change_cnt = 0 # dangerfiles have been recently touched and can't be committed to # a persistent cache yet, but they are returned to the caller. dangerfiles = [] now = int(time.time()) ## mutter('update statcache under %r' % basedir) for file_id, path in to_update: abspath = os.path.join(basedir, path) fp = fingerprint(abspath) stat_cnt += 1 cacheentry = cache.get(file_id) if fp == None: # not here if cacheentry: del cache[file_id] change_cnt += 1 missing_cnt += 1 continue elif not cacheentry: new_cnt += 1 if (fp[FP_MTIME] >= now) or (fp[FP_CTIME] >= now): dangerfiles.append(file_id) if cacheentry and (cacheentry[3:] == fp): continue # all stat fields unchanged hardcheck += 1 dig = sha.new(file(abspath, 'rb').read()).hexdigest() # We update the cache even if the digest has not changed from # last time we looked, so that the fingerprint fields will # match in future. cacheentry = (file_id, dig, path) + fp cache[file_id] = cacheentry change_cnt += 1 mutter('statcache: statted %d files, read %d files, %d changed, %d dangerous, ' '%d deleted, %d new, ' '%d in cache' % (stat_cnt, hardcheck, change_cnt, len(dangerfiles), missing_cnt, new_cnt, len(cache))) if change_cnt: mutter('updating on-disk statcache') if dangerfiles: safe_cache = cache.copy() for file_id in dangerfiles: del safe_cache[file_id] else: safe_cache = cache _try_write_cache(basedir, safe_cache.itervalues()) return cache commit refs/heads/master mark :603 committer Martin Pool 1117507906 +1000 data 3 doc from :602 M 644 inline TODO data 11794 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * ``bzr log DIR`` should give changes to any files within DIR. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. Perhaps ~/.bzr/http-cache. Baz has a fairly simple cache under ~/.arch-cache, containing revision information encoded almost as a bunch of archives. Perhaps we could simply store full paths. * Maybe also store directories in the statcache so that we can quickly identify that they still exist. * Diff should show timestamps; for files from the working directory we can use the file itself; for files from a revision we should use the commit time of the revision. * Perhaps split command infrastructure from the actual command definitions. * Cleaner support for negative boolean options like --no-recurse. * Statcache should possibly map all file paths to / separators Medium things ------------- * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. We should be able to just get the id for the selected files, look up their location and diff just those files. No need to traverse the entire inventories. * ``bzr status DIR`` or ``bzr diff DIR`` should report on all changes under that directory. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from ElementTree to an object when it is read in, but rather wait until the program actually wants to know about that node. * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. - Selected-file commit - Impossible selected-file commit: adding things in non-versioned directories, crossing renames, etc. * Write a reproducible benchmark, perhaps importing various kernel versions. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Paranoid mode where we never trust SHA-1 matches. * Don't commit if there are no changes unless forced. * --dry-run mode for commit? (Or maybe just run with check-command=false?) * Generally, be a bit more verbose unless --silent is specified. * Function that finds all changes to files under a given directory; perhaps log should use this if a directory is given. * XML attributes might have trouble with filenames containing \n and \r. Do we really want to support this? I think perhaps not. * Remember execute bits, so that exports will work OK. * Unify smart_add and plain Branch.add(); perhaps smart_add should just build a list of files to add and pass that to the regular add function. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. Possibly this should be done by splitting the commit function into several parts (under a single interface). It is already rather large. Decomposition: - find tree modifications and prepare in-memory inventory - export that inventory to a temporary directory - run the test in that temporary directory - if that succeeded, continue to actually finish the commit What should be done with the text of modified files while this is underway? I don't think we want to count on holding them in memory and we can't trust the working files to stay in one place so I suppose we need to move them into the text store, or otherwise into a temporary directory. If the commit does not actually complete, we would rather the content was not left behind in the stores. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :604 committer Martin Pool 1117507941 +1000 data 3 doc from :603 M 644 inline TODO data 11961 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * ``bzr log DIR`` should give changes to any files within DIR. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. Perhaps ~/.bzr/http-cache. Baz has a fairly simple cache under ~/.arch-cache, containing revision information encoded almost as a bunch of archives. Perhaps we could simply store full paths. * Maybe also store directories in the statcache so that we can quickly identify that they still exist. * Diff should show timestamps; for files from the working directory we can use the file itself; for files from a revision we should use the commit time of the revision. * Perhaps split command infrastructure from the actual command definitions. * Cleaner support for negative boolean options like --no-recurse. * Statcache should possibly map all file paths to / separators Medium things ------------- * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. We should be able to just get the id for the selected files, look up their location and diff just those files. No need to traverse the entire inventories. * ``bzr status DIR`` or ``bzr diff DIR`` should report on all changes under that directory. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from ElementTree to an object when it is read in, but rather wait until the program actually wants to know about that node. * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. - Selected-file commit - Impossible selected-file commit: adding things in non-versioned directories, crossing renames, etc. * Write a reproducible benchmark, perhaps importing various kernel versions. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Paranoid mode where we never trust SHA-1 matches. * Don't commit if there are no changes unless forced. * --dry-run mode for commit? (Or maybe just run with check-command=false?) * Generally, be a bit more verbose unless --silent is specified. * Function that finds all changes to files under a given directory; perhaps log should use this if a directory is given. * XML attributes might have trouble with filenames containing \n and \r. Do we really want to support this? I think perhaps not. * Remember execute bits, so that exports will work OK. * Unify smart_add and plain Branch.add(); perhaps smart_add should just build a list of files to add and pass that to the regular add function. * Function to list a directory, saying in which revision each file was last modified. Useful for web and gui interfaces, and slow to compute one file at a time. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. Possibly this should be done by splitting the commit function into several parts (under a single interface). It is already rather large. Decomposition: - find tree modifications and prepare in-memory inventory - export that inventory to a temporary directory - run the test in that temporary directory - if that succeeded, continue to actually finish the commit What should be done with the text of modified files while this is underway? I don't think we want to count on holding them in memory and we can't trust the working files to stay in one place so I suppose we need to move them into the text store, or otherwise into a temporary directory. If the commit does not actually complete, we would rather the content was not left behind in the stores. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :605 committer Martin Pool 1117508172 +1000 data 81 - patch from Lalo Martins to show version of bzr itself in the --version output from :604 M 644 inline bzrlib/commands.py data 38456 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout) show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures return 1 else: print return 0 class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? try: branch = Branch(bzrlib.__path__[0]) except BzrError: pass else: print " (bzr checkout, revision %s)" % branch.revno() print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :606 committer Martin Pool 1117508565 +1000 data 119 - new bzrlib.get_bzr_revision() tells about the history of bzr itself - also display revision-id in --version output from :605 M 644 inline bzrlib/__init__.py data 2102 # (C) 2005 Canonical Development Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr library""" from inventory import Inventory, InventoryEntry from branch import Branch, ScratchBranch, find_branch from osutils import format_date from tree import Tree from diff import compare_trees from trace import mutter, warning, open_tracefile from log import show_log import add BZRDIR = ".bzr" DEFAULT_IGNORE = ['.bzr.log', '*~', '#*#', '*$', '.#*', '.*.swp', '.*.tmp', '*.tmp', '*.bak', '*.BAK', '*.orig', '*.o', '*.obj', '*.a', '*.py[oc]', '*.so', '*.exe', '*.elc', '{arch}', 'CVS', 'CVS.adm', '.svn', '_darcs', 'SCCS', 'RCS', '*,v', 'BitKeeper', 'TAGS', '.make.state', '.sconsign', '.tmp*', '.del-*'] IGNORE_FILENAME = ".bzrignore" import locale user_encoding = locale.getpreferredencoding() del locale __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __version__ = '0.0.5pre' def get_bzr_revision(): """If bzr is run from a branch, return (revno,revid) or None""" from errors import BzrError try: branch = Branch(__path__[0]) rh = branch.revision_history() if rh: return len(rh), rh[-1] else: return None except BzrError: return None M 644 inline bzrlib/commands.py data 38410 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout) show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): failures, tests = 0, 0 import doctest, bzrlib.store bzrlib.trace.verbose = False for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.commands, bzrlib.add: mf, mt = doctest.testmod(m) failures += mf tests += mt print '%-40s %3d tests' % (m.__name__, mt), if mf: print '%3d FAILED!' % mf else: print print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures return 1 else: print return 0 class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :607 committer Martin Pool 1117508634 +1000 data 3 doc from :606 M 644 inline NEWS data 8306 bzr-0.0.5 NOT RELEASED YET CHANGES: * ``bzr`` with no command now shows help rather than giving an error. Suggested by Michael Ellerman. * ``bzr status`` output format changed, because svn-style output doesn't really match the model of bzr. Now files are grouped by status and can be shown with their IDs. ``bzr status --all`` shows all versioned files and unknown files but not ignored files. * ``bzr log`` runs from most-recent to least-recent, the reverse of the previous order. The previous behaviour can be obtained with the ``--forward`` option. * ``bzr inventory`` by default shows only filenames, and also ids if ``--show-ids`` is given, in which case the id is the second field. ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New ``bzr ignore PATTERN`` command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.swp .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. File may be in a branch other than the working directory. * ``bzr log`` and ``bzr root`` can be given an http URL instead of a filename. * Commands can now be defined by external programs or scripts in a directory on $BZRPATH. * New "stat cache" avoids reading the contents of files if they haven't changed since the previous time. * If the Python interpreter is too old, try to find a better one or give an error. Based on a patch from Fredrik Lundh. * New optional parameter ``bzr info [BRANCH]``. * New form ``bzr commit SELECTED`` to commit only selected files. * New form ``bzr log -r FROM:TO`` shows changes in selected range; contributed by John A Meinel. * New option ``bzr diff --diff-options 'OPTS'`` allows passing options through to an external GNU diff. * New option ``bzr add --no-recurse`` to add a directory but not their contents. * ``bzr --version`` now shows more information if bzr is being run from a branch. BUG FIXES: * Fixed diff format so that added and removed files will be handled properly by patch. Fix from Lalo Martins. * Various fixes for files whose names contain spaces or other metacharacters. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. * testbzr requires python2.4, but can be used to test bzr running under a different version. * Tests added for many other changes in this release. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. * New developer commands ``added``, ``modified``. PORTABILITY: * Cope on Windows on python2.3 by using the weaker random seed. 2.4 is now only recommended. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. commit refs/heads/master mark :608 committer Martin Pool 1117525364 +1000 data 92 - Split selftests out into a new module and start changing them to use the unittest module from :607 M 644 inline bzrlib/selftest.py data 1284 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def selftest(): import unittest from unittest import TestLoader import bzrlib from doctest import DocTestSuite tr = unittest.TextTestRunner(verbosity=2) suite = unittest.TestSuite() import bzrlib.whitebox suite.addTest(TestLoader().loadTestsFromModule(bzrlib.whitebox)) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.commands, bzrlib.add: suite.addTest(DocTestSuite(m)) result = tr.run(suite) return result.wasSuccessful() M 644 inline bzrlib/commands.py data 37853 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout) show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest if selftest(): return 0 else: return 1 class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/whitebox.py data 2196 #! /usr/bin/python from bzrlib.branch import ScratchBranch from bzrlib.errors import NotBranchError from unittest import TestCase import os, unittest def Reporter(TestResult): def startTest(self, test): super(Reporter, self).startTest(test) print test.id(), def stopTest(self, test): print class BranchPathTestCase(TestCase): """test for branch path lookups Branch.relpath and bzrlib.branch._relpath do a simple but subtle job: given a path (either relative to cwd or absolute), work out if it is inside a branch and return the path relative to the base. """ def runTest(self): from bzrlib.branch import _relpath import tempfile, shutil savedir = os.getcwdu() dtmp = tempfile.mkdtemp() def rp(p): return _relpath(dtmp, p) try: # check paths inside dtmp while standing outside it self.assertEqual(rp(os.path.join(dtmp, 'foo')), 'foo') # root = nothing self.assertEqual(rp(dtmp), '') self.assertRaises(NotBranchError, rp, '/etc') # now some near-miss operations -- note that # os.path.commonprefix gets these wrong! self.assertRaises(NotBranchError, rp, dtmp.rstrip('\\/') + '2') self.assertRaises(NotBranchError, rp, dtmp.rstrip('\\/') + '2/foo') # now operations based on relpath of files in current # directory, or nearby os.chdir(dtmp) self.assertEqual(rp('foo/bar/quux'), 'foo/bar/quux') self.assertEqual(rp('foo'), 'foo') self.assertEqual(rp('./foo'), 'foo') self.assertEqual(rp(os.path.abspath('foo')), 'foo') self.assertRaises(NotBranchError, rp, '../foo') finally: os.chdir(savedir) shutil.rmtree(dtmp) if __name__ == '__main__': unittest.main() commit refs/heads/master mark :609 committer Martin Pool 1117525958 +1000 data 19 - cleanup test code from :608 M 644 inline TODO data 12056 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * ``bzr log DIR`` should give changes to any files within DIR. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. Perhaps ~/.bzr/http-cache. Baz has a fairly simple cache under ~/.arch-cache, containing revision information encoded almost as a bunch of archives. Perhaps we could simply store full paths. * Maybe also store directories in the statcache so that we can quickly identify that they still exist. * Diff should show timestamps; for files from the working directory we can use the file itself; for files from a revision we should use the commit time of the revision. * Perhaps split command infrastructure from the actual command definitions. * Cleaner support for negative boolean options like --no-recurse. * Statcache should possibly map all file paths to / separators Medium things ------------- * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. We should be able to just get the id for the selected files, look up their location and diff just those files. No need to traverse the entire inventories. * ``bzr status DIR`` or ``bzr diff DIR`` should report on all changes under that directory. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from ElementTree to an object when it is read in, but rather wait until the program actually wants to know about that node. * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. - Selected-file commit - Impossible selected-file commit: adding things in non-versioned directories, crossing renames, etc. * Write a reproducible benchmark, perhaps importing various kernel versions. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Paranoid mode where we never trust SHA-1 matches. * Don't commit if there are no changes unless forced. * --dry-run mode for commit? (Or maybe just run with check-command=false?) * Generally, be a bit more verbose unless --silent is specified. * Function that finds all changes to files under a given directory; perhaps log should use this if a directory is given. * XML attributes might have trouble with filenames containing \n and \r. Do we really want to support this? I think perhaps not. * Remember execute bits, so that exports will work OK. * Unify smart_add and plain Branch.add(); perhaps smart_add should just build a list of files to add and pass that to the regular add function. * Function to list a directory, saying in which revision each file was last modified. Useful for web and gui interfaces, and slow to compute one file at a time. * unittest is standard, but the results are kind of ugly; would be nice to make it cleaner. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. Possibly this should be done by splitting the commit function into several parts (under a single interface). It is already rather large. Decomposition: - find tree modifications and prepare in-memory inventory - export that inventory to a temporary directory - run the test in that temporary directory - if that succeeded, continue to actually finish the commit What should be done with the text of modified files while this is underway? I don't think we want to count on holding them in memory and we can't trust the working files to stay in one place so I suppose we need to move them into the text store, or otherwise into a temporary directory. If the commit does not actually complete, we would rather the content was not left behind in the stores. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` M 644 inline bzrlib/selftest.py data 1285 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def selftest(): import unittest from unittest import TestLoader import bzrlib from doctest import DocTestSuite tr = unittest.TextTestRunner(verbosity=2) suite = unittest.TestSuite() import bzrlib.whitebox suite.addTest(TestLoader().loadTestsFromModule(bzrlib.whitebox)) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.tree, bzrlib.commands, bzrlib.add: suite.addTest(DocTestSuite(m)) result = tr.run(suite) return result.wasSuccessful() M 644 inline bzrlib/whitebox.py data 2022 #! /usr/bin/python from bzrlib.branch import ScratchBranch from bzrlib.errors import NotBranchError from unittest import TestCase import os, unittest class BranchPathTestCase(TestCase): """test for branch path lookups Branch.relpath and bzrlib.branch._relpath do a simple but subtle job: given a path (either relative to cwd or absolute), work out if it is inside a branch and return the path relative to the base. """ def runTest(self): from bzrlib.branch import _relpath import tempfile, shutil savedir = os.getcwdu() dtmp = tempfile.mkdtemp() def rp(p): return _relpath(dtmp, p) try: # check paths inside dtmp while standing outside it self.assertEqual(rp(os.path.join(dtmp, 'foo')), 'foo') # root = nothing self.assertEqual(rp(dtmp), '') self.assertRaises(NotBranchError, rp, '/etc') # now some near-miss operations -- note that # os.path.commonprefix gets these wrong! self.assertRaises(NotBranchError, rp, dtmp.rstrip('\\/') + '2') self.assertRaises(NotBranchError, rp, dtmp.rstrip('\\/') + '2/foo') # now operations based on relpath of files in current # directory, or nearby os.chdir(dtmp) self.assertEqual(rp('foo/bar/quux'), 'foo/bar/quux') self.assertEqual(rp('foo'), 'foo') self.assertEqual(rp('./foo'), 'foo') self.assertEqual(rp(os.path.abspath('foo')), 'foo') self.assertRaises(NotBranchError, rp, '../foo') finally: os.chdir(savedir) shutil.rmtree(dtmp) if __name__ == '__main__': unittest.main() commit refs/heads/master mark :610 committer Martin Pool 1117527044 +1000 data 77 - replace Branch.lock(mode) with separate lock_read and lock_write methods from :609 M 644 inline bzrlib/branch.py data 28205 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def with_writelock(method): """Method decorator for functions run with the branch locked.""" def d(self, *a, **k): # called with self set to the branch self.lock_write() try: return method(self, *a, **k) finally: self.unlock() return d def with_readlock(method): def d(self, *a, **k): self.lock_read() try: return method(self, *a, **k) finally: self.unlock() return d def _relpath(base, path): """Return path relative to base, or raise exception. The path may be either an absolute path or a path relative to the current working directory. Lifted out of Branch.relpath for ease of testing. os.path.commonprefix (python2.4) has a bad bug that it works just on string prefixes, assuming that '/u' is a prefix of '/u2'. This avoids that problem.""" rp = os.path.abspath(path) s = [] head = rp while len(head) >= len(base): if head == base: break head, tail = os.path.split(head) if tail: s.insert(0, tail) else: from errors import NotBranchError raise NotBranchError("path %r is not within branch %r" % (rp, base)) return os.sep.join(s) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch(object): """Branch holding a history of revisions. base Base directory of the branch. _lock_mode None, or 'r' or 'w' _lock_count If _lock_mode is true, a positive count of the number of times the lock has been taken. _lockfile Open file used for locking. """ base = None _lock_mode = None _lock_count = None def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): from errors import NotBranchError raise NotBranchError("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self._lockfile = self.controlfile('branch-lock', 'wb') self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def __del__(self): if self._lock_mode: from warnings import warn warn("branch %r was not explicitly unlocked" % self) self.unlock() def lock_write(self): if self._lock_mode: if self._lock_mode != 'w': from errors import LockError raise LockError("can't upgrade to a write lock from %r" % self._lock_mode) self._lock_count += 1 else: from bzrlib.lock import lock, LOCK_EX lock(self._lockfile, LOCK_EX) self._lock_mode = 'w' self._lock_count = 1 def lock_read(self): if self._lock_mode: assert self._lock_mode in ('r', 'w'), \ "invalid lock mode %r" % self._lock_mode self._lock_count += 1 else: from bzrlib.lock import lock, LOCK_SH lock(self._lockfile, LOCK_SH) self._lock_mode = 'r' self._lock_count = 1 def unlock(self): if not self._lock_mode: from errors import LockError raise LockError('branch %r is not locked' % (self)) if self._lock_count > 1: self._lock_count -= 1 else: assert self._lock_count == 1 from bzrlib.lock import unlock unlock(self._lockfile) self._lock_mode = self._lock_count = None def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" return _relpath(self.base, path) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: raise BzrError('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) @with_readlock def read_working_inventory(self): """Read the working inventory.""" before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") @with_writelock def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. files List of paths to add, relative to the base of the tree. ids If set, use these instead of automatically generated ids. Must be the same length as the list of files, but may contain None for ids that are to be autogenerated. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): raise BzrError("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: raise BzrError("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) def print_file(self, file, revno): """Print `file` to stdout.""" tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: raise BzrError("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) @with_writelock def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: raise BzrError("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) @with_readlock def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def commit(self, *args, **kw): """Deprecated""" from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) @with_writelock def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): raise BzrError("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): raise BzrError("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: raise BzrError("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): raise BzrError("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': raise BzrError("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) @with_writelock def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): raise BzrError("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): raise BzrError("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': raise BzrError("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): raise BzrError("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): raise BzrError("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: raise BzrError("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): raise BzrError("destination %r already exists" % dest_path) if f_id in to_idpath: raise BzrError("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: if self.base: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/commands.py data 37982 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest if selftest(): return 0 else: return 1 class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees.""" takes_args = ['other_spec', 'base_spec'] def run(self, other_spec, base_spec): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/commit.py data 9017 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def commit(branch, message, timestamp=None, timezone=None, committer=None, verbose=True, specific_files=None, rev_id=None): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. specific_files If true, commit only those files. rev_id If set, use this as the new revision id. Useful for test or import commands that need to tightly control what revisions are assigned. If you duplicate a revision id that exists elsewhere it is your own fault. If null (default), a time/random revision id is generated. """ import os, time, tempfile from inventory import Inventory from osutils import isdir, isfile, sha_string, quotefn, \ local_time_offset, username, kind_marker, is_inside_any from branch import gen_file_id from errors import BzrError from revision import Revision from trace import mutter, note branch.lock_write() try: # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_tree = branch.working_tree() work_inv = work_tree.inventory inv = Inventory() basis = branch.basis_tree() basis_inv = basis.inventory missing_ids = [] if verbose: note('looking for changes...') for path, entry in work_inv.iter_entries(): ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). entry = entry.copy() p = branch.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if specific_files and not is_inside_any(specific_files, path): if basis_inv.has_id(file_id): # carry over with previous state inv.add(basis_inv[file_id].copy()) else: # omit this from committed inventory pass continue if not work_tree.has_id(file_id): if verbose: print('deleted %s%s' % (path, kind_marker(entry.kind))) mutter(" file is missing, removing from inventory") missing_ids.append(file_id) continue inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: raise BzrError("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): raise BzrError("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): raise BzrError("%s is entered as file but is not a file" % quotefn(p)) new_sha1 = work_tree.get_file_sha1(file_id) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and old_ie.text_sha1 == new_sha1): ## assert content == basis.get_file(file_id).read() entry.text_id = old_ie.text_id entry.text_sha1 = new_sha1 entry.text_size = old_ie.text_size mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: content = file(p, 'rb').read() # calculate the sha again, just in case the file contents # changed since we updated the cache entry.text_sha1 = sha_string(content) entry.text_size = len(content) entry.text_id = gen_file_id(entry.name) branch.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: if not old_ie: print('added %s' % path) elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): print('modified %s' % path) else: print('renamed %s' % path) for file_id in missing_ids: # Any files that have been deleted are now removed from the # working inventory. Files that were not selected for commit # are left as they were in the working inventory and ommitted # from the revision inventory. # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itbranch. if work_inv.has_id(file_id): del work_inv[file_id] if rev_id is None: rev_id = _gen_revision_id(time.time()) inv_id = rev_id inv_tmp = tempfile.TemporaryFile() inv.write_xml(inv_tmp) inv_tmp.seek(0) branch.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) branch._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = branch.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) branch.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (branch.revno() + 1)) branch.append_revision(rev_id) if verbose: note("commited r%d" % branch.revno()) finally: branch.unlock() def _gen_revision_id(when): """Return new revision-id.""" from binascii import hexlify from osutils import rand_bytes, compact_date, user_email s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s M 644 inline bzrlib/errors.py data 1639 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " ###################################################################### # exceptions class BzrError(StandardError): pass class BzrCheckError(BzrError): pass class BzrCommandError(BzrError): # Error from malformed user command pass class NotBranchError(BzrError): """Specified path is not in a branch""" pass class BadFileKindError(BzrError): """Specified file is of a kind that cannot be added. (For example a symlink or device file.)""" pass class ForbiddenFileError(BzrError): """Cannot operate on a file because it is a control file.""" pass class LockError(BzrError): pass def bailout(msg, explanation=[]): ex = BzrError(msg, explanation) import trace trace._tracefile.write('* raising %s\n' % ex) raise ex M 644 inline bzrlib/status.py data 1933 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def show_status(branch, show_unchanged=False, specific_files=None, show_ids=False): """Display single-line status for non-ignored working files. show_all If true, show unmodified files too. specific_files If set, only show the status of files in this list. """ import sys from bzrlib.diff import compare_trees branch.lock_read() try: old = branch.basis_tree() new = branch.working_tree() delta = compare_trees(old, new, want_unchanged=show_unchanged, specific_files=specific_files) delta.show(sys.stdout, show_ids=show_ids, show_unchanged=show_unchanged) unknowns = new.unknowns() done_header = False for path in unknowns: # FIXME: Should also match if the unknown file is within a # specified directory. if specific_files: if path not in specific_files: continue if not done_header: print 'unknown:' done_header = True print ' ', path finally: branch.unlock() commit refs/heads/master mark :611 committer Martin Pool 1117527404 +1000 data 83 - remove @with_writelock, @with_readlock decorators which don't work in python2.3 from :610 M 644 inline bzrlib/branch.py data 28697 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def _relpath(base, path): """Return path relative to base, or raise exception. The path may be either an absolute path or a path relative to the current working directory. Lifted out of Branch.relpath for ease of testing. os.path.commonprefix (python2.4) has a bad bug that it works just on string prefixes, assuming that '/u' is a prefix of '/u2'. This avoids that problem.""" rp = os.path.abspath(path) s = [] head = rp while len(head) >= len(base): if head == base: break head, tail = os.path.split(head) if tail: s.insert(0, tail) else: from errors import NotBranchError raise NotBranchError("path %r is not within branch %r" % (rp, base)) return os.sep.join(s) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch(object): """Branch holding a history of revisions. base Base directory of the branch. _lock_mode None, or 'r' or 'w' _lock_count If _lock_mode is true, a positive count of the number of times the lock has been taken. _lockfile Open file used for locking. """ base = None _lock_mode = None _lock_count = None def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): from errors import NotBranchError raise NotBranchError("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self._lockfile = self.controlfile('branch-lock', 'wb') self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def __del__(self): if self._lock_mode: from warnings import warn warn("branch %r was not explicitly unlocked" % self) self.unlock() def lock_write(self): if self._lock_mode: if self._lock_mode != 'w': from errors import LockError raise LockError("can't upgrade to a write lock from %r" % self._lock_mode) self._lock_count += 1 else: from bzrlib.lock import lock, LOCK_EX lock(self._lockfile, LOCK_EX) self._lock_mode = 'w' self._lock_count = 1 def lock_read(self): if self._lock_mode: assert self._lock_mode in ('r', 'w'), \ "invalid lock mode %r" % self._lock_mode self._lock_count += 1 else: from bzrlib.lock import lock, LOCK_SH lock(self._lockfile, LOCK_SH) self._lock_mode = 'r' self._lock_count = 1 def unlock(self): if not self._lock_mode: from errors import LockError raise LockError('branch %r is not locked' % (self)) if self._lock_count > 1: self._lock_count -= 1 else: assert self._lock_count == 1 from bzrlib.lock import unlock unlock(self._lockfile) self._lock_mode = self._lock_count = None def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" return _relpath(self.base, path) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: raise BzrError('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. self.lock_read() try: inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv finally: self.unlock() def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. files List of paths to add, relative to the base of the tree. ids If set, use these instead of automatically generated ids. Must be the same length as the list of files, but may contain None for ids that are to be autogenerated. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) self.lock_write() try: inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): raise BzrError("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: raise BzrError("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) finally: self.unlock() def print_file(self, file, revno): """Print `file` to stdout.""" self.lock_read() try: tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: raise BzrError("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) finally: self.unlock() def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] self.lock_write() try: tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: raise BzrError("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) finally: self.unlock() # TODO: remove? def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self.lock_read() try: return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] finally: self.unlock() def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def commit(self, *args, **kw): """Deprecated""" from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self.lock_write() try: tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): raise BzrError("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): raise BzrError("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: raise BzrError("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): raise BzrError("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': raise BzrError("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self.lock_write() try: ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): raise BzrError("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): raise BzrError("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': raise BzrError("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): raise BzrError("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): raise BzrError("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: raise BzrError("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): raise BzrError("destination %r already exists" % dest_path) if f_id in to_idpath: raise BzrError("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: if self.base: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :612 committer Martin Pool 1117529767 +1000 data 3 doc from :611 M 644 inline bzrlib/branch.py data 28730 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def _relpath(base, path): """Return path relative to base, or raise exception. The path may be either an absolute path or a path relative to the current working directory. Lifted out of Branch.relpath for ease of testing. os.path.commonprefix (python2.4) has a bad bug that it works just on string prefixes, assuming that '/u' is a prefix of '/u2'. This avoids that problem.""" rp = os.path.abspath(path) s = [] head = rp while len(head) >= len(base): if head == base: break head, tail = os.path.split(head) if tail: s.insert(0, tail) else: from errors import NotBranchError raise NotBranchError("path %r is not within branch %r" % (rp, base)) return os.sep.join(s) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch(object): """Branch holding a history of revisions. base Base directory of the branch. _lock_mode None, or 'r' or 'w' _lock_count If _lock_mode is true, a positive count of the number of times the lock has been taken. _lockfile Open file used for locking. """ base = None _lock_mode = None _lock_count = None def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): from errors import NotBranchError raise NotBranchError("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self._lockfile = self.controlfile('branch-lock', 'wb') self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def __del__(self): if self._lock_mode: from warnings import warn warn("branch %r was not explicitly unlocked" % self) self.unlock() def lock_write(self): if self._lock_mode: if self._lock_mode != 'w': from errors import LockError raise LockError("can't upgrade to a write lock from %r" % self._lock_mode) self._lock_count += 1 else: from bzrlib.lock import lock, LOCK_EX lock(self._lockfile, LOCK_EX) self._lock_mode = 'w' self._lock_count = 1 def lock_read(self): if self._lock_mode: assert self._lock_mode in ('r', 'w'), \ "invalid lock mode %r" % self._lock_mode self._lock_count += 1 else: from bzrlib.lock import lock, LOCK_SH lock(self._lockfile, LOCK_SH) self._lock_mode = 'r' self._lock_count = 1 def unlock(self): if not self._lock_mode: from errors import LockError raise LockError('branch %r is not locked' % (self)) if self._lock_count > 1: self._lock_count -= 1 else: assert self._lock_count == 1 from bzrlib.lock import unlock unlock(self._lockfile) self._lock_mode = self._lock_count = None def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" return _relpath(self.base, path) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: raise BzrError('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. self.lock_read() try: inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv finally: self.unlock() def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. files List of paths to add, relative to the base of the tree. ids If set, use these instead of automatically generated ids. Must be the same length as the list of files, but may contain None for ids that are to be autogenerated. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) self.lock_write() try: inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): raise BzrError("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: raise BzrError("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) finally: self.unlock() def print_file(self, file, revno): """Print `file` to stdout.""" self.lock_read() try: tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: raise BzrError("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) finally: self.unlock() def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] self.lock_write() try: tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: raise BzrError("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) finally: self.unlock() # FIXME: this doesn't need to be a branch method def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self.lock_read() try: return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] finally: self.unlock() def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def commit(self, *args, **kw): """Deprecated""" from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self.lock_write() try: tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): raise BzrError("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): raise BzrError("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: raise BzrError("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): raise BzrError("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': raise BzrError("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self.lock_write() try: ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): raise BzrError("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): raise BzrError("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': raise BzrError("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): raise BzrError("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): raise BzrError("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: raise BzrError("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): raise BzrError("destination %r already exists" % dest_path) if f_id in to_idpath: raise BzrError("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: if self.base: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :613 committer Martin Pool 1117595778 +1000 data 64 - fix locking for RemoteBranch - add RemoteBranch.revision_store from :612 M 644 inline bzrlib/remotebranch.py data 6946 #! /usr/bin/env python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Proxy object for access to remote branches. At the moment remote branches are only for HTTP and only for read access. """ import gzip from cStringIO import StringIO import urllib2 from errors import BzrError, BzrCheckError from branch import Branch, BZR_BRANCH_FORMAT from trace import mutter # velocitynet.com.au transparently proxies connections and thereby # breaks keep-alive -- sucks! ENABLE_URLGRABBER = True if ENABLE_URLGRABBER: import urlgrabber import urlgrabber.keepalive urlgrabber.keepalive.DEBUG = 0 def get_url(path, compressed=False): try: url = path if compressed: url += '.gz' mutter("grab url %s" % url) url_f = urlgrabber.urlopen(url, keepalive=1, close_connection=0) if not compressed: return url_f else: return gzip.GzipFile(fileobj=StringIO(url_f.read())) except urllib2.URLError, e: raise BzrError("remote fetch failed: %r: %s" % (url, e)) else: def get_url(url, compressed=False): import urllib2 if compressed: url += '.gz' mutter("get_url %s" % url) url_f = urllib2.urlopen(url) if compressed: return gzip.GzipFile(fileobj=StringIO(url_f.read())) else: return url_f def _find_remote_root(url): """Return the prefix URL that corresponds to the branch root.""" orig_url = url while True: try: ff = get_url(url + '/.bzr/branch-format') fmt = ff.read() ff.close() fmt = fmt.rstrip('\r\n') if fmt != BZR_BRANCH_FORMAT.rstrip('\r\n'): raise BzrError("sorry, branch format %r not supported at url %s" % (fmt, url)) return url except urllib2.URLError: pass try: idx = url.rindex('/') except ValueError: raise BzrError('no branch root found for URL %s' % orig_url) url = url[:idx] class RemoteBranch(Branch): def __init__(self, baseurl, find_root=True): """Create new proxy for a remote branch.""" if find_root: self.baseurl = _find_remote_root(baseurl) else: self.baseurl = baseurl self._check_format() self.inventory_store = RemoteStore(baseurl + '/.bzr/inventory-store/') self.text_store = RemoteStore(baseurl + '/.bzr/text-store/') self.revision_store = RemoteStore(baseurl + '/.bzr/revision-store/') def __str__(self): b = getattr(self, 'baseurl', 'undefined') return '%s(%r)' % (self.__class__.__name__, b) __repr__ = __str__ def controlfile(self, filename, mode): if mode not in ('rb', 'rt', 'r'): raise BzrError("file mode %r not supported for remote branches" % mode) return get_url(self.baseurl + '/.bzr/' + filename, False) def lock_read(self): # no locking for remote branches yet pass def lock_write(self): from errors import LockError raise LockError("write lock not supported for remote branch %s" % self.baseurl) def unlock(self): pass def relpath(self, path): if not path.startswith(self.baseurl): raise BzrError('path %r is not under base URL %r' % (path, self.baseurl)) pl = len(self.baseurl) return path[pl:].lstrip('/') def get_revision(self, revision_id): from revision import Revision revf = get_url(self.baseurl + '/.bzr/revision-store/' + revision_id, True) r = Revision.read_xml(revf) if r.revision_id != revision_id: raise BzrCheckError('revision stored as {%s} actually contains {%s}' % (revision_id, r.revision_id)) return r class RemoteStore(object): def __init__(self, baseurl): self._baseurl = baseurl def _path(self, name): if '/' in name: raise ValueError('invalid store id', name) return self._baseurl + '/' + name def __getitem__(self, fileid): p = self._path(fileid) return get_url(p, compressed=True) def simple_walk(): """For experimental purposes, traverse many parts of a remote branch""" from revision import Revision from branch import Branch from inventory import Inventory got_invs = {} got_texts = {} print 'read history' history = get_url('/.bzr/revision-history').readlines() num_revs = len(history) for i, rev_id in enumerate(history): rev_id = rev_id.rstrip() print 'read revision %d/%d' % (i, num_revs) # python gzip needs a seekable file (!!) but the HTTP response # isn't, so we need to buffer it rev_f = get_url('/.bzr/revision-store/%s' % rev_id, compressed=True) rev = Revision.read_xml(rev_f) print rev.message inv_id = rev.inventory_id if inv_id not in got_invs: print 'get inventory %s' % inv_id inv_f = get_url('/.bzr/inventory-store/%s' % inv_id, compressed=True) inv = Inventory.read_xml(inv_f) print '%4d inventory entries' % len(inv) for path, ie in inv.iter_entries(): text_id = ie.text_id if text_id == None: continue if text_id in got_texts: continue print ' fetch %s text {%s}' % (path, text_id) text_f = get_url('/.bzr/text-store/%s' % text_id, compressed=True) got_texts[text_id] = True got_invs.add[inv_id] = True print '----' def try_me(): BASE_URL = 'http://bazaar-ng.org/bzr/bzr.dev/' b = RemoteBranch(BASE_URL) ## print '\n'.join(b.revision_history()) from log import show_log show_log(b) if __name__ == '__main__': try_me() commit refs/heads/master mark :614 committer Martin Pool 1117598978 +1000 data 35 - unify two defintions of LockError from :613 M 644 inline bzrlib/branch.py data 28728 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def _relpath(base, path): """Return path relative to base, or raise exception. The path may be either an absolute path or a path relative to the current working directory. Lifted out of Branch.relpath for ease of testing. os.path.commonprefix (python2.4) has a bad bug that it works just on string prefixes, assuming that '/u' is a prefix of '/u2'. This avoids that problem.""" rp = os.path.abspath(path) s = [] head = rp while len(head) >= len(base): if head == base: break head, tail = os.path.split(head) if tail: s.insert(0, tail) else: from errors import NotBranchError raise NotBranchError("path %r is not within branch %r" % (rp, base)) return os.sep.join(s) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch(object): """Branch holding a history of revisions. base Base directory of the branch. _lock_mode None, or 'r' or 'w' _lock_count If _lock_mode is true, a positive count of the number of times the lock has been taken. _lock Lock object from bzrlib.lock. """ base = None _lock_mode = None _lock_count = None def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): from errors import NotBranchError raise NotBranchError("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self._lockfile = self.controlfile('branch-lock', 'wb') self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def __del__(self): if self._lock_mode: from warnings import warn warn("branch %r was not explicitly unlocked" % self) self.unlock() def lock_write(self): if self._lock_mode: if self._lock_mode != 'w': from errors import LockError raise LockError("can't upgrade to a write lock from %r" % self._lock_mode) self._lock_count += 1 else: from bzrlib.lock import lock, LOCK_EX lock(self._lockfile, LOCK_EX) self._lock_mode = 'w' self._lock_count = 1 def lock_read(self): if self._lock_mode: assert self._lock_mode in ('r', 'w'), \ "invalid lock mode %r" % self._lock_mode self._lock_count += 1 else: from bzrlib.lock import lock, LOCK_SH lock(self._lockfile, LOCK_SH) self._lock_mode = 'r' self._lock_count = 1 def unlock(self): if not self._lock_mode: from errors import LockError raise LockError('branch %r is not locked' % (self)) if self._lock_count > 1: self._lock_count -= 1 else: assert self._lock_count == 1 from bzrlib.lock import unlock unlock(self._lockfile) self._lock_mode = self._lock_count = None def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" return _relpath(self.base, path) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: raise BzrError('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. self.lock_read() try: inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv finally: self.unlock() def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. files List of paths to add, relative to the base of the tree. ids If set, use these instead of automatically generated ids. Must be the same length as the list of files, but may contain None for ids that are to be autogenerated. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) self.lock_write() try: inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): raise BzrError("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: raise BzrError("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) finally: self.unlock() def print_file(self, file, revno): """Print `file` to stdout.""" self.lock_read() try: tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: raise BzrError("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) finally: self.unlock() def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] self.lock_write() try: tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: raise BzrError("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) finally: self.unlock() # FIXME: this doesn't need to be a branch method def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self.lock_read() try: return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] finally: self.unlock() def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def commit(self, *args, **kw): """Deprecated""" from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self.lock_write() try: tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): raise BzrError("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): raise BzrError("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: raise BzrError("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): raise BzrError("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': raise BzrError("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self.lock_write() try: ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): raise BzrError("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): raise BzrError("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': raise BzrError("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): raise BzrError("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): raise BzrError("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: raise BzrError("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): raise BzrError("destination %r already exists" % dest_path) if f_id in to_idpath: raise BzrError("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: if self.base: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/errors.py data 2002 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " ###################################################################### # exceptions class BzrError(StandardError): pass class BzrCheckError(BzrError): pass class BzrCommandError(BzrError): # Error from malformed user command pass class NotBranchError(BzrError): """Specified path is not in a branch""" pass class BadFileKindError(BzrError): """Specified file is of a kind that cannot be added. (For example a symlink or device file.)""" pass class ForbiddenFileError(BzrError): """Cannot operate on a file because it is a control file.""" pass class LockError(Exception): """All exceptions from the lock/unlock functions should be from this exception class. They will be translated as necessary. The original exception is available as e.original_error """ def __init__(self, e=None): self.original_error = e if e: Exception.__init__(self, e) else: Exception.__init__(self) def bailout(msg, explanation=[]): ex = BzrError(msg, explanation) import trace trace._tracefile.write('* raising %s\n' % ex) raise ex M 644 inline bzrlib/lock.py data 5648 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Locking wrappers. This only does local locking using OS locks for now. This module causes two methods, lock() and unlock() to be defined in any way that works on the current platform. It is not specified whether these locks are reentrant (i.e. can be taken repeatedly by a single process) or whether they exclude different threads in a single process. Eventually we may need to use some kind of lock representation that will work on a dumb filesystem without actual locking primitives. """ import sys, os import bzrlib from trace import mutter, note, warning from errors import LockError try: import fcntl LOCK_SH = fcntl.LOCK_SH LOCK_EX = fcntl.LOCK_EX LOCK_NB = fcntl.LOCK_NB def lock(f, flags): try: fcntl.flock(f, flags) except Exception, e: raise LockError(e) def unlock(f): try: fcntl.flock(f, fcntl.LOCK_UN) except Exception, e: raise LockError(e) except ImportError: try: import win32con, win32file, pywintypes LOCK_SH = 0 # the default LOCK_EX = win32con.LOCKFILE_EXCLUSIVE_LOCK LOCK_NB = win32con.LOCKFILE_FAIL_IMMEDIATELY def lock(f, flags): try: if type(f) == file: hfile = win32file._get_osfhandle(f.fileno()) else: hfile = win32file._get_osfhandle(f) overlapped = pywintypes.OVERLAPPED() win32file.LockFileEx(hfile, flags, 0, 0x7fff0000, overlapped) except Exception, e: raise LockError(e) def unlock(f): try: if type(f) == file: hfile = win32file._get_osfhandle(f.fileno()) else: hfile = win32file._get_osfhandle(f) overlapped = pywintypes.OVERLAPPED() win32file.UnlockFileEx(hfile, 0, 0x7fff0000, overlapped) except Exception, e: raise LockError(e) except ImportError: try: import msvcrt # Unfortunately, msvcrt.locking() doesn't distinguish between # read locks and write locks. Also, the way the combinations # work to get non-blocking is not the same, so we # have to write extra special functions here. LOCK_SH = 1 LOCK_EX = 2 LOCK_NB = 4 def lock(f, flags): try: # Unfortunately, msvcrt.LK_RLCK is equivalent to msvcrt.LK_LOCK # according to the comments, LK_RLCK is open the lock for writing. # Unfortunately, msvcrt.locking() also has the side effect that it # will only block for 10 seconds at most, and then it will throw an # exception, this isn't terrible, though. if type(f) == file: fpos = f.tell() fn = f.fileno() f.seek(0) else: fn = f fpos = os.lseek(fn, 0,0) os.lseek(fn, 0,0) if flags & LOCK_SH: if flags & LOCK_NB: lock_mode = msvcrt.LK_NBLCK else: lock_mode = msvcrt.LK_LOCK elif flags & LOCK_EX: if flags & LOCK_NB: lock_mode = msvcrt.LK_NBRLCK else: lock_mode = msvcrt.LK_RLCK else: raise ValueError('Invalid lock mode: %r' % flags) try: msvcrt.locking(fn, lock_mode, -1) finally: os.lseek(fn, fpos, 0) except Exception, e: raise LockError(e) def unlock(f): try: if type(f) == file: fpos = f.tell() fn = f.fileno() f.seek(0) else: fn = f fpos = os.lseek(fn, 0,0) os.lseek(fn, 0,0) try: msvcrt.locking(fn, msvcrt.LK_UNLCK, -1) finally: os.lseek(fn, fpos, 0) except Exception, e: raise LockError(e) except ImportError: from warnings import Warning warning("please write a locking method for platform %r" % sys.platform) # Creating no-op lock/unlock for now def lock(f, flags): pass def unlock(f): pass commit refs/heads/master mark :615 committer Martin Pool 1117606199 +1000 data 351 Major rework of locking code: - New ReadLock and WriteLock objects, with unlock methods, and that give a warning if they leak without being unlocked - The lock file is opened readonly for read locks, which should avoid problems when the user only has read permission for a branch. - Selective definitions of locking code is perhaps simpler now from :614 M 644 inline bzrlib/branch.py data 28695 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def _relpath(base, path): """Return path relative to base, or raise exception. The path may be either an absolute path or a path relative to the current working directory. Lifted out of Branch.relpath for ease of testing. os.path.commonprefix (python2.4) has a bad bug that it works just on string prefixes, assuming that '/u' is a prefix of '/u2'. This avoids that problem.""" rp = os.path.abspath(path) s = [] head = rp while len(head) >= len(base): if head == base: break head, tail = os.path.split(head) if tail: s.insert(0, tail) else: from errors import NotBranchError raise NotBranchError("path %r is not within branch %r" % (rp, base)) return os.sep.join(s) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch(object): """Branch holding a history of revisions. base Base directory of the branch. _lock_mode None, or 'r' or 'w' _lock_count If _lock_mode is true, a positive count of the number of times the lock has been taken. _lock Lock object from bzrlib.lock. """ base = None _lock_mode = None _lock_count = None _lock = None def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): from errors import NotBranchError raise NotBranchError("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def __del__(self): if self._lock_mode or self._lock: from warnings import warn warn("branch %r was not explicitly unlocked" % self) self._lock.unlock() def lock_write(self): if self._lock_mode: if self._lock_mode != 'w': from errors import LockError raise LockError("can't upgrade to a write lock from %r" % self._lock_mode) self._lock_count += 1 else: from bzrlib.lock import WriteLock self._lock = WriteLock(self.controlfilename('branch-lock')) self._lock_mode = 'w' self._lock_count = 1 def lock_read(self): if self._lock_mode: assert self._lock_mode in ('r', 'w'), \ "invalid lock mode %r" % self._lock_mode self._lock_count += 1 else: from bzrlib.lock import ReadLock self._lock = ReadLock(self.controlfilename('branch-lock')) self._lock_mode = 'r' self._lock_count = 1 def unlock(self): if not self._lock_mode: from errors import LockError raise LockError('branch %r is not locked' % (self)) if self._lock_count > 1: self._lock_count -= 1 else: self._lock.unlock() self._lock = None self._lock_mode = self._lock_count = None def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" return _relpath(self.base, path) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: raise BzrError('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. self.lock_read() try: inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv finally: self.unlock() def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. files List of paths to add, relative to the base of the tree. ids If set, use these instead of automatically generated ids. Must be the same length as the list of files, but may contain None for ids that are to be autogenerated. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) self.lock_write() try: inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): raise BzrError("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: raise BzrError("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) finally: self.unlock() def print_file(self, file, revno): """Print `file` to stdout.""" self.lock_read() try: tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: raise BzrError("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) finally: self.unlock() def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] self.lock_write() try: tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: raise BzrError("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) finally: self.unlock() # FIXME: this doesn't need to be a branch method def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self.lock_read() try: return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] finally: self.unlock() def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def commit(self, *args, **kw): """Deprecated""" from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self.lock_write() try: tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): raise BzrError("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): raise BzrError("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: raise BzrError("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): raise BzrError("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': raise BzrError("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self.lock_write() try: ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): raise BzrError("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): raise BzrError("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': raise BzrError("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): raise BzrError("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): raise BzrError("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: raise BzrError("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): raise BzrError("destination %r already exists" % dest_path) if f_id in to_idpath: raise BzrError("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[]): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ Branch.__init__(self, tempfile.mkdtemp(), init=True) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: if self.base: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/lock.py data 7464 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Locking wrappers. This only does local locking using OS locks for now. This module causes two methods, lock() and unlock() to be defined in any way that works on the current platform. It is not specified whether these locks are reentrant (i.e. can be taken repeatedly by a single process) or whether they exclude different threads in a single process. Eventually we may need to use some kind of lock representation that will work on a dumb filesystem without actual locking primitives. This defines two classes: ReadLock and WriteLock, which can be implemented in different ways on different platforms. Both have an unlock() method. """ import sys, os from trace import mutter, note, warning from errors import LockError class _base_Lock(object): def _open(self, filename, filemode): self.f = open(filename, filemode) return self.f def __del__(self): if self.f: from warnings import warn warn("lock on %r not released" % self.f) self.unlock() def unlock(self): raise NotImplementedError() ############################################################ # msvcrt locks try: import fcntl class _fcntl_FileLock(_base_Lock): f = None def unlock(self): fcntl.flock(self.f, fcntl.LOCK_UN) self.f.close() del self.f class _fcntl_WriteLock(_fcntl_FileLock): def __init__(self, filename): try: fcntl.flock(self._open(filename, 'wb'), fcntl.LOCK_EX) except Exception, e: raise LockError(e) class _fcntl_ReadLock(_fcntl_FileLock): def __init__(self, filename): try: fcntl.flock(self._open(filename, 'rb'), fcntl.LOCK_SH) except Exception, e: raise LockError(e) WriteLock = _fcntl_WriteLock ReadLock = _fcntl_ReadLock except ImportError: try: import win32con, win32file, pywintypes #LOCK_SH = 0 # the default #LOCK_EX = win32con.LOCKFILE_EXCLUSIVE_LOCK #LOCK_NB = win32con.LOCKFILE_FAIL_IMMEDIATELY class _w32c_FileLock(_base_Lock): def _lock(self, filename, openmode, lockmode): try: self._open(filename, openmode) self.hfile = win32file._get_osfhandle(self.f.fileno()) overlapped = pywintypes.OVERLAPPED() win32file.LockFileEx(self.hfile, lockmode, 0, 0x7fff0000, overlapped) except Exception, e: raise LockError(e) def unlock(self): try: overlapped = pywintypes.OVERLAPPED() win32file.UnlockFileEx(self.hfile, 0, 0x7fff0000, overlapped) self.f.close() self.f = None except Exception, e: raise LockError(e) class _w32c_ReadLock(_w32c_FileLock): def __init__(self, filename): _w32c_FileLock._lock(self, filename, 'rb', 0) class _w32c_WriteLock(_w32c_FileLock): def __init__(self, filename): _w32c_FileLock._lock(self, filename, 'wb', win32con.LOCKFILE_EXCLUSIVE_LOCK) WriteLock = _w32c_WriteLock ReadLock = _w32c_ReadLock except ImportError: try: import msvcrt # Unfortunately, msvcrt.locking() doesn't distinguish between # read locks and write locks. Also, the way the combinations # work to get non-blocking is not the same, so we # have to write extra special functions here. class _msvc_FileLock(_base_Lock): LOCK_SH = 1 LOCK_EX = 2 LOCK_NB = 4 def unlock(self): _msvc_unlock(self.f) class _msvc_ReadLock(_msvc_FileLock): def __init__(self, filename): _msvc_lock(self._open(filename, 'rb'), self.LOCK_SH) class _msvc_WriteLock(_msvc_FileLock): def __init__(self, filename): _msvc_lock(self._open(filename, 'wb'), self.LOCK_EX) def _msvc_lock(f, flags): try: # Unfortunately, msvcrt.LK_RLCK is equivalent to msvcrt.LK_LOCK # according to the comments, LK_RLCK is open the lock for writing. # Unfortunately, msvcrt.locking() also has the side effect that it # will only block for 10 seconds at most, and then it will throw an # exception, this isn't terrible, though. if type(f) == file: fpos = f.tell() fn = f.fileno() f.seek(0) else: fn = f fpos = os.lseek(fn, 0,0) os.lseek(fn, 0,0) if flags & self.LOCK_SH: if flags & self.LOCK_NB: lock_mode = msvcrt.LK_NBLCK else: lock_mode = msvcrt.LK_LOCK elif flags & self.LOCK_EX: if flags & self.LOCK_NB: lock_mode = msvcrt.LK_NBRLCK else: lock_mode = msvcrt.LK_RLCK else: raise ValueError('Invalid lock mode: %r' % flags) try: msvcrt.locking(fn, lock_mode, -1) finally: os.lseek(fn, fpos, 0) except Exception, e: raise LockError(e) def _msvc_unlock(f): try: if type(f) == file: fpos = f.tell() fn = f.fileno() f.seek(0) else: fn = f fpos = os.lseek(fn, 0,0) os.lseek(fn, 0,0) try: msvcrt.locking(fn, msvcrt.LK_UNLCK, -1) finally: os.lseek(fn, fpos, 0) except Exception, e: raise LockError(e) WriteLock = _msvc_WriteLock ReadLock = _msvc_ReadLock except ImportError: raise NotImplementedError("please write a locking method " "for platform %r" % sys.platform) commit refs/heads/master mark :616 committer Martin Pool 1117611803 +1000 data 37 - ignore more vi swap files .*sw[nop] from :615 M 644 inline bzrlib/__init__.py data 2106 # (C) 2005 Canonical Development Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr library""" from inventory import Inventory, InventoryEntry from branch import Branch, ScratchBranch, find_branch from osutils import format_date from tree import Tree from diff import compare_trees from trace import mutter, warning, open_tracefile from log import show_log import add BZRDIR = ".bzr" DEFAULT_IGNORE = ['.bzr.log', '*~', '#*#', '*$', '.#*', '.*.sw[onp]', '.*.tmp', '*.tmp', '*.bak', '*.BAK', '*.orig', '*.o', '*.obj', '*.a', '*.py[oc]', '*.so', '*.exe', '*.elc', '{arch}', 'CVS', 'CVS.adm', '.svn', '_darcs', 'SCCS', 'RCS', '*,v', 'BitKeeper', 'TAGS', '.make.state', '.sconsign', '.tmp*', '.del-*'] IGNORE_FILENAME = ".bzrignore" import locale user_encoding = locale.getpreferredencoding() del locale __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __version__ = '0.0.5pre' def get_bzr_revision(): """If bzr is run from a branch, return (revno,revid) or None""" from errors import BzrError try: branch = Branch(__path__[0]) rh = branch.revision_history() if rh: return len(rh), rh[-1] else: return None except BzrError: return None commit refs/heads/master mark :617 committer Martin Pool 1117611879 +1000 data 13 - ignore .git from :616 M 644 inline bzrlib/__init__.py data 2132 # (C) 2005 Canonical Development Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr library""" from inventory import Inventory, InventoryEntry from branch import Branch, ScratchBranch, find_branch from osutils import format_date from tree import Tree from diff import compare_trees from trace import mutter, warning, open_tracefile from log import show_log import add BZRDIR = ".bzr" DEFAULT_IGNORE = ['.bzr.log', '*~', '#*#', '*$', '.#*', '.*.sw[nop]', '.*.tmp', '*.tmp', '*.bak', '*.BAK', '*.orig', '*.o', '*.obj', '*.a', '*.py[oc]', '*.so', '*.exe', '*.elc', '{arch}', 'CVS', 'CVS.adm', '.svn', '_darcs', 'SCCS', 'RCS', '*,v', 'BitKeeper', '.git', 'TAGS', '.make.state', '.sconsign', '.tmp*', '.del-*'] IGNORE_FILENAME = ".bzrignore" import locale user_encoding = locale.getpreferredencoding() del locale __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __version__ = '0.0.5pre' def get_bzr_revision(): """If bzr is run from a branch, return (revno,revid) or None""" from errors import BzrError try: branch = Branch(__path__[0]) rh = branch.revision_history() if rh: return len(rh), rh[-1] else: return None except BzrError: return None commit refs/heads/master mark :618 committer Martin Pool 1117611928 +1000 data 27 - ignore .git files as well from :617 M 644 inline NEWS data 8315 bzr-0.0.5 NOT RELEASED YET CHANGES: * ``bzr`` with no command now shows help rather than giving an error. Suggested by Michael Ellerman. * ``bzr status`` output format changed, because svn-style output doesn't really match the model of bzr. Now files are grouped by status and can be shown with their IDs. ``bzr status --all`` shows all versioned files and unknown files but not ignored files. * ``bzr log`` runs from most-recent to least-recent, the reverse of the previous order. The previous behaviour can be obtained with the ``--forward`` option. * ``bzr inventory`` by default shows only filenames, and also ids if ``--show-ids`` is given, in which case the id is the second field. ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New ``bzr ignore PATTERN`` command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.sw[nop] .git .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. File may be in a branch other than the working directory. * ``bzr log`` and ``bzr root`` can be given an http URL instead of a filename. * Commands can now be defined by external programs or scripts in a directory on $BZRPATH. * New "stat cache" avoids reading the contents of files if they haven't changed since the previous time. * If the Python interpreter is too old, try to find a better one or give an error. Based on a patch from Fredrik Lundh. * New optional parameter ``bzr info [BRANCH]``. * New form ``bzr commit SELECTED`` to commit only selected files. * New form ``bzr log -r FROM:TO`` shows changes in selected range; contributed by John A Meinel. * New option ``bzr diff --diff-options 'OPTS'`` allows passing options through to an external GNU diff. * New option ``bzr add --no-recurse`` to add a directory but not their contents. * ``bzr --version`` now shows more information if bzr is being run from a branch. BUG FIXES: * Fixed diff format so that added and removed files will be handled properly by patch. Fix from Lalo Martins. * Various fixes for files whose names contain spaces or other metacharacters. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. * testbzr requires python2.4, but can be used to test bzr running under a different version. * Tests added for many other changes in this release. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. * New developer commands ``added``, ``modified``. PORTABILITY: * Cope on Windows on python2.3 by using the weaker random seed. 2.4 is now only recommended. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. commit refs/heads/master mark :619 committer Martin Pool 1117679874 +1000 data 3 doc from :618 M 644 inline bzrlib/diff.py data 13734 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from trace import mutter from errors import BzrError def internal_diff(old_label, oldlines, new_label, newlines, to_file): import difflib # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, fromfile=old_label, tofile=new_label) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') to_file.writelines(ud) if nonl: print >>to_file, "\\ No newline at end of file" print >>to_file def external_diff(old_label, oldlines, new_label, newlines, to_file, diff_opts): """Display a diff by calling out to the external diff program.""" import sys if to_file != sys.stdout: raise NotImplementedError("sorry, can't send external diff other than to stdout yet", to_file) # make sure our own output is properly ordered before the diff to_file.flush() from tempfile import NamedTemporaryFile import os oldtmpf = NamedTemporaryFile() newtmpf = NamedTemporaryFile() try: # TODO: perhaps a special case for comparing to or from the empty # sequence; can just use /dev/null on Unix # TODO: if either of the files being compared already exists as a # regular named file (e.g. in the working directory) then we can # compare directly to that, rather than copying it. oldtmpf.writelines(oldlines) newtmpf.writelines(newlines) oldtmpf.flush() newtmpf.flush() if not diff_opts: diff_opts = [] diffcmd = ['diff', '--label', old_label, oldtmpf.name, '--label', new_label, newtmpf.name] # diff only allows one style to be specified; they don't override. # note that some of these take optargs, and the optargs can be # directly appended to the options. # this is only an approximate parser; it doesn't properly understand # the grammar. for s in ['-c', '-u', '-C', '-U', '-e', '--ed', '-q', '--brief', '--normal', '-n', '--rcs', '-y', '--side-by-side', '-D', '--ifdef']: for j in diff_opts: if j.startswith(s): break else: continue break else: diffcmd.append('-u') if diff_opts: diffcmd.extend(diff_opts) rc = os.spawnvp(os.P_WAIT, 'diff', diffcmd) if rc != 0 and rc != 1: # returns 1 if files differ; that's OK if rc < 0: msg = 'signal %d' % (-rc) else: msg = 'exit code %d' % rc raise BzrError('external diff failed with %s; command: %r' % (rc, diffcmd)) finally: oldtmpf.close() # and delete newtmpf.close() def show_diff(b, revision, specific_files, external_diff_options=None): """Shortcut for showing the diff to the working tree. b Branch. revision None for each, or otherwise the old revision to compare against. The more general form is show_diff_trees(), where the caller supplies any two trees. """ import sys if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() show_diff_trees(old_tree, new_tree, sys.stdout, specific_files, external_diff_options) def show_diff_trees(old_tree, new_tree, to_file, specific_files=None, external_diff_options=None): """Show in text form the changes from one tree to another. to_files If set, include only changes to these files. external_diff_options If set, use an external GNU diff and pass these options. """ # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. if external_diff_options: assert isinstance(external_diff_options, basestring) opts = external_diff_options.split() def diff_file(olab, olines, nlab, nlines, to_file): external_diff(olab, olines, nlab, nlines, to_file, opts) else: diff_file = internal_diff delta = compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=specific_files) for path, file_id, kind in delta.removed: print '*** removed %s %r' % (kind, path) if kind == 'file': diff_file(old_label + path, old_tree.get_file(file_id).readlines(), DEVNULL, [], to_file) for path, file_id, kind in delta.added: print '*** added %s %r' % (kind, path) if kind == 'file': diff_file(DEVNULL, [], new_label + path, new_tree.get_file(file_id).readlines(), to_file) for old_path, new_path, file_id, kind, text_modified in delta.renamed: print '*** renamed %s %r => %r' % (kind, old_path, new_path) if text_modified: diff_file(old_label + old_path, old_tree.get_file(file_id).readlines(), new_label + new_path, new_tree.get_file(file_id).readlines(), to_file) for path, file_id, kind in delta.modified: print '*** modified %s %r' % (kind, path) if kind == 'file': diff_file(old_label + path, old_tree.get_file(file_id).readlines(), new_label + path, new_tree.get_file(file_id).readlines(), to_file) class TreeDelta(object): """Describes changes from one tree to another. Contains four lists: added (path, id, kind) removed (path, id, kind) renamed (oldpath, newpath, id, kind, text_modified) modified (path, id, kind) unchanged (path, id, kind) Each id is listed only once. Files that are both modified and renamed are listed only in renamed, with the text_modified flag true. The lists are normally sorted when the delta is created. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] self.unchanged = [] def touches_file_id(self, file_id): """Return True if file_id is modified by this delta.""" for l in self.added, self.removed, self.modified: for v in l: if v[1] == file_id: return True for v in self.renamed: if v[2] == file_id: return True return False def show(self, to_file, show_ids=False, show_unchanged=False): def show_list(files): for path, fid, kind in files: if kind == 'directory': path += '/' elif kind == 'symlink': path += '@' if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if self.removed: print >>to_file, 'removed:' show_list(self.removed) if self.added: print >>to_file, 'added:' show_list(self.added) if self.renamed: print >>to_file, 'renamed:' for oldpath, newpath, fid, kind, text_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if self.modified: print >>to_file, 'modified:' show_list(self.modified) if show_unchanged and self.unchanged: print >>to_file, 'unchanged:' show_list(self.unchanged) def compare_trees(old_tree, new_tree, want_unchanged, specific_files=None): """Describe changes from one tree to another. Returns a TreeDelta with details of added, modified, renamed, and deleted entries. The root entry is specifically exempt. This only considers versioned files. want_unchanged If true, also list files unchanged from one version to the next. specific_files If true, only check for changes to specified names or files within them. """ from osutils import is_inside_any old_inv = old_tree.inventory new_inv = new_tree.inventory delta = TreeDelta() mutter('start compare_trees') # TODO: match for specific files can be rather smarter by finding # the IDs of those files up front and then considering only that. for file_id in old_tree: if file_id in new_tree: kind = old_inv.get_file_kind(file_id) assert kind == new_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ 'invalid file kind %r' % kind if kind == 'root_directory': continue old_path = old_inv.id2path(file_id) new_path = new_inv.id2path(file_id) if specific_files: if (not is_inside_any(specific_files, old_path) and not is_inside_any(specific_files, new_path)): continue if kind == 'file': old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False # TODO: Can possibly avoid calculating path strings if the # two files are unchanged and their names and parents are # the same and the parents are unchanged all the way up. # May not be worthwhile. if old_path != new_path: delta.renamed.append((old_path, new_path, file_id, kind, text_modified)) elif text_modified: delta.modified.append((new_path, file_id, kind)) elif want_unchanged: delta.unchanged.append((new_path, file_id, kind)) else: kind = old_inv.get_file_kind(file_id) old_path = old_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, old_path): continue delta.removed.append((old_path, file_id, kind)) mutter('start looking for new files') for file_id in new_inv: if file_id in old_inv: continue new_path = new_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, new_path): continue kind = new_inv.get_file_kind(file_id) delta.added.append((new_path, file_id, kind)) delta.removed.sort() delta.added.sort() delta.renamed.sort() delta.modified.sort() delta.unchanged.sort() return delta commit refs/heads/master mark :620 committer Martin Pool 1117680093 +1000 data 63 - Improved bash completion script contributed by Sven Wilhelm from :619 R contrib/bash/bzr contrib/bash/bzr.simple M 644 inline contrib/bash/bzr data 2379 # Programmable completion for the Bazaar-NG bzr command under bash. Source # this file (or on some systems add it to ~/.bash_completion and start a new # shell) and bash's completion mechanism will know all about bzr's options! # Known to work with bash 2.05a with programmable completion and extended # pattern matching enabled (use 'shopt -s extglob progcomp' to enable # these if they are not already enabled). # Based originally on the svn bash completition script. # Customized by Sven Wilhelm/Icecrash.com _bzr () { local cur cmds cmdOpts opt helpCmds optBase i COMPREPLY=() cur=${COMP_WORDS[COMP_CWORD]} cmds='status diff commit ci checkin move remove log info check ignored' if [[ $COMP_CWORD -eq 1 ]] ; then COMPREPLY=( $( compgen -W "$cmds" -- $cur ) ) return 0 fi # if not typing an option, or if the previous option required a # parameter, then fallback on ordinary filename expansion helpCmds='help|--help|h|\?' if [[ ${COMP_WORDS[1]} != @($helpCmds) ]] && \ [[ "$cur" != -* ]] ; then return 0 fi cmdOpts= case ${COMP_WORDS[1]} in status) cmdOpts="--all --show-ids" ;; diff) cmdOpts="-r --revision --diff-options" ;; commit|ci|checkin) cmdOpts="-r --message -F --file -v --verbose" ;; move) cmdOpts="" ;; remove) cmdOpts="-v --verbose" ;; log) cmdOpts="--forward --timezone -v --verbose --show-ids -r --revision" ;; info) cmdOpts="" ;; ignored) cmdOpts="" ;; check) cmdOpts="" ;; help|h|\?) cmdOpts="$cmds $qOpts" ;; *) ;; esac cmdOpts="$cmdOpts --help -h" # take out options already given for (( i=2; i<=$COMP_CWORD-1; ++i )) ; do opt=${COMP_WORDS[$i]} case $opt in --*) optBase=${opt/=*/} ;; -*) optBase=${opt:0:2} ;; esac cmdOpts=" $cmdOpts " cmdOpts=${cmdOpts/ ${optBase} / } # take out alternatives case $optBase in -v) cmdOpts=${cmdOpts/ --verbose / } ;; --verbose) cmdOpts=${cmdOpts/ -v / } ;; -h) cmdOpts=${cmdOpts/ --help / } ;; --help) cmdOpts=${cmdOpts/ -h / } ;; -r) cmdOpts=${cmdOpts/ --revision / } ;; --revision) cmdOpts=${cmdOpts/ -r / } ;; esac # skip next option if this one requires a parameter if [[ $opt == @($optsParam) ]] ; then ((++i)) fi done COMPREPLY=( $( compgen -W "$cmdOpts" -- $cur ) ) return 0 } complete -F _bzr -o default bzr commit refs/heads/master mark :621 committer Martin Pool 1118031344 +1000 data 37 - script to create rollups, from John from :620 M 644 inline contrib/create_bzr_rollup.py data 5516 #!/usr/bin/env python """\ This script runs after rsyncing bzr. It checks the bzr version, and sees if there is a tarball and zipfile that exist with that version. If not, it creates them. """ import os, sys, tempfile def sync(remote, local, verbose=False): """Do the actual synchronization """ if verbose: status = os.system('rsync -av --delete "%s" "%s"' % (remote, local)) else: status = os.system('rsync -a --delete "%s" "%s"' % (remote, local)) return status==0 def create_tar_gz(local_dir, output_dir=None, verbose=False): import tarfile, bzrlib out_name = os.path.basename(local_dir) + '-' + str(bzrlib.Branch(local_dir).revno()) final_path = os.path.join(output_dir, out_name + '.tar.gz') if os.path.exists(final_path): if verbose: print 'Output file already exists: %r' % final_path return fn, tmp_path=tempfile.mkstemp(suffix='.tar', prefix=out_name, dir=output_dir) os.close(fn) try: if verbose: print 'Creating %r (%r)' % (final_path, tmp_path) tar = tarfile.TarFile(name=tmp_path, mode='w') tar.add(local_dir, arcname=out_name, recursive=True) tar.close() if verbose: print 'Compressing...' if os.system('gzip "%s"' % tmp_path) != 0: raise ValueError('Failed to compress') tmp_path += '.gz' os.rename(tmp_path, final_path) except: os.remove(tmp_path) raise def create_tar_bz2(local_dir, output_dir=None, verbose=False): import tarfile, bzrlib out_name = os.path.basename(local_dir) + '-' + str(bzrlib.Branch(local_dir).revno()) final_path = os.path.join(output_dir, out_name + '.tar.bz2') if os.path.exists(final_path): if verbose: print 'Output file already exists: %r' % final_path return fn, tmp_path=tempfile.mkstemp(suffix='.tar', prefix=out_name, dir=output_dir) os.close(fn) try: if verbose: print 'Creating %r (%r)' % (final_path, tmp_path) tar = tarfile.TarFile(name=tmp_path, mode='w') tar.add(local_dir, arcname=out_name, recursive=True) tar.close() if verbose: print 'Compressing...' if os.system('bzip2 "%s"' % tmp_path) != 0: raise ValueError('Failed to compress') tmp_path += '.bz2' os.rename(tmp_path, final_path) except: os.remove(tmp_path) raise def create_zip(local_dir, output_dir=None, verbose=False): import zipfile, bzrlib out_name = os.path.basename(local_dir) + '-' + str(bzrlib.Branch(local_dir).revno()) final_path = os.path.join(output_dir, out_name + '.zip') if os.path.exists(final_path): if verbose: print 'Output file already exists: %r' % final_path return fn, tmp_path=tempfile.mkstemp(suffix='.zip', prefix=out_name, dir=output_dir) os.close(fn) try: if verbose: print 'Creating %r (%r)' % (final_path, tmp_path) zip = zipfile.ZipFile(file=tmp_path, mode='w') try: for root, dirs, files in os.walk(local_dir): for f in files: path = os.path.join(root, f) arcname = os.path.join(out_name, path[len(local_dir)+1:]) zip.write(path, arcname=arcname) finally: zip.close() os.rename(tmp_path, final_path) except: os.remove(tmp_path) raise def get_local_dir(remote, local): """This returns the full path to the local directory where the files are kept. rsync has the trick that if the source directory ends in a '/' then the file will be copied *into* the target. If it does not end in a slash, then the directory will be added into the target. """ if remote[-1:] == '/': return local # rsync paths are typically user@host:path/to/something # the reason for the split(':') is in case path doesn't contain a slash extra = remote.split(':')[-1].split('/')[-1] return os.path.join(local, extra) def get_output_dir(output, local): if output: return output return os.path.dirname(os.path.realpath(local)) def main(args): import optparse p = optparse.OptionParser(usage='%prog [options] [remote] [local]' '\n rsync the remote repository to the local directory' '\n if remote is not given, it defaults to "bazaar-ng.org::bazaar-ng/bzr/bzr.dev"' '\n if local is not given it defaults to "."') p.add_option('--verbose', action='store_true' , help="Describe the process") p.add_option('--no-tar-gz', action='store_false', dest='create_tar_gz', default=True , help="Don't create a gzip compressed tarfile.") p.add_option('--no-tar-bz2', action='store_false', dest='create_tar_bz2', default=True , help="Don't create a bzip2 compressed tarfile.") p.add_option('--no-zip', action='store_false', dest='create_zip', default=True , help="Don't create a zipfile.") p.add_option('--output-dir', default=None , help="Set the output location, default is just above the final local directory.") (opts, args) = p.parse_args(args) if len(args) < 1: remote = 'bazaar-ng.org::bazaar-ng/bzr/bzr.dev' else: remote = args[0] if len(args) < 2: local = '.' else: local = args[1] if len(args) > 2: print 'Invalid number of arguments, see --help for details.' if not sync(remote, local, verbose=opts.verbose): if opts.verbose: print '** rsync failed' return 1 # Now we have the new update local_dir = get_local_dir(remote, local) output_dir = get_output_dir(opts.output_dir, local_dir) if opts.create_tar_gz: create_tar_gz(local_dir, output_dir=output_dir, verbose=opts.verbose) if opts.create_tar_bz2: create_tar_bz2(local_dir, output_dir=output_dir, verbose=opts.verbose) if opts.create_zip: create_zip(local_dir, output_dir=output_dir, verbose=opts.verbose) return 0 if __name__ == '__main__': sys.exit(main(sys.argv[1:])) commit refs/heads/master mark :622 committer Martin Pool 1118031473 +1000 data 373 Updated merge patch from Aaron This patch contains all the changes to merge that I'd like to get into 0.5, namely * common ancestor BASE selection * merge reports conflicts when they are encountered * merge refuses to operate in working trees with changes * introduces revert command to revert the working tree to the last-committed state * Adds some reasonable help text from :621 M 644 inline bzrlib/branch.py data 30821 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def _relpath(base, path): """Return path relative to base, or raise exception. The path may be either an absolute path or a path relative to the current working directory. Lifted out of Branch.relpath for ease of testing. os.path.commonprefix (python2.4) has a bad bug that it works just on string prefixes, assuming that '/u' is a prefix of '/u2'. This avoids that problem.""" rp = os.path.abspath(path) s = [] head = rp while len(head) >= len(base): if head == base: break head, tail = os.path.split(head) if tail: s.insert(0, tail) else: from errors import NotBranchError raise NotBranchError("path %r is not within branch %r" % (rp, base)) return os.sep.join(s) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head ###################################################################### # branch objects class Branch(object): """Branch holding a history of revisions. base Base directory of the branch. _lock_mode None, or 'r' or 'w' _lock_count If _lock_mode is true, a positive count of the number of times the lock has been taken. _lock Lock object from bzrlib.lock. """ base = None _lock_mode = None _lock_count = None _lock = None def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): from errors import NotBranchError raise NotBranchError("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def __del__(self): if self._lock_mode or self._lock: from warnings import warn warn("branch %r was not explicitly unlocked" % self) self._lock.unlock() def lock_write(self): if self._lock_mode: if self._lock_mode != 'w': from errors import LockError raise LockError("can't upgrade to a write lock from %r" % self._lock_mode) self._lock_count += 1 else: from bzrlib.lock import WriteLock self._lock = WriteLock(self.controlfilename('branch-lock')) self._lock_mode = 'w' self._lock_count = 1 def lock_read(self): if self._lock_mode: assert self._lock_mode in ('r', 'w'), \ "invalid lock mode %r" % self._lock_mode self._lock_count += 1 else: from bzrlib.lock import ReadLock self._lock = ReadLock(self.controlfilename('branch-lock')) self._lock_mode = 'r' self._lock_count = 1 def unlock(self): if not self._lock_mode: from errors import LockError raise LockError('branch %r is not locked' % (self)) if self._lock_count > 1: self._lock_count -= 1 else: self._lock.unlock() self._lock = None self._lock_mode = self._lock_count = None def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" return _relpath(self.base, path) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: raise BzrError('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. self.lock_read() try: inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv finally: self.unlock() def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. files List of paths to add, relative to the base of the tree. ids If set, use these instead of automatically generated ids. Must be the same length as the list of files, but may contain None for ids that are to be autogenerated. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) self.lock_write() try: inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): raise BzrError("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: raise BzrError("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) finally: self.unlock() def print_file(self, file, revno): """Print `file` to stdout.""" self.lock_read() try: tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: raise BzrError("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) finally: self.unlock() def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] self.lock_write() try: tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: raise BzrError("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) finally: self.unlock() # FIXME: this doesn't need to be a branch method def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self.lock_read() try: return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] finally: self.unlock() def common_ancestor(self, other, self_revno=None, other_revno=None): """ >>> import commit >>> sb = ScratchBranch(files=['foo', 'foo~']) >>> sb.common_ancestor(sb) == (None, None) True >>> commit.commit(sb, "Committing first revision", verbose=False) >>> sb.common_ancestor(sb)[0] 1 >>> clone = sb.clone() >>> commit.commit(sb, "Committing second revision", verbose=False) >>> sb.common_ancestor(sb)[0] 2 >>> sb.common_ancestor(clone)[0] 1 >>> commit.commit(clone, "Committing divergent second revision", ... verbose=False) >>> sb.common_ancestor(clone)[0] 1 >>> sb.common_ancestor(clone) == clone.common_ancestor(sb) True >>> sb.common_ancestor(sb) != clone.common_ancestor(clone) True >>> clone2 = sb.clone() >>> sb.common_ancestor(clone2)[0] 2 >>> sb.common_ancestor(clone2, self_revno=1)[0] 1 >>> sb.common_ancestor(clone2, other_revno=1)[0] 1 """ my_history = self.revision_history() other_history = other.revision_history() if self_revno is None: self_revno = len(my_history) if other_revno is None: other_revno = len(other_history) indices = range(min((self_revno, other_revno))) indices.reverse() for r in indices: if my_history[r] == other_history[r]: return r+1, my_history[r] return None, None def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def commit(self, *args, **kw): """Deprecated""" from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self.lock_write() try: tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): raise BzrError("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): raise BzrError("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: raise BzrError("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): raise BzrError("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': raise BzrError("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self.lock_write() try: ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): raise BzrError("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): raise BzrError("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': raise BzrError("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): raise BzrError("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): raise BzrError("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: raise BzrError("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): raise BzrError("destination %r already exists" % dest_path) if f_id in to_idpath: raise BzrError("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[], base=None): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ init = False if base is None: base = tempfile.mkdtemp() init = True Branch.__init__(self, base, init=init) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def clone(self): """ >>> orig = ScratchBranch(files=["file1", "file2"]) >>> clone = orig.clone() >>> os.path.samefile(orig.base, clone.base) False >>> os.path.isfile(os.path.join(clone.base, "file1")) True """ base = tempfile.mkdtemp() os.rmdir(base) shutil.copytree(self.base, base, symlinks=True) return ScratchBranch(base=base) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: if self.base: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/changeset.py data 53482 # Copyright (C) 2004 Aaron Bentley # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os.path import errno import patch import stat """ Represent and apply a changeset """ __docformat__ = "restructuredtext" NULL_ID = "!NULL" def invert_dict(dict): newdict = {} for (key,value) in dict.iteritems(): newdict[value] = key return newdict class PatchApply(object): """Patch application as a kind of content change""" def __init__(self, contents): """Constructor. :param contents: The text of the patch to apply :type contents: str""" self.contents = contents def __eq__(self, other): if not isinstance(other, PatchApply): return False elif self.contents != other.contents: return False else: return True def __ne__(self, other): return not (self == other) def apply(self, filename, conflict_handler, reverse=False): """Applies the patch to the specified file. :param filename: the file to apply the patch to :type filename: str :param reverse: If true, apply the patch in reverse :type reverse: bool """ input_name = filename+".orig" try: os.rename(filename, input_name) except OSError, e: if e.errno != errno.ENOENT: raise if conflict_handler.patch_target_missing(filename, self.contents)\ == "skip": return os.rename(filename, input_name) status = patch.patch(self.contents, input_name, filename, reverse) os.chmod(filename, os.stat(input_name).st_mode) if status == 0: os.unlink(input_name) elif status == 1: conflict_handler.failed_hunks(filename) class ChangeUnixPermissions(object): """This is two-way change, suitable for file modification, creation, deletion""" def __init__(self, old_mode, new_mode): self.old_mode = old_mode self.new_mode = new_mode def apply(self, filename, conflict_handler, reverse=False): if not reverse: from_mode = self.old_mode to_mode = self.new_mode else: from_mode = self.new_mode to_mode = self.old_mode try: current_mode = os.stat(filename).st_mode &0777 except OSError, e: if e.errno == errno.ENOENT: if conflict_handler.missing_for_chmod(filename) == "skip": return else: current_mode = from_mode if from_mode is not None and current_mode != from_mode: if conflict_handler.wrong_old_perms(filename, from_mode, current_mode) != "continue": return if to_mode is not None: try: os.chmod(filename, to_mode) except IOError, e: if e.errno == errno.ENOENT: conflict_handler.missing_for_chmod(filename) def __eq__(self, other): if not isinstance(other, ChangeUnixPermissions): return False elif self.old_mode != other.old_mode: return False elif self.new_mode != other.new_mode: return False else: return True def __ne__(self, other): return not (self == other) def dir_create(filename, conflict_handler, reverse): """Creates the directory, or deletes it if reverse is true. Intended to be used with ReplaceContents. :param filename: The name of the directory to create :type filename: str :param reverse: If true, delete the directory, instead :type reverse: bool """ if not reverse: try: os.mkdir(filename) except OSError, e: if e.errno != errno.EEXIST: raise if conflict_handler.dir_exists(filename) == "continue": os.mkdir(filename) except IOError, e: if e.errno == errno.ENOENT: if conflict_handler.missing_parent(filename)=="continue": file(filename, "wb").write(self.contents) else: try: os.rmdir(filename) except OSError, e: if e.errno != 39: raise if conflict_handler.rmdir_non_empty(filename) == "skip": return os.rmdir(filename) class SymlinkCreate(object): """Creates or deletes a symlink (for use with ReplaceContents)""" def __init__(self, contents): """Constructor. :param contents: The filename of the target the symlink should point to :type contents: str """ self.target = contents def __call__(self, filename, conflict_handler, reverse): """Creates or destroys the symlink. :param filename: The name of the symlink to create :type filename: str """ if reverse: assert(os.readlink(filename) == self.target) os.unlink(filename) else: try: os.symlink(self.target, filename) except OSError, e: if e.errno != errno.EEXIST: raise if conflict_handler.link_name_exists(filename) == "continue": os.symlink(self.target, filename) def __eq__(self, other): if not isinstance(other, SymlinkCreate): return False elif self.target != other.target: return False else: return True def __ne__(self, other): return not (self == other) class FileCreate(object): """Create or delete a file (for use with ReplaceContents)""" def __init__(self, contents): """Constructor :param contents: The contents of the file to write :type contents: str """ self.contents = contents def __repr__(self): return "FileCreate(%i b)" % len(self.contents) def __eq__(self, other): if not isinstance(other, FileCreate): return False elif self.contents != other.contents: return False else: return True def __ne__(self, other): return not (self == other) def __call__(self, filename, conflict_handler, reverse): """Create or delete a file :param filename: The name of the file to create :type filename: str :param reverse: Delete the file instead of creating it :type reverse: bool """ if not reverse: try: file(filename, "wb").write(self.contents) except IOError, e: if e.errno == errno.ENOENT: if conflict_handler.missing_parent(filename)=="continue": file(filename, "wb").write(self.contents) else: raise else: try: if (file(filename, "rb").read() != self.contents): direction = conflict_handler.wrong_old_contents(filename, self.contents) if direction != "continue": return os.unlink(filename) except IOError, e: if e.errno != errno.ENOENT: raise if conflict_handler.missing_for_rm(filename, undo) == "skip": return def reversed(sequence): max = len(sequence) - 1 for i in range(len(sequence)): yield sequence[max - i] class ReplaceContents(object): """A contents-replacement framework. It allows a file/directory/symlink to be created, deleted, or replaced with another file/directory/symlink. Arguments must be callable with (filename, reverse). """ def __init__(self, old_contents, new_contents): """Constructor. :param old_contents: The change to reverse apply (e.g. a deletion), \ when going forwards. :type old_contents: `dir_create`, `SymlinkCreate`, `FileCreate`, \ NoneType, etc. :param new_contents: The second change to apply (e.g. a creation), \ when going forwards. :type new_contents: `dir_create`, `SymlinkCreate`, `FileCreate`, \ NoneType, etc. """ self.old_contents=old_contents self.new_contents=new_contents def __repr__(self): return "ReplaceContents(%r -> %r)" % (self.old_contents, self.new_contents) def __eq__(self, other): if not isinstance(other, ReplaceContents): return False elif self.old_contents != other.old_contents: return False elif self.new_contents != other.new_contents: return False else: return True def __ne__(self, other): return not (self == other) def apply(self, filename, conflict_handler, reverse=False): """Applies the FileReplacement to the specified filename :param filename: The name of the file to apply changes to :type filename: str :param reverse: If true, apply the change in reverse :type reverse: bool """ if not reverse: undo = self.old_contents perform = self.new_contents else: undo = self.new_contents perform = self.old_contents mode = None if undo is not None: try: mode = os.lstat(filename).st_mode if stat.S_ISLNK(mode): mode = None except OSError, e: if e.errno != errno.ENOENT: raise if conflict_handler.missing_for_rm(filename, undo) == "skip": return undo(filename, conflict_handler, reverse=True) if perform is not None: perform(filename, conflict_handler, reverse=False) if mode is not None: os.chmod(filename, mode) class ApplySequence(object): def __init__(self, changes=None): self.changes = [] if changes is not None: self.changes.extend(changes) def __eq__(self, other): if not isinstance(other, ApplySequence): return False elif len(other.changes) != len(self.changes): return False else: for i in range(len(self.changes)): if self.changes[i] != other.changes[i]: return False return True def __ne__(self, other): return not (self == other) def apply(self, filename, conflict_handler, reverse=False): if not reverse: iter = self.changes else: iter = reversed(self.changes) for change in iter: change.apply(filename, conflict_handler, reverse) class Diff3Merge(object): def __init__(self, base_file, other_file): self.base_file = base_file self.other_file = other_file def __eq__(self, other): if not isinstance(other, Diff3Merge): return False return (self.base_file == other.base_file and self.other_file == other.other_file) def __ne__(self, other): return not (self == other) def apply(self, filename, conflict_handler, reverse=False): new_file = filename+".new" if not reverse: base = self.base_file other = self.other_file else: base = self.other_file other = self.base_file status = patch.diff3(new_file, filename, base, other) if status == 0: os.chmod(new_file, os.stat(filename).st_mode) os.rename(new_file, filename) return else: assert(status == 1) conflict_handler.merge_conflict(new_file, filename, base, other) def CreateDir(): """Convenience function to create a directory. :return: A ReplaceContents that will create a directory :rtype: `ReplaceContents` """ return ReplaceContents(None, dir_create) def DeleteDir(): """Convenience function to delete a directory. :return: A ReplaceContents that will delete a directory :rtype: `ReplaceContents` """ return ReplaceContents(dir_create, None) def CreateFile(contents): """Convenience fucntion to create a file. :param contents: The contents of the file to create :type contents: str :return: A ReplaceContents that will create a file :rtype: `ReplaceContents` """ return ReplaceContents(None, FileCreate(contents)) def DeleteFile(contents): """Convenience fucntion to delete a file. :param contents: The contents of the file to delete :type contents: str :return: A ReplaceContents that will delete a file :rtype: `ReplaceContents` """ return ReplaceContents(FileCreate(contents), None) def ReplaceFileContents(old_contents, new_contents): """Convenience fucntion to replace the contents of a file. :param old_contents: The contents of the file to replace :type old_contents: str :param new_contents: The contents to replace the file with :type new_contents: str :return: A ReplaceContents that will replace the contents of a file a file :rtype: `ReplaceContents` """ return ReplaceContents(FileCreate(old_contents), FileCreate(new_contents)) def CreateSymlink(target): """Convenience fucntion to create a symlink. :param target: The path the link should point to :type target: str :return: A ReplaceContents that will delete a file :rtype: `ReplaceContents` """ return ReplaceContents(None, SymlinkCreate(target)) def DeleteSymlink(target): """Convenience fucntion to delete a symlink. :param target: The path the link should point to :type target: str :return: A ReplaceContents that will delete a file :rtype: `ReplaceContents` """ return ReplaceContents(SymlinkCreate(target), None) def ChangeTarget(old_target, new_target): """Convenience fucntion to change the target of a symlink. :param old_target: The current link target :type old_target: str :param new_target: The new link target to use :type new_target: str :return: A ReplaceContents that will delete a file :rtype: `ReplaceContents` """ return ReplaceContents(SymlinkCreate(old_target), SymlinkCreate(new_target)) class InvalidEntry(Exception): """Raise when a ChangesetEntry is invalid in some way""" def __init__(self, entry, problem): """Constructor. :param entry: The invalid ChangesetEntry :type entry: `ChangesetEntry` :param problem: The problem with the entry :type problem: str """ msg = "Changeset entry for %s (%s) is invalid.\n%s" % (entry.id, entry.path, problem) Exception.__init__(self, msg) self.entry = entry class SourceRootHasName(InvalidEntry): """This changeset entry has a name other than "", but its parent is !NULL""" def __init__(self, entry, name): """Constructor. :param entry: The invalid ChangesetEntry :type entry: `ChangesetEntry` :param name: The name of the entry :type name: str """ msg = 'Child of !NULL is named "%s", not "./.".' % name InvalidEntry.__init__(self, entry, msg) class NullIDAssigned(InvalidEntry): """The id !NULL was assigned to a real entry""" def __init__(self, entry): """Constructor. :param entry: The invalid ChangesetEntry :type entry: `ChangesetEntry` """ msg = '"!NULL" id assigned to a file "%s".' % entry.path InvalidEntry.__init__(self, entry, msg) class ParentIDIsSelf(InvalidEntry): """An entry is marked as its own parent""" def __init__(self, entry): """Constructor. :param entry: The invalid ChangesetEntry :type entry: `ChangesetEntry` """ msg = 'file %s has "%s" id for both self id and parent id.' % \ (entry.path, entry.id) InvalidEntry.__init__(self, entry, msg) class ChangesetEntry(object): """An entry the changeset""" def __init__(self, id, parent, path): """Constructor. Sets parent and name assuming it was not renamed/created/deleted. :param id: The id associated with the entry :param parent: The id of the parent of this entry (or !NULL if no parent) :param path: The file path relative to the tree root of this entry """ self.id = id self.path = path self.new_path = path self.parent = parent self.new_parent = parent self.contents_change = None self.metadata_change = None if parent == NULL_ID and path !='./.': raise SourceRootHasName(self, path) if self.id == NULL_ID: raise NullIDAssigned(self) if self.id == self.parent: raise ParentIDIsSelf(self) def __str__(self): return "ChangesetEntry(%s)" % self.id def __get_dir(self): if self.path is None: return None return os.path.dirname(self.path) def __set_dir(self, dir): self.path = os.path.join(dir, os.path.basename(self.path)) dir = property(__get_dir, __set_dir) def __get_name(self): if self.path is None: return None return os.path.basename(self.path) def __set_name(self, name): self.path = os.path.join(os.path.dirname(self.path), name) name = property(__get_name, __set_name) def __get_new_dir(self): if self.new_path is None: return None return os.path.dirname(self.new_path) def __set_new_dir(self, dir): self.new_path = os.path.join(dir, os.path.basename(self.new_path)) new_dir = property(__get_new_dir, __set_new_dir) def __get_new_name(self): if self.new_path is None: return None return os.path.basename(self.new_path) def __set_new_name(self, name): self.new_path = os.path.join(os.path.dirname(self.new_path), name) new_name = property(__get_new_name, __set_new_name) def needs_rename(self): """Determines whether the entry requires renaming. :rtype: bool """ return (self.parent != self.new_parent or self.name != self.new_name) def is_deletion(self, reverse): """Return true if applying the entry would delete a file/directory. :param reverse: if true, the changeset is being applied in reverse :rtype: bool """ return ((self.new_parent is None and not reverse) or (self.parent is None and reverse)) def is_creation(self, reverse): """Return true if applying the entry would create a file/directory. :param reverse: if true, the changeset is being applied in reverse :rtype: bool """ return ((self.parent is None and not reverse) or (self.new_parent is None and reverse)) def is_creation_or_deletion(self): """Return true if applying the entry would create or delete a file/directory. :rtype: bool """ return self.parent is None or self.new_parent is None def get_cset_path(self, mod=False): """Determine the path of the entry according to the changeset. :param changeset: The changeset to derive the path from :type changeset: `Changeset` :param mod: If true, generate the MOD path. Otherwise, generate the \ ORIG path. :return: the path of the entry, or None if it did not exist in the \ requested tree. :rtype: str or NoneType """ if mod: if self.new_parent == NULL_ID: return "./." elif self.new_parent is None: return None return self.new_path else: if self.parent == NULL_ID: return "./." elif self.parent is None: return None return self.path def summarize_name(self, changeset, reverse=False): """Produce a one-line summary of the filename. Indicates renames as old => new, indicates creation as None => new, indicates deletion as old => None. :param changeset: The changeset to get paths from :type changeset: `Changeset` :param reverse: If true, reverse the names in the output :type reverse: bool :rtype: str """ orig_path = self.get_cset_path(False) mod_path = self.get_cset_path(True) if orig_path is not None: orig_path = orig_path[2:] if mod_path is not None: mod_path = mod_path[2:] if orig_path == mod_path: return orig_path else: if not reverse: return "%s => %s" % (orig_path, mod_path) else: return "%s => %s" % (mod_path, orig_path) def get_new_path(self, id_map, changeset, reverse=False): """Determine the full pathname to rename to :param id_map: The map of ids to filenames for the tree :type id_map: Dictionary :param changeset: The changeset to get data from :type changeset: `Changeset` :param reverse: If true, we're applying the changeset in reverse :type reverse: bool :rtype: str """ if reverse: parent = self.parent to_dir = self.dir from_dir = self.new_dir to_name = self.name from_name = self.new_name else: parent = self.new_parent to_dir = self.new_dir from_dir = self.dir to_name = self.new_name from_name = self.name if to_name is None: return None if parent == NULL_ID or parent is None: if to_name != '.': raise SourceRootHasName(self, to_name) else: return '.' if from_dir == to_dir: dir = os.path.dirname(id_map[self.id]) else: parent_entry = changeset.entries[parent] dir = parent_entry.get_new_path(id_map, changeset, reverse) if from_name == to_name: name = os.path.basename(id_map[self.id]) else: name = to_name assert(from_name is None or from_name == os.path.basename(id_map[self.id])) return os.path.join(dir, name) def is_boring(self): """Determines whether the entry does nothing :return: True if the entry does no renames or content changes :rtype: bool """ if self.contents_change is not None: return False elif self.metadata_change is not None: return False elif self.parent != self.new_parent: return False elif self.name != self.new_name: return False else: return True def apply(self, filename, conflict_handler, reverse=False): """Applies the file content and/or metadata changes. :param filename: the filename of the entry :type filename: str :param reverse: If true, apply the changes in reverse :type reverse: bool """ if self.is_deletion(reverse) and self.metadata_change is not None: self.metadata_change.apply(filename, conflict_handler, reverse) if self.contents_change is not None: self.contents_change.apply(filename, conflict_handler, reverse) if not self.is_deletion(reverse) and self.metadata_change is not None: self.metadata_change.apply(filename, conflict_handler, reverse) class IDPresent(Exception): def __init__(self, id): msg = "Cannot add entry because that id has already been used:\n%s" %\ id Exception.__init__(self, msg) self.id = id class Changeset(object): """A set of changes to apply""" def __init__(self): self.entries = {} def add_entry(self, entry): """Add an entry to the list of entries""" if self.entries.has_key(entry.id): raise IDPresent(entry.id) self.entries[entry.id] = entry def my_sort(sequence, key, reverse=False): """A sort function that supports supplying a key for comparison :param sequence: The sequence to sort :param key: A callable object that returns the values to be compared :param reverse: If true, sort in reverse order :type reverse: bool """ def cmp_by_key(entry_a, entry_b): if reverse: tmp=entry_a entry_a = entry_b entry_b = tmp return cmp(key(entry_a), key(entry_b)) sequence.sort(cmp_by_key) def get_rename_entries(changeset, inventory, reverse): """Return a list of entries that will be renamed. Entries are sorted from longest to shortest source path and from shortest to longest target path. :param changeset: The changeset to look in :type changeset: `Changeset` :param inventory: The source of current tree paths for the given ids :type inventory: Dictionary :param reverse: If true, the changeset is being applied in reverse :type reverse: bool :return: source entries and target entries as a tuple :rtype: (List, List) """ source_entries = [x for x in changeset.entries.itervalues() if x.needs_rename()] # these are done from longest path to shortest, to avoid deleting a # parent before its children are deleted/renamed def longest_to_shortest(entry): path = inventory.get(entry.id) if path is None: return 0 else: return len(path) my_sort(source_entries, longest_to_shortest, reverse=True) target_entries = source_entries[:] # These are done from shortest to longest path, to avoid creating a # child before its parent has been created/renamed def shortest_to_longest(entry): path = entry.get_new_path(inventory, changeset, reverse) if path is None: return 0 else: return len(path) my_sort(target_entries, shortest_to_longest) return (source_entries, target_entries) def rename_to_temp_delete(source_entries, inventory, dir, conflict_handler, reverse): """Delete and rename entries as appropriate. Entries are renamed to temp names. A map of id -> temp name is returned. :param source_entries: The entries to rename and delete :type source_entries: List of `ChangesetEntry` :param inventory: The map of id -> filename in the current tree :type inventory: Dictionary :param dir: The directory to apply changes to :type dir: str :param reverse: Apply changes in reverse :type reverse: bool :return: a mapping of id to temporary name :rtype: Dictionary """ temp_dir = os.path.join(dir, "temp") temp_name = {} for i in range(len(source_entries)): entry = source_entries[i] if entry.is_deletion(reverse): path = os.path.join(dir, inventory[entry.id]) entry.apply(path, conflict_handler, reverse) else: to_name = temp_dir+"/"+str(i) src_path = inventory.get(entry.id) if src_path is not None: src_path = os.path.join(dir, src_path) try: os.rename(src_path, to_name) temp_name[entry.id] = to_name except OSError, e: if e.errno != errno.ENOENT: raise if conflict_handler.missing_for_rename(src_path) == "skip": continue return temp_name def rename_to_new_create(temp_name, target_entries, inventory, changeset, dir, conflict_handler, reverse): """Rename entries with temp names to their final names, create new files. :param temp_name: A mapping of id to temporary name :type temp_name: Dictionary :param target_entries: The entries to apply changes to :type target_entries: List of `ChangesetEntry` :param changeset: The changeset to apply :type changeset: `Changeset` :param dir: The directory to apply changes to :type dir: str :param reverse: If true, apply changes in reverse :type reverse: bool """ for entry in target_entries: new_path = entry.get_new_path(inventory, changeset, reverse) if new_path is None: continue new_path = os.path.join(dir, new_path) old_path = temp_name.get(entry.id) if os.path.exists(new_path): if conflict_handler.target_exists(entry, new_path, old_path) == \ "skip": continue if entry.is_creation(reverse): entry.apply(new_path, conflict_handler, reverse) else: if old_path is None: continue try: os.rename(old_path, new_path) except OSError, e: raise Exception ("%s is missing" % new_path) class TargetExists(Exception): def __init__(self, entry, target): msg = "The path %s already exists" % target Exception.__init__(self, msg) self.entry = entry self.target = target class RenameConflict(Exception): def __init__(self, id, this_name, base_name, other_name): msg = """Trees all have different names for a file this: %s base: %s other: %s id: %s""" % (this_name, base_name, other_name, id) Exception.__init__(self, msg) self.this_name = this_name self.base_name = base_name self_other_name = other_name class MoveConflict(Exception): def __init__(self, id, this_parent, base_parent, other_parent): msg = """The file is in different directories in every tree this: %s base: %s other: %s id: %s""" % (this_parent, base_parent, other_parent, id) Exception.__init__(self, msg) self.this_parent = this_parent self.base_parent = base_parent self_other_parent = other_parent class MergeConflict(Exception): def __init__(self, this_path): Exception.__init__(self, "Conflict applying changes to %s" % this_path) self.this_path = this_path class MergePermissionConflict(Exception): def __init__(self, this_path, base_path, other_path): this_perms = os.stat(this_path).st_mode & 0755 base_perms = os.stat(base_path).st_mode & 0755 other_perms = os.stat(other_path).st_mode & 0755 msg = """Conflicting permission for %s this: %o base: %o other: %o """ % (this_path, this_perms, base_perms, other_perms) self.this_path = this_path self.base_path = base_path self.other_path = other_path Exception.__init__(self, msg) class WrongOldContents(Exception): def __init__(self, filename): msg = "Contents mismatch deleting %s" % filename self.filename = filename Exception.__init__(self, msg) class WrongOldPermissions(Exception): def __init__(self, filename, old_perms, new_perms): msg = "Permission missmatch on %s:\n" \ "Expected 0%o, got 0%o." % (filename, old_perms, new_perms) self.filename = filename Exception.__init__(self, msg) class RemoveContentsConflict(Exception): def __init__(self, filename): msg = "Conflict deleting %s, which has different contents in BASE"\ " and THIS" % filename self.filename = filename Exception.__init__(self, msg) class DeletingNonEmptyDirectory(Exception): def __init__(self, filename): msg = "Trying to remove dir %s while it still had files" % filename self.filename = filename Exception.__init__(self, msg) class PatchTargetMissing(Exception): def __init__(self, filename): msg = "Attempt to patch %s, which does not exist" % filename Exception.__init__(self, msg) self.filename = filename class MissingPermsFile(Exception): def __init__(self, filename): msg = "Attempt to change permissions on %s, which does not exist" %\ filename Exception.__init__(self, msg) self.filename = filename class MissingForRm(Exception): def __init__(self, filename): msg = "Attempt to remove missing path %s" % filename Exception.__init__(self, msg) self.filename = filename class MissingForRename(Exception): def __init__(self, filename): msg = "Attempt to move missing path %s" % (filename) Exception.__init__(self, msg) self.filename = filename class ExceptionConflictHandler(object): def __init__(self, dir): self.dir = dir def missing_parent(self, pathname): parent = os.path.dirname(pathname) raise Exception("Parent directory missing for %s" % pathname) def dir_exists(self, pathname): raise Exception("Directory already exists for %s" % pathname) def failed_hunks(self, pathname): raise Exception("Failed to apply some hunks for %s" % pathname) def target_exists(self, entry, target, old_path): raise TargetExists(entry, target) def rename_conflict(self, id, this_name, base_name, other_name): raise RenameConflict(id, this_name, base_name, other_name) def move_conflict(self, id, inventory): this_dir = inventory.this.get_dir(id) base_dir = inventory.base.get_dir(id) other_dir = inventory.other.get_dir(id) raise MoveConflict(id, this_dir, base_dir, other_dir) def merge_conflict(self, new_file, this_path, base_path, other_path): os.unlink(new_file) raise MergeConflict(this_path) def permission_conflict(self, this_path, base_path, other_path): raise MergePermissionConflict(this_path, base_path, other_path) def wrong_old_contents(self, filename, expected_contents): raise WrongOldContents(filename) def rem_contents_conflict(self, filename, this_contents, base_contents): raise RemoveContentsConflict(filename) def wrong_old_perms(self, filename, old_perms, new_perms): raise WrongOldPermissions(filename, old_perms, new_perms) def rmdir_non_empty(self, filename): raise DeletingNonEmptyDirectory(filename) def link_name_exists(self, filename): raise TargetExists(filename) def patch_target_missing(self, filename, contents): raise PatchTargetMissing(filename) def missing_for_chmod(self, filename): raise MissingPermsFile(filename) def missing_for_rm(self, filename, change): raise MissingForRm(filename) def missing_for_rename(self, filename): raise MissingForRename(filename) def finalize(): pass def apply_changeset(changeset, inventory, dir, conflict_handler=None, reverse=False): """Apply a changeset to a directory. :param changeset: The changes to perform :type changeset: `Changeset` :param inventory: The mapping of id to filename for the directory :type inventory: Dictionary :param dir: The path of the directory to apply the changes to :type dir: str :param reverse: If true, apply the changes in reverse :type reverse: bool :return: The mapping of the changed entries :rtype: Dictionary """ if conflict_handler is None: conflict_handler = ExceptionConflictHandler(dir) temp_dir = dir+"/temp" os.mkdir(temp_dir) #apply changes that don't affect filenames for entry in changeset.entries.itervalues(): if not entry.is_creation_or_deletion(): path = os.path.join(dir, inventory[entry.id]) entry.apply(path, conflict_handler, reverse) # Apply renames in stages, to minimize conflicts: # Only files whose name or parent change are interesting, because their # target name may exist in the source tree. If a directory's name changes, # that doesn't make its children interesting. (source_entries, target_entries) = get_rename_entries(changeset, inventory, reverse) temp_name = rename_to_temp_delete(source_entries, inventory, dir, conflict_handler, reverse) rename_to_new_create(temp_name, target_entries, inventory, changeset, dir, conflict_handler, reverse) os.rmdir(temp_dir) r_inventory = invert_dict(inventory) new_entries, removed_entries = get_inventory_change(inventory, r_inventory, changeset, reverse) new_inventory = {} for path, file_id in new_entries.iteritems(): new_inventory[file_id] = path for file_id in removed_entries: new_inventory[file_id] = None return new_inventory def apply_changeset_tree(cset, tree, reverse=False): r_inventory = {} for entry in tree.source_inventory().itervalues(): inventory[entry.id] = entry.path new_inventory = apply_changeset(cset, r_inventory, tree.root, reverse=reverse) new_entries, remove_entries = \ get_inventory_change(inventory, new_inventory, cset, reverse) tree.update_source_inventory(new_entries, remove_entries) def get_inventory_change(inventory, new_inventory, cset, reverse=False): new_entries = {} remove_entries = [] r_inventory = invert_dict(inventory) r_new_inventory = invert_dict(new_inventory) for entry in cset.entries.itervalues(): if entry.needs_rename(): old_path = r_inventory.get(entry.id) if old_path is not None: remove_entries.append(old_path) else: new_path = entry.get_new_path(inventory, cset) if new_path is not None: new_entries[new_path] = entry.id return new_entries, remove_entries def print_changeset(cset): """Print all non-boring changeset entries :param cset: The changeset to print :type cset: `Changeset` """ for entry in cset.entries.itervalues(): if entry.is_boring(): continue print entry.id print entry.summarize_name(cset) class CompositionFailure(Exception): def __init__(self, old_entry, new_entry, problem): msg = "Unable to conpose entries.\n %s" % problem Exception.__init__(self, msg) class IDMismatch(CompositionFailure): def __init__(self, old_entry, new_entry): problem = "Attempt to compose entries with different ids: %s and %s" %\ (old_entry.id, new_entry.id) CompositionFailure.__init__(self, old_entry, new_entry, problem) def compose_changesets(old_cset, new_cset): """Combine two changesets into one. This works well for exact patching. Otherwise, not so well. :param old_cset: The first changeset that would be applied :type old_cset: `Changeset` :param new_cset: The second changeset that would be applied :type new_cset: `Changeset` :return: A changeset that combines the changes in both changesets :rtype: `Changeset` """ composed = Changeset() for old_entry in old_cset.entries.itervalues(): new_entry = new_cset.entries.get(old_entry.id) if new_entry is None: composed.add_entry(old_entry) else: composed_entry = compose_entries(old_entry, new_entry) if composed_entry.parent is not None or\ composed_entry.new_parent is not None: composed.add_entry(composed_entry) for new_entry in new_cset.entries.itervalues(): if not old_cset.entries.has_key(new_entry.id): composed.add_entry(new_entry) return composed def compose_entries(old_entry, new_entry): """Combine two entries into one. :param old_entry: The first entry that would be applied :type old_entry: ChangesetEntry :param old_entry: The second entry that would be applied :type old_entry: ChangesetEntry :return: A changeset entry combining both entries :rtype: `ChangesetEntry` """ if old_entry.id != new_entry.id: raise IDMismatch(old_entry, new_entry) output = ChangesetEntry(old_entry.id, old_entry.parent, old_entry.path) if (old_entry.parent != old_entry.new_parent or new_entry.parent != new_entry.new_parent): output.new_parent = new_entry.new_parent if (old_entry.path != old_entry.new_path or new_entry.path != new_entry.new_path): output.new_path = new_entry.new_path output.contents_change = compose_contents(old_entry, new_entry) output.metadata_change = compose_metadata(old_entry, new_entry) return output def compose_contents(old_entry, new_entry): """Combine the contents of two changeset entries. Entries are combined intelligently where possible, but the fallback behavior returns an ApplySequence. :param old_entry: The first entry that would be applied :type old_entry: `ChangesetEntry` :param new_entry: The second entry that would be applied :type new_entry: `ChangesetEntry` :return: A combined contents change :rtype: anything supporting the apply(reverse=False) method """ old_contents = old_entry.contents_change new_contents = new_entry.contents_change if old_entry.contents_change is None: return new_entry.contents_change elif new_entry.contents_change is None: return old_entry.contents_change elif isinstance(old_contents, ReplaceContents) and \ isinstance(new_contents, ReplaceContents): if old_contents.old_contents == new_contents.new_contents: return None else: return ReplaceContents(old_contents.old_contents, new_contents.new_contents) elif isinstance(old_contents, ApplySequence): output = ApplySequence(old_contents.changes) if isinstance(new_contents, ApplySequence): output.changes.extend(new_contents.changes) else: output.changes.append(new_contents) return output elif isinstance(new_contents, ApplySequence): output = ApplySequence((old_contents.changes,)) output.extend(new_contents.changes) return output else: return ApplySequence((old_contents, new_contents)) def compose_metadata(old_entry, new_entry): old_meta = old_entry.metadata_change new_meta = new_entry.metadata_change if old_meta is None: return new_meta elif new_meta is None: return old_meta elif isinstance(old_meta, ChangeUnixPermissions) and \ isinstance(new_meta, ChangeUnixPermissions): return ChangeUnixPermissions(old_meta.old_mode, new_meta.new_mode) else: return ApplySequence(old_meta, new_meta) def changeset_is_null(changeset): for entry in changeset.entries.itervalues(): if not entry.is_boring(): return False return True class UnsuppportedFiletype(Exception): def __init__(self, full_path, stat_result): msg = "The file \"%s\" is not a supported filetype." % full_path Exception.__init__(self, msg) self.full_path = full_path self.stat_result = stat_result def generate_changeset(tree_a, tree_b, inventory_a=None, inventory_b=None): return ChangesetGenerator(tree_a, tree_b, inventory_a, inventory_b)() class ChangesetGenerator(object): def __init__(self, tree_a, tree_b, inventory_a=None, inventory_b=None): object.__init__(self) self.tree_a = tree_a self.tree_b = tree_b if inventory_a is not None: self.inventory_a = inventory_a else: self.inventory_a = tree_a.inventory() if inventory_b is not None: self.inventory_b = inventory_b else: self.inventory_b = tree_b.inventory() self.r_inventory_a = self.reverse_inventory(self.inventory_a) self.r_inventory_b = self.reverse_inventory(self.inventory_b) def reverse_inventory(self, inventory): r_inventory = {} for entry in inventory.itervalues(): if entry.id is None: continue r_inventory[entry.id] = entry return r_inventory def __call__(self): cset = Changeset() for entry in self.inventory_a.itervalues(): if entry.id is None: continue cs_entry = self.make_entry(entry.id) if cs_entry is not None and not cs_entry.is_boring(): cset.add_entry(cs_entry) for entry in self.inventory_b.itervalues(): if entry.id is None: continue if not self.r_inventory_a.has_key(entry.id): cs_entry = self.make_entry(entry.id) if cs_entry is not None and not cs_entry.is_boring(): cset.add_entry(cs_entry) for entry in list(cset.entries.itervalues()): if entry.parent != entry.new_parent: if not cset.entries.has_key(entry.parent) and\ entry.parent != NULL_ID and entry.parent is not None: parent_entry = self.make_boring_entry(entry.parent) cset.add_entry(parent_entry) if not cset.entries.has_key(entry.new_parent) and\ entry.new_parent != NULL_ID and \ entry.new_parent is not None: parent_entry = self.make_boring_entry(entry.new_parent) cset.add_entry(parent_entry) return cset def get_entry_parent(self, entry, inventory): if entry is None: return None if entry.path == "./.": return NULL_ID dirname = os.path.dirname(entry.path) if dirname == ".": dirname = "./." parent = inventory[dirname] return parent.id def get_paths(self, entry, tree): if entry is None: return (None, None) full_path = tree.readonly_path(entry.id) if entry.path == ".": return ("", full_path) return (entry.path, full_path) def make_basic_entry(self, id, only_interesting): entry_a = self.r_inventory_a.get(id) entry_b = self.r_inventory_b.get(id) if only_interesting and not self.is_interesting(entry_a, entry_b): return (None, None, None) parent = self.get_entry_parent(entry_a, self.inventory_a) (path, full_path_a) = self.get_paths(entry_a, self.tree_a) cs_entry = ChangesetEntry(id, parent, path) new_parent = self.get_entry_parent(entry_b, self.inventory_b) (new_path, full_path_b) = self.get_paths(entry_b, self.tree_b) cs_entry.new_path = new_path cs_entry.new_parent = new_parent return (cs_entry, full_path_a, full_path_b) def is_interesting(self, entry_a, entry_b): if entry_a is not None: if entry_a.interesting: return True if entry_b is not None: if entry_b.interesting: return True return False def make_boring_entry(self, id): (cs_entry, full_path_a, full_path_b) = \ self.make_basic_entry(id, only_interesting=False) if cs_entry.is_creation_or_deletion(): return self.make_entry(id, only_interesting=False) else: return cs_entry def make_entry(self, id, only_interesting=True): (cs_entry, full_path_a, full_path_b) = \ self.make_basic_entry(id, only_interesting) if cs_entry is None: return None stat_a = self.lstat(full_path_a) stat_b = self.lstat(full_path_b) if stat_b is None: cs_entry.new_parent = None cs_entry.new_path = None cs_entry.metadata_change = self.make_mode_change(stat_a, stat_b) cs_entry.contents_change = self.make_contents_change(full_path_a, stat_a, full_path_b, stat_b) return cs_entry def make_mode_change(self, stat_a, stat_b): mode_a = None if stat_a is not None and not stat.S_ISLNK(stat_a.st_mode): mode_a = stat_a.st_mode & 0777 mode_b = None if stat_b is not None and not stat.S_ISLNK(stat_b.st_mode): mode_b = stat_b.st_mode & 0777 if mode_a == mode_b: return None return ChangeUnixPermissions(mode_a, mode_b) def make_contents_change(self, full_path_a, stat_a, full_path_b, stat_b): if stat_a is None and stat_b is None: return None if None not in (stat_a, stat_b) and stat.S_ISDIR(stat_a.st_mode) and\ stat.S_ISDIR(stat_b.st_mode): return None if None not in (stat_a, stat_b) and stat.S_ISREG(stat_a.st_mode) and\ stat.S_ISREG(stat_b.st_mode): if stat_a.st_ino == stat_b.st_ino and \ stat_a.st_dev == stat_b.st_dev: return None if file(full_path_a, "rb").read() == \ file(full_path_b, "rb").read(): return None patch_contents = patch.diff(full_path_a, file(full_path_b, "rb").read()) if patch_contents is None: return None return PatchApply(patch_contents) a_contents = self.get_contents(stat_a, full_path_a) b_contents = self.get_contents(stat_b, full_path_b) if a_contents == b_contents: return None return ReplaceContents(a_contents, b_contents) def get_contents(self, stat_result, full_path): if stat_result is None: return None elif stat.S_ISREG(stat_result.st_mode): return FileCreate(file(full_path, "rb").read()) elif stat.S_ISDIR(stat_result.st_mode): return dir_create elif stat.S_ISLNK(stat_result.st_mode): return SymlinkCreate(os.readlink(full_path)) else: raise UnsupportedFiletype(full_path, stat_result) def lstat(self, full_path): stat_result = None if full_path is not None: try: stat_result = os.lstat(full_path) except OSError, e: if e.errno != errno.ENOENT: raise return stat_result def full_path(entry, tree): return os.path.join(tree.root, entry.path) def new_delete_entry(entry, tree, inventory, delete): if entry.path == "": parent = NULL_ID else: parent = inventory[dirname(entry.path)].id cs_entry = ChangesetEntry(parent, entry.path) if delete: cs_entry.new_path = None cs_entry.new_parent = None else: cs_entry.path = None cs_entry.parent = None full_path = full_path(entry, tree) status = os.lstat(full_path) if stat.S_ISDIR(file_stat.st_mode): action = dir_create class Inventory(object): def __init__(self, inventory): self.inventory = inventory self.rinventory = None def get_rinventory(self): if self.rinventory is None: self.rinventory = invert_dict(self.inventory) return self.rinventory def get_path(self, id): return self.inventory.get(id) def get_name(self, id): return os.path.basename(self.get_path(id)) def get_dir(self, id): path = self.get_path(id) if path == "": return None return os.path.dirname(path) def get_parent(self, id): directory = self.get_dir(id) if directory == '.': directory = './.' if directory is None: return NULL_ID return self.get_rinventory().get(directory) M 644 inline bzrlib/commands.py data 39229 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest if selftest(): return 0 else: return 1 class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. """ takes_args = ['other_spec', 'base_spec?'] def run(self, other_spec, base_spec=None): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec)) class cmd_revert(Command): """ Reverse all changes since the last commit. Only versioned files are affected. """ takes_options = ['revision'] def run(self, revision=-1): merge.merge(('.', revision), parse_spec('.'), no_changes=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/diff.py data 13904 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from trace import mutter from errors import BzrError def internal_diff(old_label, oldlines, new_label, newlines, to_file): import difflib # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, fromfile=old_label, tofile=new_label) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') to_file.writelines(ud) if nonl: print >>to_file, "\\ No newline at end of file" print >>to_file def external_diff(old_label, oldlines, new_label, newlines, to_file, diff_opts): """Display a diff by calling out to the external diff program.""" import sys if to_file != sys.stdout: raise NotImplementedError("sorry, can't send external diff other than to stdout yet", to_file) # make sure our own output is properly ordered before the diff to_file.flush() from tempfile import NamedTemporaryFile import os oldtmpf = NamedTemporaryFile() newtmpf = NamedTemporaryFile() try: # TODO: perhaps a special case for comparing to or from the empty # sequence; can just use /dev/null on Unix # TODO: if either of the files being compared already exists as a # regular named file (e.g. in the working directory) then we can # compare directly to that, rather than copying it. oldtmpf.writelines(oldlines) newtmpf.writelines(newlines) oldtmpf.flush() newtmpf.flush() if not diff_opts: diff_opts = [] diffcmd = ['diff', '--label', old_label, oldtmpf.name, '--label', new_label, newtmpf.name] # diff only allows one style to be specified; they don't override. # note that some of these take optargs, and the optargs can be # directly appended to the options. # this is only an approximate parser; it doesn't properly understand # the grammar. for s in ['-c', '-u', '-C', '-U', '-e', '--ed', '-q', '--brief', '--normal', '-n', '--rcs', '-y', '--side-by-side', '-D', '--ifdef']: for j in diff_opts: if j.startswith(s): break else: continue break else: diffcmd.append('-u') if diff_opts: diffcmd.extend(diff_opts) rc = os.spawnvp(os.P_WAIT, 'diff', diffcmd) if rc != 0 and rc != 1: # returns 1 if files differ; that's OK if rc < 0: msg = 'signal %d' % (-rc) else: msg = 'exit code %d' % rc raise BzrError('external diff failed with %s; command: %r' % (rc, diffcmd)) finally: oldtmpf.close() # and delete newtmpf.close() def show_diff(b, revision, specific_files, external_diff_options=None): """Shortcut for showing the diff to the working tree. b Branch. revision None for each, or otherwise the old revision to compare against. The more general form is show_diff_trees(), where the caller supplies any two trees. """ import sys if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() show_diff_trees(old_tree, new_tree, sys.stdout, specific_files, external_diff_options) def show_diff_trees(old_tree, new_tree, to_file, specific_files=None, external_diff_options=None): """Show in text form the changes from one tree to another. to_files If set, include only changes to these files. external_diff_options If set, use an external GNU diff and pass these options. """ # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. if external_diff_options: assert isinstance(external_diff_options, basestring) opts = external_diff_options.split() def diff_file(olab, olines, nlab, nlines, to_file): external_diff(olab, olines, nlab, nlines, to_file, opts) else: diff_file = internal_diff delta = compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=specific_files) for path, file_id, kind in delta.removed: print '*** removed %s %r' % (kind, path) if kind == 'file': diff_file(old_label + path, old_tree.get_file(file_id).readlines(), DEVNULL, [], to_file) for path, file_id, kind in delta.added: print '*** added %s %r' % (kind, path) if kind == 'file': diff_file(DEVNULL, [], new_label + path, new_tree.get_file(file_id).readlines(), to_file) for old_path, new_path, file_id, kind, text_modified in delta.renamed: print '*** renamed %s %r => %r' % (kind, old_path, new_path) if text_modified: diff_file(old_label + old_path, old_tree.get_file(file_id).readlines(), new_label + new_path, new_tree.get_file(file_id).readlines(), to_file) for path, file_id, kind in delta.modified: print '*** modified %s %r' % (kind, path) if kind == 'file': diff_file(old_label + path, old_tree.get_file(file_id).readlines(), new_label + path, new_tree.get_file(file_id).readlines(), to_file) class TreeDelta(object): """Describes changes from one tree to another. Contains four lists: added (path, id, kind) removed (path, id, kind) renamed (oldpath, newpath, id, kind, text_modified) modified (path, id, kind) unchanged (path, id, kind) Each id is listed only once. Files that are both modified and renamed are listed only in renamed, with the text_modified flag true. The lists are normally sorted when the delta is created. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] self.unchanged = [] def has_changed(self): changes = len(self.added) + len(self.removed) + len(self.renamed) changes += len(self.modified) return (changes != 0) def touches_file_id(self, file_id): """Return True if file_id is modified by this delta.""" for l in self.added, self.removed, self.modified: for v in l: if v[1] == file_id: return True for v in self.renamed: if v[2] == file_id: return True return False def show(self, to_file, show_ids=False, show_unchanged=False): def show_list(files): for path, fid, kind in files: if kind == 'directory': path += '/' elif kind == 'symlink': path += '@' if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if self.removed: print >>to_file, 'removed:' show_list(self.removed) if self.added: print >>to_file, 'added:' show_list(self.added) if self.renamed: print >>to_file, 'renamed:' for oldpath, newpath, fid, kind, text_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if self.modified: print >>to_file, 'modified:' show_list(self.modified) if show_unchanged and self.unchanged: print >>to_file, 'unchanged:' show_list(self.unchanged) def compare_trees(old_tree, new_tree, want_unchanged, specific_files=None): """Describe changes from one tree to another. Returns a TreeDelta with details of added, modified, renamed, and deleted entries. The root entry is specifically exempt. This only considers versioned files. want_unchanged If true, also list files unchanged from one version to the next. specific_files If true, only check for changes to specified names or files within them. """ from osutils import is_inside_any old_inv = old_tree.inventory new_inv = new_tree.inventory delta = TreeDelta() mutter('start compare_trees') # TODO: match for specific files can be rather smarter by finding # the IDs of those files up front and then considering only that. for file_id in old_tree: if file_id in new_tree: kind = old_inv.get_file_kind(file_id) assert kind == new_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ 'invalid file kind %r' % kind if kind == 'root_directory': continue old_path = old_inv.id2path(file_id) new_path = new_inv.id2path(file_id) if specific_files: if (not is_inside_any(specific_files, old_path) and not is_inside_any(specific_files, new_path)): continue if kind == 'file': old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False # TODO: Can possibly avoid calculating path strings if the # two files are unchanged and their names and parents are # the same and the parents are unchanged all the way up. # May not be worthwhile. if old_path != new_path: delta.renamed.append((old_path, new_path, file_id, kind, text_modified)) elif text_modified: delta.modified.append((new_path, file_id, kind)) elif want_unchanged: delta.unchanged.append((new_path, file_id, kind)) else: kind = old_inv.get_file_kind(file_id) old_path = old_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, old_path): continue delta.removed.append((old_path, file_id, kind)) mutter('start looking for new files') for file_id in new_inv: if file_id in old_inv: continue new_path = new_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, new_path): continue kind = new_inv.get_file_kind(file_id) delta.added.append((new_path, file_id, kind)) delta.removed.sort() delta.added.sort() delta.renamed.sort() delta.modified.sort() delta.unchanged.sort() return delta M 644 inline bzrlib/merge.py data 10016 from merge_core import merge_flex from changeset import generate_changeset, ExceptionConflictHandler from changeset import Inventory from bzrlib import find_branch import bzrlib.osutils from bzrlib.errors import BzrCommandError from bzrlib.diff import compare_trees from trace import mutter, warning import os.path import tempfile import shutil import errno class UnrelatedBranches(BzrCommandError): def __init__(self): msg = "Branches have no common ancestor, and no base revision"\ " specified." BzrCommandError.__init__(self, msg) class MergeConflictHandler(ExceptionConflictHandler): """Handle conflicts encountered while merging""" def __init__(self, dir, ignore_zero=False): ExceptionConflictHandler.__init__(self, dir) self.conflicts = 0 self.ignore_zero = ignore_zero def copy(self, source, dest): """Copy the text and mode of a file :param source: The path of the file to copy :param dest: The distination file to create """ s_file = file(source, "rb") d_file = file(dest, "wb") for line in s_file: d_file.write(line) os.chmod(dest, 0777 & os.stat(source).st_mode) def add_suffix(self, name, suffix, last_new_name=None): """Rename a file to append a suffix. If the new name exists, the suffix is added repeatedly until a non-existant name is found :param name: The path of the file :param suffix: The suffix to append :param last_new_name: (used for recursive calls) the last name tried """ if last_new_name is None: last_new_name = name new_name = last_new_name+suffix try: os.rename(name, new_name) return new_name except OSError, e: if e.errno != errno.EEXIST and e.errno != errno.ENOTEMPTY: raise return self.add_suffix(name, suffix, last_new_name=new_name) def conflict(self, text): warning(text) self.conflicts += 1 def merge_conflict(self, new_file, this_path, base_path, other_path): """ Handle diff3 conflicts by producing a .THIS, .BASE and .OTHER. The main file will be a version with diff3 conflicts. :param new_file: Path to the output file with diff3 markers :param this_path: Path to the file text for the THIS tree :param base_path: Path to the file text for the BASE tree :param other_path: Path to the file text for the OTHER tree """ self.add_suffix(this_path, ".THIS") self.copy(base_path, this_path+".BASE") self.copy(other_path, this_path+".OTHER") os.rename(new_file, this_path) self.conflict("Diff3 conflict encountered in %s" % this_path) def target_exists(self, entry, target, old_path): """Handle the case when the target file or dir exists""" moved_path = self.add_suffix(target, ".moved") self.conflict("Moved existing %s to %s" % (target, moved_path)) def finalize(self): if not self.ignore_zero: print "%d conflicts encountered.\n" % self.conflicts class SourceFile(object): def __init__(self, path, id, present=None, isdir=None): self.path = path self.id = id self.present = present self.isdir = isdir self.interesting = True def __repr__(self): return "SourceFile(%s, %s)" % (self.path, self.id) def get_tree(treespec, temp_root, label): location, revno = treespec branch = find_branch(location) if revno is None: base_tree = branch.working_tree() elif revno == -1: base_tree = branch.basis_tree() else: base_tree = branch.revision_tree(branch.lookup_revision(revno)) temp_path = os.path.join(temp_root, label) os.mkdir(temp_path) return branch, MergeTree(base_tree, temp_path) def abspath(tree, file_id): path = tree.inventory.id2path(file_id) if path == "": return "./." return "./" + path def file_exists(tree, file_id): return tree.has_filename(tree.id2path(file_id)) def inventory_map(tree): inventory = {} for file_id in tree.inventory: if not file_exists(tree, file_id): continue path = abspath(tree, file_id) inventory[path] = SourceFile(path, file_id) return inventory class MergeTree(object): def __init__(self, tree, tempdir): object.__init__(self) if hasattr(tree, "basedir"): self.root = tree.basedir else: self.root = None self.inventory = inventory_map(tree) self.tree = tree self.tempdir = tempdir os.mkdir(os.path.join(self.tempdir, "texts")) self.cached = {} def readonly_path(self, id): if self.root is not None: return self.tree.abspath(self.tree.id2path(id)) else: if self.tree.inventory[id].kind in ("directory", "root_directory"): return self.tempdir if not self.cached.has_key(id): path = os.path.join(self.tempdir, "texts", id) outfile = file(path, "wb") outfile.write(self.tree.get_file(id).read()) assert(os.path.exists(path)) self.cached[id] = path return self.cached[id] def merge(other_revision, base_revision, no_changes=True, ignore_zero=False): tempdir = tempfile.mkdtemp(prefix="bzr-") try: this_branch = find_branch('.') if no_changes: changes = compare_trees(this_branch.working_tree(), this_branch.basis_tree(), False) if changes.has_changed(): raise BzrCommandError("Working tree has uncommitted changes.") other_branch, other_tree = get_tree(other_revision, tempdir, "other") if base_revision == [None, None]: if other_revision[1] == -1: o_revno = None else: o_revno = other_revision[1] base_revno = this_branch.common_ancestor(other_branch, other_revno=o_revno)[0] if base_revno is None: raise UnrelatedBranches() base_revision = ['.', base_revno] base_branch, base_tree = get_tree(base_revision, tempdir, "base") merge_inner(this_branch, other_tree, base_tree, tempdir, ignore_zero=ignore_zero) finally: shutil.rmtree(tempdir) def generate_cset_optimized(tree_a, tree_b, inventory_a, inventory_b): """Generate a changeset, using the text_id to mark really-changed files. This permits blazing comparisons when text_ids are present. It also disables metadata comparison for files with identical texts. """ for file_id in tree_a.tree.inventory: if file_id not in tree_b.tree.inventory: continue entry_a = tree_a.tree.inventory[file_id] entry_b = tree_b.tree.inventory[file_id] if (entry_a.kind, entry_b.kind) != ("file", "file"): continue if None in (entry_a.text_id, entry_b.text_id): continue if entry_a.text_id != entry_b.text_id: continue inventory_a[abspath(tree_a.tree, file_id)].interesting = False inventory_b[abspath(tree_b.tree, file_id)].interesting = False cset = generate_changeset(tree_a, tree_b, inventory_a, inventory_b) for entry in cset.entries.itervalues(): entry.metadata_change = None return cset def merge_inner(this_branch, other_tree, base_tree, tempdir, ignore_zero=False): this_tree = get_tree(('.', None), tempdir, "this")[1] def get_inventory(tree): return tree.inventory inv_changes = merge_flex(this_tree, base_tree, other_tree, generate_cset_optimized, get_inventory, MergeConflictHandler(base_tree.root, ignore_zero=ignore_zero)) adjust_ids = [] for id, path in inv_changes.iteritems(): if path is not None: if path == '.': path = '' else: assert path.startswith('./') path = path[2:] adjust_ids.append((path, id)) this_branch.set_inventory(regen_inventory(this_branch, this_tree.root, adjust_ids)) def regen_inventory(this_branch, root, new_entries): old_entries = this_branch.read_working_inventory() new_inventory = {} by_path = {} for file_id in old_entries: entry = old_entries[file_id] path = old_entries.id2path(file_id) new_inventory[file_id] = (path, file_id, entry.parent_id, entry.kind) by_path[path] = file_id deletions = 0 insertions = 0 new_path_list = [] for path, file_id in new_entries: if path is None: del new_inventory[file_id] deletions += 1 else: new_path_list.append((path, file_id)) if file_id not in old_entries: insertions += 1 # Ensure no file is added before its parent new_path_list.sort() for path, file_id in new_path_list: if path == '': parent = None else: parent = by_path[os.path.dirname(path)] kind = bzrlib.osutils.file_kind(os.path.join(root, path)) new_inventory[file_id] = (path, file_id, parent, kind) by_path[path] = file_id # Get a list in insertion order new_inventory_list = new_inventory.values() mutter ("""Inventory regeneration: old length: %i insertions: %i deletions: %i new_length: %i"""\ % (len(old_entries), insertions, deletions, len(new_inventory_list))) assert len(new_inventory_list) == len(old_entries) + insertions - deletions new_inventory_list.sort() return new_inventory_list M 644 inline bzrlib/merge_core.py data 19560 import changeset from changeset import Inventory, apply_changeset, invert_dict import os.path class ThreewayInventory(object): def __init__(self, this_inventory, base_inventory, other_inventory): self.this = this_inventory self.base = base_inventory self.other = other_inventory def invert_invent(inventory): invert_invent = {} for key, value in inventory.iteritems(): invert_invent[value.id] = key return invert_invent def make_inv(inventory): return Inventory(invert_invent(inventory)) def merge_flex(this, base, other, changeset_function, inventory_function, conflict_handler): this_inventory = inventory_function(this) base_inventory = inventory_function(base) other_inventory = inventory_function(other) inventory = ThreewayInventory(make_inv(this_inventory), make_inv(base_inventory), make_inv(other_inventory)) cset = changeset_function(base, other, base_inventory, other_inventory) new_cset = make_merge_changeset(cset, inventory, this, base, other, conflict_handler) result = apply_changeset(new_cset, invert_invent(this_inventory), this.root, conflict_handler, False) conflict_handler.finalize() return result def make_merge_changeset(cset, inventory, this, base, other, conflict_handler=None): new_cset = changeset.Changeset() def get_this_contents(id): path = os.path.join(this.root, inventory.this.get_path(id)) if os.path.isdir(path): return changeset.dir_create else: return changeset.FileCreate(file(path, "rb").read()) for entry in cset.entries.itervalues(): if entry.is_boring(): new_cset.add_entry(entry) elif entry.is_creation(False): if inventory.this.get_path(entry.id) is None: new_cset.add_entry(entry) else: this_contents = get_this_contents(entry.id) other_contents = entry.contents_change.new_contents if other_contents == this_contents: boring_entry = changeset.ChangesetEntry(entry.id, entry.new_parent, entry.new_path) new_cset.add_entry(boring_entry) else: conflict_handler.contents_conflict(this_contents, other_contents) elif entry.is_deletion(False): if inventory.this.get_path(entry.id) is None: boring_entry = changeset.ChangesetEntry(entry.id, entry.parent, entry.path) new_cset.add_entry(boring_entry) elif entry.contents_change is not None: this_contents = get_this_contents(entry.id) base_contents = entry.contents_change.old_contents if base_contents == this_contents: new_cset.add_entry(entry) else: entry_path = inventory.this.get_path(entry.id) conflict_handler.rem_contents_conflict(entry_path, this_contents, base_contents) else: new_cset.add_entry(entry) else: entry = get_merge_entry(entry, inventory, base, other, conflict_handler) if entry is not None: new_cset.add_entry(entry) return new_cset def get_merge_entry(entry, inventory, base, other, conflict_handler): this_name = inventory.this.get_name(entry.id) this_parent = inventory.this.get_parent(entry.id) this_dir = inventory.this.get_dir(entry.id) if this_dir is None: this_dir = "" if this_name is None: return conflict_handler.merge_missing(entry.id, inventory) base_name = inventory.base.get_name(entry.id) base_parent = inventory.base.get_parent(entry.id) base_dir = inventory.base.get_dir(entry.id) if base_dir is None: base_dir = "" other_name = inventory.other.get_name(entry.id) other_parent = inventory.other.get_parent(entry.id) other_dir = inventory.base.get_dir(entry.id) if other_dir is None: other_dir = "" if base_name == other_name: old_name = this_name new_name = this_name else: if this_name != base_name and this_name != other_name: conflict_handler.rename_conflict(entry.id, this_name, base_name, other_name) else: old_name = this_name new_name = other_name if base_parent == other_parent: old_parent = this_parent new_parent = this_parent old_dir = this_dir new_dir = this_dir else: if this_parent != base_parent and this_parent != other_parent: conflict_handler.move_conflict(entry.id, inventory) else: old_parent = this_parent old_dir = this_dir new_parent = other_parent new_dir = other_dir old_path = os.path.join(old_dir, old_name) new_entry = changeset.ChangesetEntry(entry.id, old_parent, old_name) if new_name is not None or new_parent is not None: new_entry.new_path = os.path.join(new_dir, new_name) else: new_entry.new_path = None new_entry.new_parent = new_parent base_path = base.readonly_path(entry.id) other_path = other.readonly_path(entry.id) if entry.contents_change is not None: new_entry.contents_change = changeset.Diff3Merge(base_path, other_path) if entry.metadata_change is not None: new_entry.metadata_change = PermissionsMerge(base_path, other_path) return new_entry class PermissionsMerge(object): def __init__(self, base_path, other_path): self.base_path = base_path self.other_path = other_path def apply(self, filename, conflict_handler, reverse=False): if not reverse: base = self.base_path other = self.other_path else: base = self.other_path other = self.base_path base_stat = os.stat(base).st_mode other_stat = os.stat(other).st_mode this_stat = os.stat(filename).st_mode if base_stat &0777 == other_stat &0777: return elif this_stat &0777 == other_stat &0777: return elif this_stat &0777 == base_stat &0777: os.chmod(filename, other_stat) else: conflict_handler.permission_conflict(filename, base, other) import unittest import tempfile import shutil class MergeTree(object): def __init__(self, dir): self.dir = dir; os.mkdir(dir) self.inventory = {'0': ""} def child_path(self, parent, name): return os.path.join(self.inventory[parent], name) def add_file(self, id, parent, name, contents, mode): path = self.child_path(parent, name) full_path = self.abs_path(path) assert not os.path.exists(full_path) file(full_path, "wb").write(contents) os.chmod(self.abs_path(path), mode) self.inventory[id] = path def add_dir(self, id, parent, name, mode): path = self.child_path(parent, name) full_path = self.abs_path(path) assert not os.path.exists(full_path) os.mkdir(self.abs_path(path)) os.chmod(self.abs_path(path), mode) self.inventory[id] = path def abs_path(self, path): return os.path.join(self.dir, path) def full_path(self, id): return self.abs_path(self.inventory[id]) def change_path(self, id, path): new = os.path.join(self.dir, self.inventory[id]) os.rename(self.abs_path(self.inventory[id]), self.abs_path(path)) self.inventory[id] = path class MergeBuilder(object): def __init__(self): self.dir = tempfile.mkdtemp(prefix="BaZing") self.base = MergeTree(os.path.join(self.dir, "base")) self.this = MergeTree(os.path.join(self.dir, "this")) self.other = MergeTree(os.path.join(self.dir, "other")) self.cset = changeset.Changeset() self.cset.add_entry(changeset.ChangesetEntry("0", changeset.NULL_ID, "./.")) def get_cset_path(self, parent, name): if name is None: assert (parent is None) return None return os.path.join(self.cset.entries[parent].path, name) def add_file(self, id, parent, name, contents, mode): self.base.add_file(id, parent, name, contents, mode) self.this.add_file(id, parent, name, contents, mode) self.other.add_file(id, parent, name, contents, mode) path = self.get_cset_path(parent, name) self.cset.add_entry(changeset.ChangesetEntry(id, parent, path)) def add_dir(self, id, parent, name, mode): path = self.get_cset_path(parent, name) self.base.add_dir(id, parent, name, mode) self.cset.add_entry(changeset.ChangesetEntry(id, parent, path)) self.this.add_dir(id, parent, name, mode) self.other.add_dir(id, parent, name, mode) def change_name(self, id, base=None, this=None, other=None): if base is not None: self.change_name_tree(id, self.base, base) self.cset.entries[id].name = base if this is not None: self.change_name_tree(id, self.this, this) if other is not None: self.change_name_tree(id, self.other, other) self.cset.entries[id].new_name = other def change_parent(self, id, base=None, this=None, other=None): if base is not None: self.change_parent_tree(id, self.base, base) self.cset.entries[id].parent = base self.cset.entries[id].dir = self.cset.entries[base].path if this is not None: self.change_parent_tree(id, self.this, this) if other is not None: self.change_parent_tree(id, self.other, other) self.cset.entries[id].new_parent = other self.cset.entries[id].new_dir = \ self.cset.entries[other].new_path def change_contents(self, id, base=None, this=None, other=None): if base is not None: self.change_contents_tree(id, self.base, base) if this is not None: self.change_contents_tree(id, self.this, this) if other is not None: self.change_contents_tree(id, self.other, other) if base is not None or other is not None: old_contents = file(self.base.full_path(id)).read() new_contents = file(self.other.full_path(id)).read() contents = changeset.ReplaceFileContents(old_contents, new_contents) self.cset.entries[id].contents_change = contents def change_perms(self, id, base=None, this=None, other=None): if base is not None: self.change_perms_tree(id, self.base, base) if this is not None: self.change_perms_tree(id, self.this, this) if other is not None: self.change_perms_tree(id, self.other, other) if base is not None or other is not None: old_perms = os.stat(self.base.full_path(id)).st_mode &077 new_perms = os.stat(self.other.full_path(id)).st_mode &077 contents = changeset.ChangeUnixPermissions(old_perms, new_perms) self.cset.entries[id].metadata_change = contents def change_name_tree(self, id, tree, name): new_path = tree.child_path(self.cset.entries[id].parent, name) tree.change_path(id, new_path) def change_parent_tree(self, id, tree, parent): new_path = tree.child_path(parent, self.cset.entries[id].name) tree.change_path(id, new_path) def change_contents_tree(self, id, tree, contents): path = tree.full_path(id) mode = os.stat(path).st_mode file(path, "w").write(contents) os.chmod(path, mode) def change_perms_tree(self, id, tree, mode): os.chmod(tree.full_path(id), mode) def merge_changeset(self): all_inventory = ThreewayInventory(Inventory(self.this.inventory), Inventory(self.base.inventory), Inventory(self.other.inventory)) conflict_handler = changeset.ExceptionConflictHandler(self.this.dir) return make_merge_changeset(self.cset, all_inventory, self.this.dir, self.base.dir, self.other.dir, conflict_handler) def apply_changeset(self, cset, conflict_handler=None, reverse=False): self.this.inventory = \ changeset.apply_changeset(cset, self.this.inventory, self.this.dir, conflict_handler, reverse) def cleanup(self): shutil.rmtree(self.dir) class MergeTest(unittest.TestCase): def test_change_name(self): """Test renames""" builder = MergeBuilder() builder.add_file("1", "0", "name1", "hello1", 0755) builder.change_name("1", other="name2") builder.add_file("2", "0", "name3", "hello2", 0755) builder.change_name("2", base="name4") builder.add_file("3", "0", "name5", "hello3", 0755) builder.change_name("3", this="name6") cset = builder.merge_changeset() assert(cset.entries["2"].is_boring()) assert(cset.entries["1"].name == "name1") assert(cset.entries["1"].new_name == "name2") assert(cset.entries["3"].is_boring()) for tree in (builder.this, builder.other, builder.base): assert(tree.dir != builder.dir and tree.dir.startswith(builder.dir)) for path in tree.inventory.itervalues(): fullpath = tree.abs_path(path) assert(fullpath.startswith(tree.dir)) assert(not path.startswith(tree.dir)) assert os.path.exists(fullpath) builder.apply_changeset(cset) builder.cleanup() builder = MergeBuilder() builder.add_file("1", "0", "name1", "hello1", 0644) builder.change_name("1", other="name2", this="name3") self.assertRaises(changeset.RenameConflict, builder.merge_changeset) builder.cleanup() def test_file_moves(self): """Test moves""" builder = MergeBuilder() builder.add_dir("1", "0", "dir1", 0755) builder.add_dir("2", "0", "dir2", 0755) builder.add_file("3", "1", "file1", "hello1", 0644) builder.add_file("4", "1", "file2", "hello2", 0644) builder.add_file("5", "1", "file3", "hello3", 0644) builder.change_parent("3", other="2") assert(Inventory(builder.other.inventory).get_parent("3") == "2") builder.change_parent("4", this="2") assert(Inventory(builder.this.inventory).get_parent("4") == "2") builder.change_parent("5", base="2") assert(Inventory(builder.base.inventory).get_parent("5") == "2") cset = builder.merge_changeset() for id in ("1", "2", "4", "5"): assert(cset.entries[id].is_boring()) assert(cset.entries["3"].parent == "1") assert(cset.entries["3"].new_parent == "2") builder.apply_changeset(cset) builder.cleanup() builder = MergeBuilder() builder.add_dir("1", "0", "dir1", 0755) builder.add_dir("2", "0", "dir2", 0755) builder.add_dir("3", "0", "dir3", 0755) builder.add_file("4", "1", "file1", "hello1", 0644) builder.change_parent("4", other="2", this="3") self.assertRaises(changeset.MoveConflict, builder.merge_changeset) builder.cleanup() def test_contents_merge(self): """Test diff3 merging""" builder = MergeBuilder() builder.add_file("1", "0", "name1", "text1", 0755) builder.change_contents("1", other="text4") builder.add_file("2", "0", "name3", "text2", 0655) builder.change_contents("2", base="text5") builder.add_file("3", "0", "name5", "text3", 0744) builder.change_contents("3", this="text6") cset = builder.merge_changeset() assert(cset.entries["1"].contents_change is not None) assert(isinstance(cset.entries["1"].contents_change, changeset.Diff3Merge)) assert(isinstance(cset.entries["2"].contents_change, changeset.Diff3Merge)) assert(cset.entries["3"].is_boring()) builder.apply_changeset(cset) assert(file(builder.this.full_path("1"), "rb").read() == "text4" ) assert(file(builder.this.full_path("2"), "rb").read() == "text2" ) assert(os.stat(builder.this.full_path("1")).st_mode &0777 == 0755) assert(os.stat(builder.this.full_path("2")).st_mode &0777 == 0655) assert(os.stat(builder.this.full_path("3")).st_mode &0777 == 0744) builder.cleanup() builder = MergeBuilder() builder.add_file("1", "0", "name1", "text1", 0755) builder.change_contents("1", other="text4", this="text3") cset = builder.merge_changeset() self.assertRaises(changeset.MergeConflict, builder.apply_changeset, cset) builder.cleanup() def test_perms_merge(self): builder = MergeBuilder() builder.add_file("1", "0", "name1", "text1", 0755) builder.change_perms("1", other=0655) builder.add_file("2", "0", "name2", "text2", 0755) builder.change_perms("2", base=0655) builder.add_file("3", "0", "name3", "text3", 0755) builder.change_perms("3", this=0655) cset = builder.merge_changeset() assert(cset.entries["1"].metadata_change is not None) assert(isinstance(cset.entries["1"].metadata_change, PermissionsMerge)) assert(isinstance(cset.entries["2"].metadata_change, PermissionsMerge)) assert(cset.entries["3"].is_boring()) builder.apply_changeset(cset) assert(os.stat(builder.this.full_path("1")).st_mode &0777 == 0655) assert(os.stat(builder.this.full_path("2")).st_mode &0777 == 0755) assert(os.stat(builder.this.full_path("3")).st_mode &0777 == 0655) builder.cleanup(); builder = MergeBuilder() builder.add_file("1", "0", "name1", "text1", 0755) builder.change_perms("1", other=0655, base=0555) cset = builder.merge_changeset() self.assertRaises(changeset.MergePermissionConflict, builder.apply_changeset, cset) builder.cleanup() def test(): changeset_suite = unittest.makeSuite(MergeTest, 'test_') runner = unittest.TextTestRunner() runner.run(changeset_suite) if __name__ == "__main__": test() commit refs/heads/master mark :623 committer Martin Pool 1118033253 +1000 data 61 - fix invocation of testbzr when giving explicit bzr location from :622 M 644 inline testbzr data 12198 #! /usr/bin/python # -*- coding: utf-8 -*- # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. usage: testbzr [-p PYTHON] [BZR] By default this tests the copy of bzr found in the same directory as testbzr, or the first one found on the $PATH. A copy of bzr may be given on the command line to override this, for example when applying a new test suite to an old copy of bzr or vice versa. testbzr normally invokes bzr using the same version of python as it would normally use to run -- that is, the system default python, unless that is older than 2.3. The -p option allows specification of a different Python interpreter, such as when testing that bzr still works on python2.3. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" OVERRIDE_PYTHON = None LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = BZRPATH if OVERRIDE_PYTHON: cmd.insert(0, OVERRIDE_PYTHON) logfile.write('$ %r\n' % cmd) return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) start_dir = os.getcwd() logfile = open(LOGFILENAME, 'wt', buffering=1) try: from getopt import getopt opts, args = getopt(sys.argv[1:], 'p:') for option, value in opts: if option == '-p': OVERRIDE_PYTHON = value mypath = os.path.abspath(sys.argv[0]) print '%-30s %s' % ('running tests from', mypath) global BZRPATH if args: BZRPATH = args[0] else: BZRPATH = os.path.join(os.path.split(mypath)[0], 'bzr') print '%-30s %s' % ('against bzr', BZRPATH) print '%-30s %s' % ('in directory', os.getcwd()) print '%-30s %s' % ('with python', (OVERRIDE_PYTHON or '(default)')) print print backtick([BZRPATH, 'version']) runcmd(['mkdir', TESTDIR]) cd(TESTDIR) test_root = os.getcwd() progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("internal tests") runcmd("bzr selftest") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) runcmd("bzr diff --message foo", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == 'unknown:\n test.txt\n' out = backtick("bzr status --all") assert out == "unknown:\n test.txt\n" out = backtick("bzr status test.txt --all") assert out == "unknown:\n test.txt\n" f = file('test2.txt', 'wt') f.write('goodbye cruel world...\n') f.close() out = backtick("bzr status test.txt") assert out == "unknown:\n test.txt\n" out = backtick("bzr status") assert out == ("unknown:\n" " test.txt\n" " test2.txt\n") os.unlink('test2.txt') progress("command aliases") out = backtick("bzr st --all") assert out == ("unknown:\n" " test.txt\n") out = backtick("bzr stat") assert out == ("unknown:\n" " test.txt\n") progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == ("added:\n" " test.txt\n") progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == os.path.join("sub2", "hello.txt\n") assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') assert backtick('bzr relpath sub2/hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') runcmd('bzr log') runcmd('bzr log -v') progress("file with spaces in name") mkdir('sub directory') file('sub directory/file with spaces ', 'wt').write('see how this works\n') runcmd('bzr add .') runcmd('bzr diff') runcmd('bzr commit -m add-spaces') runcmd('bzr check') runcmd('bzr log') runcmd('bzr log --forward') runcmd('bzr info') cd('..') cd('..') progress('status after remove') mkdir('status-after-remove') # see mail from William Dodé, 2005-05-25 # $ bzr init; touch a; bzr add a; bzr commit -m "add a" # * looking for changes... # added a # * commited r1 # $ bzr remove a # $ bzr status # bzr: local variable 'kind' referenced before assignment # at /vrac/python/bazaar-ng/bzrlib/diff.py:286 in compare_trees() # see ~/.bzr.log for debug information cd('status-after-remove') runcmd('bzr init') file('a', 'w').write('foo') runcmd('bzr add a') runcmd(['bzr', 'commit', '-m', 'add a']) runcmd('bzr remove a') runcmd('bzr status') cd('..') progress('ignore patterns') mkdir('ignorebranch') cd('ignorebranch') runcmd('bzr init') assert backtick('bzr unknowns') == '' file('foo.tmp', 'wt').write('tmp files are ignored') assert backtick('bzr unknowns') == '' file('foo.c', 'wt').write('int main() {}') assert backtick('bzr unknowns') == 'foo.c\n' runcmd('bzr add foo.c') assert backtick('bzr unknowns') == '' # 'ignore' works when creating the .bzignore file file('foo.blah', 'wt').write('blah') assert backtick('bzr unknowns') == 'foo.blah\n' runcmd('bzr ignore *.blah') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\n' # 'ignore' works when then .bzrignore file already exists file('garh', 'wt').write('garh') assert backtick('bzr unknowns') == 'garh\n' runcmd('bzr ignore garh') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\ngarh\n' cd('..') progress("recursive and non-recursive add") mkdir('no-recurse') cd('no-recurse') runcmd('bzr init') mkdir('foo') fp = os.path.join('foo', 'test.txt') f = file(fp, 'w') f.write('hello!\n') f.close() runcmd('bzr add --no-recurse foo') runcmd('bzr file-id foo') runcmd('bzr file-id ' + fp, 1) # not versioned yet runcmd('bzr commit -m add-dir-only') runcmd('bzr file-id ' + fp, 1) # still not versioned runcmd('bzr add foo') runcmd('bzr file-id ' + fp) runcmd('bzr commit -m add-sub-file') cd('..') progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) logfile.close() sys.stdout.writelines(file(os.path.join(start_dir, LOGFILENAME), 'rt').readlines()[-50:]) sys.exit(1) commit refs/heads/master mark :624 committer Martin Pool 1118034801 +1000 data 91 - make sure bzr is always explicitly invoked through python in case it's not executable from :623 M 644 inline testbzr data 12327 #! /usr/bin/python # -*- coding: utf-8 -*- # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. usage: testbzr [-p PYTHON] [BZR] By default this tests the copy of bzr found in the same directory as testbzr, or the first one found on the $PATH. A copy of bzr may be given on the command line to override this, for example when applying a new test suite to an old copy of bzr or vice versa. testbzr normally invokes bzr using the same version of python as it would normally use to run -- that is, the system default python, unless that is older than 2.3. The -p option allows specification of a different Python interpreter, such as when testing that bzr still works on python2.3. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" # we always invoke bzr as 'python bzr' (or e.g. 'python2.3 bzr') # partly so as to cope if the bzr binary is not marked executable OVERRIDE_PYTHON = 'python' LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = BZRPATH if OVERRIDE_PYTHON: cmd.insert(0, OVERRIDE_PYTHON) logfile.write('$ %r\n' % cmd) return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) start_dir = os.getcwd() logfile = open(LOGFILENAME, 'wt', buffering=1) try: from getopt import getopt opts, args = getopt(sys.argv[1:], 'p:') for option, value in opts: if option == '-p': OVERRIDE_PYTHON = value mypath = os.path.abspath(sys.argv[0]) print '%-30s %s' % ('running tests from', mypath) global BZRPATH if args: BZRPATH = args[0] else: BZRPATH = os.path.join(os.path.split(mypath)[0], 'bzr') print '%-30s %s' % ('against bzr', BZRPATH) print '%-30s %s' % ('in directory', os.getcwd()) print '%-30s %s' % ('with python', (OVERRIDE_PYTHON or '(default)')) print print backtick('bzr version') runcmd(['mkdir', TESTDIR]) cd(TESTDIR) test_root = os.getcwd() progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("internal tests") runcmd("bzr selftest") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) runcmd("bzr diff --message foo", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == 'unknown:\n test.txt\n' out = backtick("bzr status --all") assert out == "unknown:\n test.txt\n" out = backtick("bzr status test.txt --all") assert out == "unknown:\n test.txt\n" f = file('test2.txt', 'wt') f.write('goodbye cruel world...\n') f.close() out = backtick("bzr status test.txt") assert out == "unknown:\n test.txt\n" out = backtick("bzr status") assert out == ("unknown:\n" " test.txt\n" " test2.txt\n") os.unlink('test2.txt') progress("command aliases") out = backtick("bzr st --all") assert out == ("unknown:\n" " test.txt\n") out = backtick("bzr stat") assert out == ("unknown:\n" " test.txt\n") progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == ("added:\n" " test.txt\n") progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == os.path.join("sub2", "hello.txt\n") assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') assert backtick('bzr relpath sub2/hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') runcmd('bzr log') runcmd('bzr log -v') progress("file with spaces in name") mkdir('sub directory') file('sub directory/file with spaces ', 'wt').write('see how this works\n') runcmd('bzr add .') runcmd('bzr diff') runcmd('bzr commit -m add-spaces') runcmd('bzr check') runcmd('bzr log') runcmd('bzr log --forward') runcmd('bzr info') cd('..') cd('..') progress('status after remove') mkdir('status-after-remove') # see mail from William Dodé, 2005-05-25 # $ bzr init; touch a; bzr add a; bzr commit -m "add a" # * looking for changes... # added a # * commited r1 # $ bzr remove a # $ bzr status # bzr: local variable 'kind' referenced before assignment # at /vrac/python/bazaar-ng/bzrlib/diff.py:286 in compare_trees() # see ~/.bzr.log for debug information cd('status-after-remove') runcmd('bzr init') file('a', 'w').write('foo') runcmd('bzr add a') runcmd(['bzr', 'commit', '-m', 'add a']) runcmd('bzr remove a') runcmd('bzr status') cd('..') progress('ignore patterns') mkdir('ignorebranch') cd('ignorebranch') runcmd('bzr init') assert backtick('bzr unknowns') == '' file('foo.tmp', 'wt').write('tmp files are ignored') assert backtick('bzr unknowns') == '' file('foo.c', 'wt').write('int main() {}') assert backtick('bzr unknowns') == 'foo.c\n' runcmd('bzr add foo.c') assert backtick('bzr unknowns') == '' # 'ignore' works when creating the .bzignore file file('foo.blah', 'wt').write('blah') assert backtick('bzr unknowns') == 'foo.blah\n' runcmd('bzr ignore *.blah') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\n' # 'ignore' works when then .bzrignore file already exists file('garh', 'wt').write('garh') assert backtick('bzr unknowns') == 'garh\n' runcmd('bzr ignore garh') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\ngarh\n' cd('..') progress("recursive and non-recursive add") mkdir('no-recurse') cd('no-recurse') runcmd('bzr init') mkdir('foo') fp = os.path.join('foo', 'test.txt') f = file(fp, 'w') f.write('hello!\n') f.close() runcmd('bzr add --no-recurse foo') runcmd('bzr file-id foo') runcmd('bzr file-id ' + fp, 1) # not versioned yet runcmd('bzr commit -m add-dir-only') runcmd('bzr file-id ' + fp, 1) # still not versioned runcmd('bzr add foo') runcmd('bzr file-id ' + fp) runcmd('bzr commit -m add-sub-file') cd('..') progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) logfile.close() sys.stdout.writelines(file(os.path.join(start_dir, LOGFILENAME), 'rt').readlines()[-50:]) sys.exit(1) commit refs/heads/master mark :625 committer Martin Pool 1118037319 +1000 data 43 - fix permissions on exported tar/zip files from :624 M 644 inline contrib/create_bzr_rollup.py data 5639 #!/usr/bin/env python """\ This script runs after rsyncing bzr. It checks the bzr version, and sees if there is a tarball and zipfile that exist with that version. If not, it creates them. """ import os, sys, tempfile def sync(remote, local, verbose=False): """Do the actual synchronization """ if verbose: status = os.system('rsync -av --delete "%s" "%s"' % (remote, local)) else: status = os.system('rsync -a --delete "%s" "%s"' % (remote, local)) return status==0 def create_tar_gz(local_dir, output_dir=None, verbose=False): import tarfile, bzrlib out_name = os.path.basename(local_dir) + '-' + str(bzrlib.Branch(local_dir).revno()) final_path = os.path.join(output_dir, out_name + '.tar.gz') if os.path.exists(final_path): if verbose: print 'Output file already exists: %r' % final_path return fn, tmp_path=tempfile.mkstemp(suffix='.tar', prefix=out_name, dir=output_dir) os.close(fn) try: if verbose: print 'Creating %r (%r)' % (final_path, tmp_path) tar = tarfile.TarFile(name=tmp_path, mode='w') tar.add(local_dir, arcname=out_name, recursive=True) tar.close() if verbose: print 'Compressing...' if os.system('gzip "%s"' % tmp_path) != 0: raise ValueError('Failed to compress') tmp_path += '.gz' os.chmod(tmp_path, 0644) os.rename(tmp_path, final_path) except: os.remove(tmp_path) raise def create_tar_bz2(local_dir, output_dir=None, verbose=False): import tarfile, bzrlib out_name = os.path.basename(local_dir) + '-' + str(bzrlib.Branch(local_dir).revno()) final_path = os.path.join(output_dir, out_name + '.tar.bz2') if os.path.exists(final_path): if verbose: print 'Output file already exists: %r' % final_path return fn, tmp_path=tempfile.mkstemp(suffix='.tar', prefix=out_name, dir=output_dir) os.close(fn) try: if verbose: print 'Creating %r (%r)' % (final_path, tmp_path) tar = tarfile.TarFile(name=tmp_path, mode='w') tar.add(local_dir, arcname=out_name, recursive=True) tar.close() if verbose: print 'Compressing...' if os.system('bzip2 "%s"' % tmp_path) != 0: raise ValueError('Failed to compress') tmp_path += '.bz2' os.chmod(tmp_path, 0644) os.rename(tmp_path, final_path) except: os.remove(tmp_path) raise def create_zip(local_dir, output_dir=None, verbose=False): import zipfile, bzrlib out_name = os.path.basename(local_dir) + '-' + str(bzrlib.Branch(local_dir).revno()) final_path = os.path.join(output_dir, out_name + '.zip') if os.path.exists(final_path): if verbose: print 'Output file already exists: %r' % final_path return fn, tmp_path=tempfile.mkstemp(suffix='.zip', prefix=out_name, dir=output_dir) os.close(fn) try: if verbose: print 'Creating %r (%r)' % (final_path, tmp_path) zip = zipfile.ZipFile(file=tmp_path, mode='w') try: for root, dirs, files in os.walk(local_dir): for f in files: path = os.path.join(root, f) arcname = os.path.join(out_name, path[len(local_dir)+1:]) zip.write(path, arcname=arcname) finally: zip.close() os.chmod(tmp_path, 0644) os.rename(tmp_path, final_path) except: os.remove(tmp_path) raise def get_local_dir(remote, local): """This returns the full path to the local directory where the files are kept. rsync has the trick that if the source directory ends in a '/' then the file will be copied *into* the target. If it does not end in a slash, then the directory will be added into the target. """ if remote[-1:] == '/': return local # rsync paths are typically user@host:path/to/something # the reason for the split(':') is in case path doesn't contain a slash extra = remote.split(':')[-1].split('/')[-1] return os.path.join(local, extra) def get_output_dir(output, local): if output: return output return os.path.dirname(os.path.realpath(local)) def main(args): import optparse p = optparse.OptionParser(usage='%prog [options] [remote] [local]' '\n rsync the remote repository to the local directory' '\n if remote is not given, it defaults to "bazaar-ng.org::bazaar-ng/bzr/bzr.dev"' '\n if local is not given it defaults to "."') p.add_option('--verbose', action='store_true' , help="Describe the process") p.add_option('--no-tar-gz', action='store_false', dest='create_tar_gz', default=True , help="Don't create a gzip compressed tarfile.") p.add_option('--no-tar-bz2', action='store_false', dest='create_tar_bz2', default=True , help="Don't create a bzip2 compressed tarfile.") p.add_option('--no-zip', action='store_false', dest='create_zip', default=True , help="Don't create a zipfile.") p.add_option('--output-dir', default=None , help="Set the output location, default is just above the final local directory.") (opts, args) = p.parse_args(args) if len(args) < 1: remote = 'bazaar-ng.org::bazaar-ng/bzr/bzr.dev' else: remote = args[0] if len(args) < 2: local = '.' else: local = args[1] if len(args) > 2: print 'Invalid number of arguments, see --help for details.' if not sync(remote, local, verbose=opts.verbose): if opts.verbose: print '** rsync failed' return 1 # Now we have the new update local_dir = get_local_dir(remote, local) output_dir = get_output_dir(opts.output_dir, local_dir) if opts.create_tar_gz: create_tar_gz(local_dir, output_dir=output_dir, verbose=opts.verbose) if opts.create_tar_bz2: create_tar_bz2(local_dir, output_dir=output_dir, verbose=opts.verbose) if opts.create_zip: create_zip(local_dir, output_dir=output_dir, verbose=opts.verbose) return 0 if __name__ == '__main__': sys.exit(main(sys.argv[1:])) commit refs/heads/master mark :626 committer Martin Pool 1118057840 +1000 data 63 - add Store.copy_multi for use in pulling changes into a branch from :625 M 644 inline bzrlib/store.py data 5799 # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Stores are the main data-storage mechanism for Bazaar-NG. A store is a simple write-once container indexed by a universally unique ID, which is typically the SHA-1 of the content.""" __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import os, tempfile, types, osutils, gzip, errno from stat import ST_SIZE from StringIO import StringIO from trace import mutter ###################################################################### # stores class StoreError(Exception): pass class ImmutableStore(object): """Store that holds files indexed by unique names. Files can be added, but not modified once they are in. Typically the hash is used as the name, or something else known to be unique, such as a UUID. >>> st = ImmutableScratchStore() >>> st.add(StringIO('hello'), 'aa') >>> 'aa' in st True >>> 'foo' in st False You are not allowed to add an id that is already present. Entries can be retrieved as files, which may then be read. >>> st.add(StringIO('goodbye'), '123123') >>> st['123123'].read() 'goodbye' TODO: Atomic add by writing to a temporary file and renaming. TODO: Perhaps automatically transform to/from XML in a method? Would just need to tell the constructor what class to use... TODO: Even within a simple disk store like this, we could gzip the files. But since many are less than one disk block, that might not help a lot. """ def __init__(self, basedir): """ImmutableStore constructor.""" self._basedir = basedir def _path(self, id): assert '/' not in id return os.path.join(self._basedir, id) def __repr__(self): return "%s(%r)" % (self.__class__.__name__, self._basedir) def add(self, f, fileid, compressed=True): """Add contents of a file into the store. f -- An open file, or file-like object.""" # FIXME: Only works on smallish files # TODO: Can be optimized by copying at the same time as # computing the sum. mutter("add store entry %r" % (fileid)) if isinstance(f, types.StringTypes): content = f else: content = f.read() p = self._path(fileid) if os.access(p, os.F_OK) or os.access(p + '.gz', os.F_OK): bailout("store %r already contains id %r" % (self._basedir, fileid)) if compressed: f = gzip.GzipFile(p + '.gz', 'wb') os.chmod(p + '.gz', 0444) else: f = file(p, 'wb') os.chmod(p, 0444) f.write(content) f.close() def copy_multi(self, other, ids): """Copy texts for ids from other into self. If an id is present in self, it is skipped. A count of copied ids is returned, which may be less than len(ids). """ count = 0 for id in ids: if id in self: continue self.add(other[id], id) count += 1 return count def __contains__(self, fileid): """""" p = self._path(fileid) return (os.access(p, os.R_OK) or os.access(p + '.gz', os.R_OK)) # TODO: Guard against the same thing being stored twice, compressed and uncompresse def __iter__(self): for f in os.listdir(self._basedir): if f[-3:] == '.gz': # TODO: case-insensitive? yield f[:-3] else: yield f def __len__(self): return len(os.listdir(self._basedir)) def __getitem__(self, fileid): """Returns a file reading from a particular entry.""" p = self._path(fileid) try: return gzip.GzipFile(p + '.gz', 'rb') except IOError, e: if e.errno == errno.ENOENT: return file(p, 'rb') else: raise e def total_size(self): """Return (count, bytes) This is the (compressed) size stored on disk, not the size of the content.""" total = 0 count = 0 for fid in self: count += 1 p = self._path(fid) try: total += os.stat(p)[ST_SIZE] except OSError: total += os.stat(p + '.gz')[ST_SIZE] return count, total class ImmutableScratchStore(ImmutableStore): """Self-destructing test subclass of ImmutableStore. The Store only exists for the lifetime of the Python object. Obviously you should not put anything precious in it. """ def __init__(self): ImmutableStore.__init__(self, tempfile.mkdtemp()) def __del__(self): for f in os.listdir(self._basedir): fpath = os.path.join(self._basedir, f) # needed on windows, and maybe some other filesystems os.chmod(fpath, 0600) os.remove(fpath) os.rmdir(self._basedir) mutter("%r destroyed" % self) commit refs/heads/master mark :627 committer Martin Pool 1118058587 +1000 data 4 todo from :626 M 644 inline TODO data 12117 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * ``bzr log DIR`` should give changes to any files within DIR. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. Perhaps ~/.bzr/http-cache. Baz has a fairly simple cache under ~/.arch-cache, containing revision information encoded almost as a bunch of archives. Perhaps we could simply store full paths. * Maybe also store directories in the statcache so that we can quickly identify that they still exist. * Diff should show timestamps; for files from the working directory we can use the file itself; for files from a revision we should use the commit time of the revision. * Perhaps split command infrastructure from the actual command definitions. * Cleaner support for negative boolean options like --no-recurse. * Statcache should possibly map all file paths to / separators Medium things ------------- * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. We should be able to just get the id for the selected files, look up their location and diff just those files. No need to traverse the entire inventories. * ``bzr status DIR`` or ``bzr diff DIR`` should report on all changes under that directory. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from ElementTree to an object when it is read in, but rather wait until the program actually wants to know about that node. * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. - Selected-file commit - Impossible selected-file commit: adding things in non-versioned directories, crossing renames, etc. * Write a reproducible benchmark, perhaps importing various kernel versions. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Paranoid mode where we never trust SHA-1 matches. * Don't commit if there are no changes unless forced. * --dry-run mode for commit? (Or maybe just run with check-command=false?) * Generally, be a bit more verbose unless --silent is specified. * Function that finds all changes to files under a given directory; perhaps log should use this if a directory is given. * XML attributes might have trouble with filenames containing \n and \r. Do we really want to support this? I think perhaps not. * Remember execute bits, so that exports will work OK. * Unify smart_add and plain Branch.add(); perhaps smart_add should just build a list of files to add and pass that to the regular add function. * Function to list a directory, saying in which revision each file was last modified. Useful for web and gui interfaces, and slow to compute one file at a time. * unittest is standard, but the results are kind of ugly; would be nice to make it cleaner. * Check locking is correct during merge-related operations. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. Possibly this should be done by splitting the commit function into several parts (under a single interface). It is already rather large. Decomposition: - find tree modifications and prepare in-memory inventory - export that inventory to a temporary directory - run the test in that temporary directory - if that succeeded, continue to actually finish the commit What should be done with the text of modified files while this is underway? I don't think we want to count on holding them in memory and we can't trust the working files to stay in one place so I suppose we need to move them into the text store, or otherwise into a temporary directory. If the commit does not actually complete, we would rather the content was not left behind in the stores. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :628 committer Martin Pool 1118058809 +1000 data 39 - merge aaron's updated merge/pull code from :627 M 644 inline bzrlib/branch.py data 34506 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def _relpath(base, path): """Return path relative to base, or raise exception. The path may be either an absolute path or a path relative to the current working directory. Lifted out of Branch.relpath for ease of testing. os.path.commonprefix (python2.4) has a bad bug that it works just on string prefixes, assuming that '/u' is a prefix of '/u2'. This avoids that problem.""" rp = os.path.abspath(path) s = [] head = rp while len(head) >= len(base): if head == base: break head, tail = os.path.split(head) if tail: s.insert(0, tail) else: from errors import NotBranchError raise NotBranchError("path %r is not within branch %r" % (rp, base)) return os.sep.join(s) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head class DivergedBranches(Exception): def __init__(self, branch1, branch2): self.branch1 = branch1 self.branch2 = branch2 Exception.__init__(self, "These branches have diverged.") ###################################################################### # branch objects class Branch(object): """Branch holding a history of revisions. base Base directory of the branch. _lock_mode None, or 'r' or 'w' _lock_count If _lock_mode is true, a positive count of the number of times the lock has been taken. _lock Lock object from bzrlib.lock. """ base = None _lock_mode = None _lock_count = None _lock = None def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): from errors import NotBranchError raise NotBranchError("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def __del__(self): if self._lock_mode or self._lock: from warnings import warn warn("branch %r was not explicitly unlocked" % self) self._lock.unlock() def lock_write(self): if self._lock_mode: if self._lock_mode != 'w': from errors import LockError raise LockError("can't upgrade to a write lock from %r" % self._lock_mode) self._lock_count += 1 else: from bzrlib.lock import WriteLock self._lock = WriteLock(self.controlfilename('branch-lock')) self._lock_mode = 'w' self._lock_count = 1 def lock_read(self): if self._lock_mode: assert self._lock_mode in ('r', 'w'), \ "invalid lock mode %r" % self._lock_mode self._lock_count += 1 else: from bzrlib.lock import ReadLock self._lock = ReadLock(self.controlfilename('branch-lock')) self._lock_mode = 'r' self._lock_count = 1 def unlock(self): if not self._lock_mode: from errors import LockError raise LockError('branch %r is not locked' % (self)) if self._lock_count > 1: self._lock_count -= 1 else: self._lock.unlock() self._lock = None self._lock_mode = self._lock_count = None def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" return _relpath(self.base, path) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: raise BzrError('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. self.lock_read() try: inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv finally: self.unlock() def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. files List of paths to add, relative to the base of the tree. ids If set, use these instead of automatically generated ids. Must be the same length as the list of files, but may contain None for ids that are to be autogenerated. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) self.lock_write() try: inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): raise BzrError("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: raise BzrError("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) finally: self.unlock() def print_file(self, file, revno): """Print `file` to stdout.""" self.lock_read() try: tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: raise BzrError("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) finally: self.unlock() def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] self.lock_write() try: tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: raise BzrError("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) finally: self.unlock() # FIXME: this doesn't need to be a branch method def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self.lock_read() try: return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] finally: self.unlock() def common_ancestor(self, other, self_revno=None, other_revno=None): """ >>> import commit >>> sb = ScratchBranch(files=['foo', 'foo~']) >>> sb.common_ancestor(sb) == (None, None) True >>> commit.commit(sb, "Committing first revision", verbose=False) >>> sb.common_ancestor(sb)[0] 1 >>> clone = sb.clone() >>> commit.commit(sb, "Committing second revision", verbose=False) >>> sb.common_ancestor(sb)[0] 2 >>> sb.common_ancestor(clone)[0] 1 >>> commit.commit(clone, "Committing divergent second revision", ... verbose=False) >>> sb.common_ancestor(clone)[0] 1 >>> sb.common_ancestor(clone) == clone.common_ancestor(sb) True >>> sb.common_ancestor(sb) != clone.common_ancestor(clone) True >>> clone2 = sb.clone() >>> sb.common_ancestor(clone2)[0] 2 >>> sb.common_ancestor(clone2, self_revno=1)[0] 1 >>> sb.common_ancestor(clone2, other_revno=1)[0] 1 """ my_history = self.revision_history() other_history = other.revision_history() if self_revno is None: self_revno = len(my_history) if other_revno is None: other_revno = len(other_history) indices = range(min((self_revno, other_revno))) indices.reverse() for r in indices: if my_history[r] == other_history[r]: return r+1, my_history[r] return None, None def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def missing_revisions(self, other): """ If self and other have not diverged, return a list of the revisions present in other, but missing from self. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch() >>> br2 = ScratchBranch() >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [u'REVISION-ID-1'] >>> br2.missing_revisions(br1) [] >>> commit(br1, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-2A") >>> br1.missing_revisions(br2) [u'REVISION-ID-2A'] >>> commit(br1, "lala!", rev_id="REVISION-ID-2B") >>> br1.missing_revisions(br2) Traceback (most recent call last): DivergedBranches: These branches have diverged. """ self_history = self.revision_history() self_len = len(self_history) other_history = other.revision_history() other_len = len(other_history) common_index = min(self_len, other_len) -1 if common_index >= 0 and \ self_history[common_index] != other_history[common_index]: raise DivergedBranches(self, other) if self_len < other_len: return other_history[self_len:] return [] def update_revisions(self, other): """If self and other have not diverged, ensure self has all the revisions in other >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch(files=['foo', 'bar']) >>> br1.add('foo') >>> br1.add('bar') >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False) >>> br2 = ScratchBranch() >>> br2.update_revisions(br1) Added 2 texts. Added 1 inventories. Added 1 revisions. >>> br2.revision_history() [u'REVISION-ID-1'] >>> br2.update_revisions(br1) Added 0 texts. Added 0 inventories. Added 0 revisions. >>> br1.text_store.total_size() == br2.text_store.total_size() True """ revision_ids = self.missing_revisions(other) revisions = [other.get_revision(f) for f in revision_ids] needed_texts = sets.Set() for rev in revisions: inv = other.get_inventory(str(rev.inventory_id)) for key, entry in inv.iter_entries(): if entry.text_id is None: continue if entry.text_id not in self.text_store: needed_texts.add(entry.text_id) count = self.text_store.copy_multi(other.text_store, needed_texts) print "Added %d texts." % count inventory_ids = [ f.inventory_id for f in revisions ] count = self.inventory_store.copy_multi(other.inventory_store, inventory_ids) print "Added %d inventories." % count revision_ids = [ f.revision_id for f in revisions] count = self.revision_store.copy_multi(other.revision_store, revision_ids) for revision_id in revision_ids: self.append_revision(revision_id) print "Added %d revisions." % count def commit(self, *args, **kw): """Deprecated""" from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self.lock_write() try: tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): raise BzrError("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): raise BzrError("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: raise BzrError("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): raise BzrError("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': raise BzrError("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self.lock_write() try: ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): raise BzrError("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): raise BzrError("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': raise BzrError("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): raise BzrError("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): raise BzrError("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: raise BzrError("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): raise BzrError("destination %r already exists" % dest_path) if f_id in to_idpath: raise BzrError("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[], base=None): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ init = False if base is None: base = tempfile.mkdtemp() init = True Branch.__init__(self, base, init=init) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def clone(self): """ >>> orig = ScratchBranch(files=["file1", "file2"]) >>> clone = orig.clone() >>> os.path.samefile(orig.base, clone.base) False >>> os.path.isfile(os.path.join(clone.base, "file1")) True """ base = tempfile.mkdtemp() os.rmdir(base) shutil.copytree(self.base, base, symlinks=True) return ScratchBranch(base=base) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: if self.base: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/commands.py data 43070 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import errno br_to = Branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if errno == errno.ENOENT: raise if location is None: location = stored_loc if location is None: raise BzrCommandError("No pull location known or specified.") from branch import find_branch, DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged. Try merge.") merge(('.', -1), ('.', old_revno)) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. """ takes_args = ['from_location', 'to_location?'] def run(self, from_location, to_location=None): import errno from bzrlib.merge import merge if to_location is None: to_location = os.path.basename(from_location) # FIXME: If there's a trailing slash, keep removing them # until we find the right bit try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) from branch import find_branch, DivergedBranches try: br_from = find_branch(from_location) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not exist.' % to_location) else: raise from_location = pull_loc(br_from) br_to.update_revisions(br_from) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False) br_to.controlfile("x-pull", "wb").write(from_location + "\n") def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest if selftest(): return 0 else: return 1 class cmd_version(Command): """Show version of bzr""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/merge.py data 10464 from merge_core import merge_flex from changeset import generate_changeset, ExceptionConflictHandler from changeset import Inventory from bzrlib import find_branch import bzrlib.osutils from bzrlib.errors import BzrCommandError from bzrlib.diff import compare_trees from trace import mutter, warning import os.path import tempfile import shutil import errno class UnrelatedBranches(BzrCommandError): def __init__(self): msg = "Branches have no common ancestor, and no base revision"\ " specified." BzrCommandError.__init__(self, msg) class MergeConflictHandler(ExceptionConflictHandler): """Handle conflicts encountered while merging""" def __init__(self, dir, ignore_zero=False): ExceptionConflictHandler.__init__(self, dir) self.conflicts = 0 self.ignore_zero = ignore_zero def copy(self, source, dest): """Copy the text and mode of a file :param source: The path of the file to copy :param dest: The distination file to create """ s_file = file(source, "rb") d_file = file(dest, "wb") for line in s_file: d_file.write(line) os.chmod(dest, 0777 & os.stat(source).st_mode) def add_suffix(self, name, suffix, last_new_name=None): """Rename a file to append a suffix. If the new name exists, the suffix is added repeatedly until a non-existant name is found :param name: The path of the file :param suffix: The suffix to append :param last_new_name: (used for recursive calls) the last name tried """ if last_new_name is None: last_new_name = name new_name = last_new_name+suffix try: os.rename(name, new_name) return new_name except OSError, e: if e.errno != errno.EEXIST and e.errno != errno.ENOTEMPTY: raise return self.add_suffix(name, suffix, last_new_name=new_name) def conflict(self, text): warning(text) self.conflicts += 1 def merge_conflict(self, new_file, this_path, base_path, other_path): """ Handle diff3 conflicts by producing a .THIS, .BASE and .OTHER. The main file will be a version with diff3 conflicts. :param new_file: Path to the output file with diff3 markers :param this_path: Path to the file text for the THIS tree :param base_path: Path to the file text for the BASE tree :param other_path: Path to the file text for the OTHER tree """ self.add_suffix(this_path, ".THIS") self.copy(base_path, this_path+".BASE") self.copy(other_path, this_path+".OTHER") os.rename(new_file, this_path) self.conflict("Diff3 conflict encountered in %s" % this_path) def target_exists(self, entry, target, old_path): """Handle the case when the target file or dir exists""" moved_path = self.add_suffix(target, ".moved") self.conflict("Moved existing %s to %s" % (target, moved_path)) def finalize(self): if not self.ignore_zero: print "%d conflicts encountered.\n" % self.conflicts class SourceFile(object): def __init__(self, path, id, present=None, isdir=None): self.path = path self.id = id self.present = present self.isdir = isdir self.interesting = True def __repr__(self): return "SourceFile(%s, %s)" % (self.path, self.id) def get_tree(treespec, temp_root, label): location, revno = treespec branch = find_branch(location) if revno is None: base_tree = branch.working_tree() elif revno == -1: base_tree = branch.basis_tree() else: base_tree = branch.revision_tree(branch.lookup_revision(revno)) temp_path = os.path.join(temp_root, label) os.mkdir(temp_path) return branch, MergeTree(base_tree, temp_path) def abspath(tree, file_id): path = tree.inventory.id2path(file_id) if path == "": return "./." return "./" + path def file_exists(tree, file_id): return tree.has_filename(tree.id2path(file_id)) def inventory_map(tree): inventory = {} for file_id in tree.inventory: if not file_exists(tree, file_id): continue path = abspath(tree, file_id) inventory[path] = SourceFile(path, file_id) return inventory class MergeTree(object): def __init__(self, tree, tempdir): object.__init__(self) if hasattr(tree, "basedir"): self.root = tree.basedir else: self.root = None self.inventory = inventory_map(tree) self.tree = tree self.tempdir = tempdir os.mkdir(os.path.join(self.tempdir, "texts")) self.cached = {} def readonly_path(self, id): if self.root is not None: return self.tree.abspath(self.tree.id2path(id)) else: if self.tree.inventory[id].kind in ("directory", "root_directory"): return self.tempdir if not self.cached.has_key(id): path = os.path.join(self.tempdir, "texts", id) outfile = file(path, "wb") outfile.write(self.tree.get_file(id).read()) assert(os.path.exists(path)) self.cached[id] = path return self.cached[id] def merge(other_revision, base_revision, check_clean=True, ignore_zero=False, this_dir=None): """Merge changes into a tree. base_revision Base for three-way merge. other_revision Other revision for three-way merge. this_dir Directory to merge changes into; '.' by default. check_clean If true, this_dir must have no uncommitted changes before the merge begins. """ tempdir = tempfile.mkdtemp(prefix="bzr-") try: if this_dir is None: this_dir = '.' this_branch = find_branch(this_dir) if check_clean: changes = compare_trees(this_branch.working_tree(), this_branch.basis_tree(), False) if changes.has_changed(): raise BzrCommandError("Working tree has uncommitted changes.") other_branch, other_tree = get_tree(other_revision, tempdir, "other") if base_revision == [None, None]: if other_revision[1] == -1: o_revno = None else: o_revno = other_revision[1] base_revno = this_branch.common_ancestor(other_branch, other_revno=o_revno)[0] if base_revno is None: raise UnrelatedBranches() base_revision = ['.', base_revno] base_branch, base_tree = get_tree(base_revision, tempdir, "base") merge_inner(this_branch, other_tree, base_tree, tempdir, ignore_zero=ignore_zero) finally: shutil.rmtree(tempdir) def generate_cset_optimized(tree_a, tree_b, inventory_a, inventory_b): """Generate a changeset, using the text_id to mark really-changed files. This permits blazing comparisons when text_ids are present. It also disables metadata comparison for files with identical texts. """ for file_id in tree_a.tree.inventory: if file_id not in tree_b.tree.inventory: continue entry_a = tree_a.tree.inventory[file_id] entry_b = tree_b.tree.inventory[file_id] if (entry_a.kind, entry_b.kind) != ("file", "file"): continue if None in (entry_a.text_id, entry_b.text_id): continue if entry_a.text_id != entry_b.text_id: continue inventory_a[abspath(tree_a.tree, file_id)].interesting = False inventory_b[abspath(tree_b.tree, file_id)].interesting = False cset = generate_changeset(tree_a, tree_b, inventory_a, inventory_b) for entry in cset.entries.itervalues(): entry.metadata_change = None return cset def merge_inner(this_branch, other_tree, base_tree, tempdir, ignore_zero=False): this_tree = get_tree((this_branch.base, None), tempdir, "this")[1] def get_inventory(tree): return tree.inventory inv_changes = merge_flex(this_tree, base_tree, other_tree, generate_cset_optimized, get_inventory, MergeConflictHandler(base_tree.root, ignore_zero=ignore_zero)) adjust_ids = [] for id, path in inv_changes.iteritems(): if path is not None: if path == '.': path = '' else: assert path.startswith('./') path = path[2:] adjust_ids.append((path, id)) this_branch.set_inventory(regen_inventory(this_branch, this_tree.root, adjust_ids)) def regen_inventory(this_branch, root, new_entries): old_entries = this_branch.read_working_inventory() new_inventory = {} by_path = {} for file_id in old_entries: entry = old_entries[file_id] path = old_entries.id2path(file_id) new_inventory[file_id] = (path, file_id, entry.parent_id, entry.kind) by_path[path] = file_id deletions = 0 insertions = 0 new_path_list = [] for path, file_id in new_entries: if path is None: del new_inventory[file_id] deletions += 1 else: new_path_list.append((path, file_id)) if file_id not in old_entries: insertions += 1 # Ensure no file is added before its parent new_path_list.sort() for path, file_id in new_path_list: if path == '': parent = None else: parent = by_path[os.path.dirname(path)] kind = bzrlib.osutils.file_kind(os.path.join(root, path)) new_inventory[file_id] = (path, file_id, parent, kind) by_path[path] = file_id # Get a list in insertion order new_inventory_list = new_inventory.values() mutter ("""Inventory regeneration: old length: %i insertions: %i deletions: %i new_length: %i"""\ % (len(old_entries), insertions, deletions, len(new_inventory_list))) assert len(new_inventory_list) == len(old_entries) + insertions - deletions new_inventory_list.sort() return new_inventory_list M 644 inline testbzr data 12982 #! /usr/bin/python # -*- coding: utf-8 -*- # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. usage: testbzr [-p PYTHON] [BZR] By default this tests the copy of bzr found in the same directory as testbzr, or the first one found on the $PATH. A copy of bzr may be given on the command line to override this, for example when applying a new test suite to an old copy of bzr or vice versa. testbzr normally invokes bzr using the same version of python as it would normally use to run -- that is, the system default python, unless that is older than 2.3. The -p option allows specification of a different Python interpreter, such as when testing that bzr still works on python2.3. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" # we always invoke bzr as 'python bzr' (or e.g. 'python2.3 bzr') # partly so as to cope if the bzr binary is not marked executable OVERRIDE_PYTHON = 'python' LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = BZRPATH if OVERRIDE_PYTHON: cmd.insert(0, OVERRIDE_PYTHON) logfile.write('$ %r\n' % cmd) return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) start_dir = os.getcwd() logfile = open(LOGFILENAME, 'wt', buffering=1) try: from getopt import getopt opts, args = getopt(sys.argv[1:], 'p:') for option, value in opts: if option == '-p': OVERRIDE_PYTHON = value mypath = os.path.abspath(sys.argv[0]) print '%-30s %s' % ('running tests from', mypath) global BZRPATH if args: BZRPATH = args[0] else: BZRPATH = os.path.join(os.path.split(mypath)[0], 'bzr') print '%-30s %s' % ('against bzr', BZRPATH) print '%-30s %s' % ('in directory', os.getcwd()) print '%-30s %s' % ('with python', (OVERRIDE_PYTHON or '(default)')) print print backtick('bzr version') runcmd(['mkdir', TESTDIR]) cd(TESTDIR) test_root = os.getcwd() progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("internal tests") runcmd("bzr selftest") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) runcmd("bzr diff --message foo", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == 'unknown:\n test.txt\n' out = backtick("bzr status --all") assert out == "unknown:\n test.txt\n" out = backtick("bzr status test.txt --all") assert out == "unknown:\n test.txt\n" f = file('test2.txt', 'wt') f.write('goodbye cruel world...\n') f.close() out = backtick("bzr status test.txt") assert out == "unknown:\n test.txt\n" out = backtick("bzr status") assert out == ("unknown:\n" " test.txt\n" " test2.txt\n") os.unlink('test2.txt') progress("command aliases") out = backtick("bzr st --all") assert out == ("unknown:\n" " test.txt\n") out = backtick("bzr stat") assert out == ("unknown:\n" " test.txt\n") progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == ("added:\n" " test.txt\n") progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == os.path.join("sub2", "hello.txt\n") assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') assert backtick('bzr relpath sub2/hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') runcmd('bzr log') runcmd('bzr log -v') progress("file with spaces in name") mkdir('sub directory') file('sub directory/file with spaces ', 'wt').write('see how this works\n') runcmd('bzr add .') runcmd('bzr diff') runcmd('bzr commit -m add-spaces') runcmd('bzr check') runcmd('bzr log') runcmd('bzr log --forward') runcmd('bzr info') cd('..') cd('..') progress('branch') # Can't create a branch if it already exists runcmd('bzr branch branch1', retcode=1) # Can't create a branch if its parent doesn't exist runcmd('bzr branch /unlikely/to/exist', retcode=1) runcmd('bzr branch branch1 branch2') progress("pull") cd('branch1') runcmd('bzr pull', retcode=1) runcmd('bzr pull ../branch2') cd('.bzr') runcmd('bzr pull') runcmd('bzr commit -m empty') runcmd('bzr pull') cd('../../branch2') runcmd('bzr pull') runcmd('bzr commit -m empty') cd('../branch1') runcmd('bzr commit -m empty') runcmd('bzr pull', retcode=1) cd ('..') progress('status after remove') mkdir('status-after-remove') # see mail from William Dodé, 2005-05-25 # $ bzr init; touch a; bzr add a; bzr commit -m "add a" # * looking for changes... # added a # * commited r1 # $ bzr remove a # $ bzr status # bzr: local variable 'kind' referenced before assignment # at /vrac/python/bazaar-ng/bzrlib/diff.py:286 in compare_trees() # see ~/.bzr.log for debug information cd('status-after-remove') runcmd('bzr init') file('a', 'w').write('foo') runcmd('bzr add a') runcmd(['bzr', 'commit', '-m', 'add a']) runcmd('bzr remove a') runcmd('bzr status') cd('..') progress('ignore patterns') mkdir('ignorebranch') cd('ignorebranch') runcmd('bzr init') assert backtick('bzr unknowns') == '' file('foo.tmp', 'wt').write('tmp files are ignored') assert backtick('bzr unknowns') == '' file('foo.c', 'wt').write('int main() {}') assert backtick('bzr unknowns') == 'foo.c\n' runcmd('bzr add foo.c') assert backtick('bzr unknowns') == '' # 'ignore' works when creating the .bzignore file file('foo.blah', 'wt').write('blah') assert backtick('bzr unknowns') == 'foo.blah\n' runcmd('bzr ignore *.blah') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\n' # 'ignore' works when then .bzrignore file already exists file('garh', 'wt').write('garh') assert backtick('bzr unknowns') == 'garh\n' runcmd('bzr ignore garh') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\ngarh\n' cd('..') progress("recursive and non-recursive add") mkdir('no-recurse') cd('no-recurse') runcmd('bzr init') mkdir('foo') fp = os.path.join('foo', 'test.txt') f = file(fp, 'w') f.write('hello!\n') f.close() runcmd('bzr add --no-recurse foo') runcmd('bzr file-id foo') runcmd('bzr file-id ' + fp, 1) # not versioned yet runcmd('bzr commit -m add-dir-only') runcmd('bzr file-id ' + fp, 1) # still not versioned runcmd('bzr add foo') runcmd('bzr file-id ' + fp) runcmd('bzr commit -m add-sub-file') cd('..') progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) logfile.close() sys.stdout.writelines(file(os.path.join(start_dir, LOGFILENAME), 'rt').readlines()[-50:]) sys.exit(1) commit refs/heads/master mark :629 committer Martin Pool 1118058902 +1000 data 4 todo from :628 M 644 inline TODO data 12234 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * ``bzr log DIR`` should give changes to any files within DIR. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. Perhaps ~/.bzr/http-cache. Baz has a fairly simple cache under ~/.arch-cache, containing revision information encoded almost as a bunch of archives. Perhaps we could simply store full paths. * Maybe also store directories in the statcache so that we can quickly identify that they still exist. * Diff should show timestamps; for files from the working directory we can use the file itself; for files from a revision we should use the commit time of the revision. * Perhaps split command infrastructure from the actual command definitions. * Cleaner support for negative boolean options like --no-recurse. * Statcache should possibly map all file paths to / separators Medium things ------------- * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. We should be able to just get the id for the selected files, look up their location and diff just those files. No need to traverse the entire inventories. * ``bzr status DIR`` or ``bzr diff DIR`` should report on all changes under that directory. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from ElementTree to an object when it is read in, but rather wait until the program actually wants to know about that node. * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. - Selected-file commit - Impossible selected-file commit: adding things in non-versioned directories, crossing renames, etc. * Write a reproducible benchmark, perhaps importing various kernel versions. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Paranoid mode where we never trust SHA-1 matches. * Don't commit if there are no changes unless forced. * --dry-run mode for commit? (Or maybe just run with check-command=false?) * Generally, be a bit more verbose unless --silent is specified. * Function that finds all changes to files under a given directory; perhaps log should use this if a directory is given. * XML attributes might have trouble with filenames containing \n and \r. Do we really want to support this? I think perhaps not. * Remember execute bits, so that exports will work OK. * Unify smart_add and plain Branch.add(); perhaps smart_add should just build a list of files to add and pass that to the regular add function. * Function to list a directory, saying in which revision each file was last modified. Useful for web and gui interfaces, and slow to compute one file at a time. * unittest is standard, but the results are kind of ugly; would be nice to make it cleaner. * Check locking is correct during merge-related operations. * Perhaps attempts to get locks should timeout after some period of time, or at least display a progress message. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. Possibly this should be done by splitting the commit function into several parts (under a single interface). It is already rather large. Decomposition: - find tree modifications and prepare in-memory inventory - export that inventory to a temporary directory - run the test in that temporary directory - if that succeeded, continue to actually finish the commit What should be done with the text of modified files while this is underway? I don't think we want to count on holding them in memory and we can't trust the working files to stay in one place so I suppose we need to move them into the text store, or otherwise into a temporary directory. If the commit does not actually complete, we would rather the content was not left behind in the stores. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :630 committer Martin Pool 1118061076 +1000 data 38 - don't ignore .diff files in bzr tree from :629 M 644 inline .bzrignore data 124 ## *.diff ./doc/*.html *.py[oc] *~ .arch-ids .bzr.profile .arch-inventory {arch} CHANGELOG bzr-test.log ,,* testbzr.log api commit refs/heads/master mark :631 committer Martin Pool 1118061191 +1000 data 61 - add deferred patch for finding touching patches from a list from :630 M 644 inline patches/find-touching-from-seq.diff data 2796 *** modified file 'bzrlib/commands.py' --- bzrlib/commands.py +++ bzrlib/commands.py @@ -610,11 +610,13 @@ hidden = True takes_args = ["filename"] def run(self, filename): + from bzrlib.log import find_touching_revisions b = Branch(filename, lock_mode='r') inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) - for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): - print "%6d %s" % (revno, what) + rh = b.revision_history() + for revision_id, what in find_touching_revisions(b, file_id, rh): + print "%-40s %s" % (revision_id, what) class cmd_ls(Command): *** modified file 'bzrlib/log.py' --- bzrlib/log.py +++ bzrlib/log.py @@ -17,7 +17,7 @@ -def find_touching_revisions(branch, file_id): +def find_touching_revisions(branch, file_id, revisions): """Yield a description of revisions which affect the file_id. Each returned element is (revno, revision_id, description) @@ -25,13 +25,20 @@ This is the list of revisions where the file is either added, modified, renamed or deleted. - TODO: Perhaps some way to limit this to only particular revisions, - or to traverse a non-mainline set of revisions? + branch + Branch to examine + + file_id + File to consider + + revisions + Sequence of revisions to search, can be + branch.revision_history() or a filtered version of that + or some sequence of non-mailine revisions. """ last_ie = None last_path = None - revno = 1 - for revision_id in branch.revision_history(): + for revision_id in revisions: this_inv = branch.get_revision_inventory(revision_id) if file_id in this_inv: this_ie = this_inv[file_id] @@ -46,19 +53,19 @@ # not present in either pass elif this_ie and not last_ie: - yield revno, revision_id, "added " + this_path + yield revision_id, "added " + this_path elif not this_ie and last_ie: # deleted here - yield revno, revision_id, "deleted " + last_path + yield revision_id, "deleted " + last_path elif this_path != last_path: - yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path)) + yield revision_id, ("renamed %s => %s" % (last_path, this_path)) elif (this_ie.text_size != last_ie.text_size or this_ie.text_sha1 != last_ie.text_sha1): - yield revno, revision_id, "modified " + this_path + yield revision_id, "modified " + this_path last_ie = this_ie last_path = this_path - revno += 1 + def show_log(branch, commit refs/heads/master mark :632 committer Martin Pool 1118062488 +1000 data 22 - refactor commit code from :631 M 644 inline bzrlib/commit.py data 9977 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def commit(branch, message, timestamp=None, timezone=None, committer=None, verbose=True, specific_files=None, rev_id=None): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. specific_files If true, commit only those files. rev_id If set, use this as the new revision id. Useful for test or import commands that need to tightly control what revisions are assigned. If you duplicate a revision id that exists elsewhere it is your own fault. If null (default), a time/random revision id is generated. """ import time, tempfile from osutils import local_time_offset, username from branch import gen_file_id from errors import BzrError from revision import Revision from trace import mutter, note branch.lock_write() try: # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_tree = branch.working_tree() work_inv = work_tree.inventory basis = branch.basis_tree() basis_inv = basis.inventory if verbose: note('looking for changes...') missing_ids, new_inv = _gather_commit(branch, work_tree, work_inv, basis_inv, specific_files, verbose) for file_id in missing_ids: # Any files that have been deleted are now removed from the # working inventory. Files that were not selected for commit # are left as they were in the working inventory and ommitted # from the revision inventory. # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itbranch. if work_inv.has_id(file_id): del work_inv[file_id] if rev_id is None: rev_id = _gen_revision_id(time.time()) inv_id = rev_id inv_tmp = tempfile.TemporaryFile() new_inv.write_xml(inv_tmp) inv_tmp.seek(0) branch.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) branch._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = branch.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) branch.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (branch.revno() + 1)) branch.append_revision(rev_id) if verbose: note("commited r%d" % branch.revno()) finally: branch.unlock() def _gen_revision_id(when): """Return new revision-id.""" from binascii import hexlify from osutils import rand_bytes, compact_date, user_email s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def _gather_commit(branch, work_tree, work_inv, basis_inv, specific_files, verbose): """Build inventory preparatory to commit. This adds any changed files into the text store, and sets their test-id, sha and size in the returned inventory appropriately. missing_ids Modified to hold a list of files that have been deleted from the working directory; these should be removed from the working inventory. """ from bzrlib.inventory import Inventory from osutils import isdir, isfile, sha_string, quotefn, \ local_time_offset, username, kind_marker, is_inside_any from branch import gen_file_id from errors import BzrError from revision import Revision from bzrlib.trace import mutter, note inv = Inventory() missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). ## TODO: Don't need to copy this unless we're going to change it entry = entry.copy() p = branch.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if specific_files and not is_inside_any(specific_files, path): if basis_inv.has_id(file_id): # carry over with previous state inv.add(basis_inv[file_id].copy()) else: # omit this from committed inventory pass continue if not work_tree.has_id(file_id): if verbose: print('deleted %s%s' % (path, kind_marker(entry.kind))) mutter(" file is missing, removing from inventory") missing_ids.append(file_id) continue inv.add(entry) if basis_inv.has_id(file_id): old_kind = basis_inv[file_id].kind if old_kind != entry.kind: raise BzrError("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): raise BzrError("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): raise BzrError("%s is entered as file but is not a file" % quotefn(p)) new_sha1 = work_tree.get_file_sha1(file_id) old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] if (old_ie and old_ie.text_sha1 == new_sha1): ## assert content == basis.get_file(file_id).read() entry.text_id = old_ie.text_id entry.text_sha1 = new_sha1 entry.text_size = old_ie.text_size mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: content = file(p, 'rb').read() # calculate the sha again, just in case the file contents # changed since we updated the cache entry.text_sha1 = sha_string(content) entry.text_size = len(content) entry.text_id = gen_file_id(entry.name) branch.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: ## TODO: Also show these for directories! if not old_ie: print('added %s' % path) elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): print('modified %s' % path) else: print('renamed %s' % path) return missing_ids, inv commit refs/heads/master mark :633 committer Martin Pool 1118063559 +1000 data 103 - Show added/renamed/modified messages from commit for non-file entries. - refactor commit a bit more from :632 M 644 inline bzrlib/commit.py data 9976 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def commit(branch, message, timestamp=None, timezone=None, committer=None, verbose=True, specific_files=None, rev_id=None): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. specific_files If true, commit only those files. rev_id If set, use this as the new revision id. Useful for test or import commands that need to tightly control what revisions are assigned. If you duplicate a revision id that exists elsewhere it is your own fault. If null (default), a time/random revision id is generated. """ import time, tempfile from osutils import local_time_offset, username from branch import gen_file_id from errors import BzrError from revision import Revision from trace import mutter, note branch.lock_write() try: # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_tree = branch.working_tree() work_inv = work_tree.inventory basis = branch.basis_tree() basis_inv = basis.inventory if verbose: note('looking for changes...') missing_ids, new_inv = _gather_commit(branch, work_tree, work_inv, basis_inv, specific_files, verbose) for file_id in missing_ids: # Any files that have been deleted are now removed from the # working inventory. Files that were not selected for commit # are left as they were in the working inventory and ommitted # from the revision inventory. # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itbranch. if work_inv.has_id(file_id): del work_inv[file_id] if rev_id is None: rev_id = _gen_revision_id(time.time()) inv_id = rev_id inv_tmp = tempfile.TemporaryFile() new_inv.write_xml(inv_tmp) inv_tmp.seek(0) branch.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) branch._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = branch.last_patch(), message = message, inventory_id=inv_id, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) branch.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (branch.revno() + 1)) branch.append_revision(rev_id) if verbose: note("commited r%d" % branch.revno()) finally: branch.unlock() def _gen_revision_id(when): """Return new revision-id.""" from binascii import hexlify from osutils import rand_bytes, compact_date, user_email s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def _gather_commit(branch, work_tree, work_inv, basis_inv, specific_files, verbose): """Build inventory preparatory to commit. This adds any changed files into the text store, and sets their test-id, sha and size in the returned inventory appropriately. missing_ids Modified to hold a list of files that have been deleted from the working directory; these should be removed from the working inventory. """ from bzrlib.inventory import Inventory from osutils import isdir, isfile, sha_string, quotefn, \ local_time_offset, username, kind_marker, is_inside_any from branch import gen_file_id from errors import BzrError from revision import Revision from bzrlib.trace import mutter, note inv = Inventory() missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). p = branch.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if specific_files and not is_inside_any(specific_files, path): if basis_inv.has_id(file_id): # carry over with previous state inv.add(basis_inv[file_id].copy()) else: # omit this from committed inventory pass continue if not work_tree.has_id(file_id): if verbose: print('deleted %s%s' % (path, kind_marker(entry.kind))) mutter(" file is missing, removing from inventory") missing_ids.append(file_id) continue # this is present in the new inventory; may be new, modified or # unchanged. old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] entry = entry.copy() inv.add(entry) if old_ie: old_kind = old_ie.kind if old_kind != entry.kind: raise BzrError("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): raise BzrError("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): raise BzrError("%s is entered as file but is not a file" % quotefn(p)) new_sha1 = work_tree.get_file_sha1(file_id) if (old_ie and old_ie.text_sha1 == new_sha1): ## assert content == basis.get_file(file_id).read() entry.text_id = old_ie.text_id entry.text_sha1 = new_sha1 entry.text_size = old_ie.text_size mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: content = file(p, 'rb').read() # calculate the sha again, just in case the file contents # changed since we updated the cache entry.text_sha1 = sha_string(content) entry.text_size = len(content) entry.text_id = gen_file_id(entry.name) branch.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: marked = path + kind_marker(entry.kind) if not old_ie: print 'added', marked elif old_ie == entry: pass # unchanged elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): print 'modified', marked else: print 'renamed', marked return missing_ids, inv commit refs/heads/master mark :634 committer Martin Pool 1118063966 +1000 data 20 - Tidy help messages from :633 M 644 inline bzrlib/commands.py data 43073 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import errno br_to = Branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if errno == errno.ENOENT: raise if location is None: location = stored_loc if location is None: raise BzrCommandError("No pull location known or specified.") from branch import find_branch, DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged. Try merge.") merge(('.', -1), ('.', old_revno)) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. """ takes_args = ['from_location', 'to_location?'] def run(self, from_location, to_location=None): import errno from bzrlib.merge import merge if to_location is None: to_location = os.path.basename(from_location) # FIXME: If there's a trailing slash, keep removing them # until we find the right bit try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) from branch import find_branch, DivergedBranches try: br_from = find_branch(from_location) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not exist.' % to_location) else: raise from_location = pull_loc(br_from) br_to.update_revisions(br_from) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False) br_to.controlfile("x-pull", "wb").write(from_location + "\n") def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest if selftest(): return 0 else: return 1 class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :635 committer Martin Pool 1118064258 +1000 data 45 - manpage generator by Hans Ulrich Niedermann from :634 M 644 inline bzr-man.py data 7629 #!/usr/bin/python # Copyright (C) 2005 by Hans Ulrich Niedermann # Portions Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #<<< code taken from bzr (C) Canonical import os, sys try: version_info = sys.version_info except AttributeError: version_info = 1, 5 # 1.5 or older REINVOKE = "__BZR_REINVOKE" NEED_VERS = (2, 3) if version_info < NEED_VERS: if not os.environ.has_key(REINVOKE): # mutating os.environ doesn't work in old Pythons os.putenv(REINVOKE, "1") for python in 'python2.4', 'python2.3': try: os.execvp(python, [python] + sys.argv) except OSError: pass print >>sys.stderr, "bzr-man.py: error: cannot find a suitable python interpreter" print >>sys.stderr, " (need %d.%d or later)" % NEED_VERS sys.exit(1) os.unsetenv(REINVOKE) import bzrlib, bzrlib.help #>>> code taken from bzr (C) Canonical #<<< code by HUN import time import re def man_escape(string): result = string.replace("\\","\\\\") result = result.replace("`","\\`") result = result.replace("'","\\'") result = result.replace("-","\\-") return result class Parser: def parse_line(self, line): pass class CommandListParser(Parser): """Parser for output of "bzr help commands". The parsed content can then be used to - write a "COMMAND OVERVIEW" section into a man page - provide a list of all commands """ def __init__(self,params): self.params = params self.command_usage = [] self.all_commands = [] self.usage_exp = re.compile("([a-z0-9-]+).*") self.descr_exp = re.compile(" ([A-Z].*)\s*") self.state = 0 self.command = None self.usage = None self.descr = None def parse_line(self, line): m = self.usage_exp.match(line) if m: if self.state == 0: if self.usage: self.command_usage.append((self.command,self.usage,self.descr)) self.all_commands.append(self.command) self.usage = line self.command = m.groups()[0] else: raise Error, "matching usage line in state %d" % state self.state = 1 return m = self.descr_exp.match(line) if m: if self.state == 1: self.descr = m.groups()[0] else: raise Error, "matching descr line in state %d" % state self.state = 0 return raise Error, "Cannot parse this line" def end_parse(self): if self.state == 0: if self.usage: self.command_usage.append((self.command,self.usage,self.descr)) self.all_commands.append(self.command) else: raise Error, "ending parse in state %d" % state def write_to_manpage(self, outfile): bzrcmd = self.params["bzrcmd"] outfile.write('.SH "COMMAND OVERVIEW"\n') for (command,usage,descr) in self.command_usage: outfile.write('.TP\n.B "%s %s"\n%s\n\n' % (bzrcmd, usage, descr)) class HelpReader: def __init__(self, parser): self.parser = parser def write(self, data): if data[-1] == '\n': data = data[:-1] for line in data.split('\n'): self.parser.parse_line(line) def write_command_details(params, command, usage, descr, outfile): x = ('.SS "%s %s"\n.B "%s"\n.PP\n.B "Usage:"\n%s %s\n\n' % (params["bzrcmd"], command, descr, params["bzrcmd"], usage)) outfile.write(man_escape(x)) man_preamble = """\ .\\\" Man page for %(bzrcmd)s (bazaar-ng) .\\\" .\\\" Large parts of this file are autogenerated from the output of .\\\" \"%(bzrcmd)s help commands\" .\\\" \"%(bzrcmd)s help \" .\\\" .\\\" Generation time: %(timestamp)s .\\\" """ # The DESCRIPTION text was taken from http://www.bazaar-ng.org/ # and is thus (C) Canonical man_head = """\ .TH bzr 1 "%(datestamp)s" "%(version)s" "bazaar-ng" .SH "NAME" %(bzrcmd)s - bazaar-ng next-generation distributed version control .SH "SYNOPSIS" .B "%(bzrcmd)s" .I "command" [ .I "command_options" ] .br .B "%(bzrcmd)s" .B "help" .br .B "%(bzrcmd)s" .B "help" .I "command" .SH "DESCRIPTION" bazaar-ng (or .B "%(bzrcmd)s" ) is a project of Canonical to develop an open source distributed version control system that is powerful, friendly, and scalable. Version control means a system that keeps track of previous revisions of software source code or similar information and helps people work on it in teams. .SS "Warning" bazaar-ng is at an early stage of development, and the design is still changing from week to week. This man page here may be inconsistent with itself, with other documentation or with the code, and sometimes refer to features that are planned but not yet written. Comments are still very welcome; please send them to bazaar-ng@lists.canonical.com. """ man_foot = """\ .SH "ENVIRONMENT" .TP .I "BZRPATH" Path where .B "%(bzrcmd)s" is to look for external command. .TP .I "BZREMAIL" E-Mail address of the user. Overrides .I "~/.bzr.conf/email" and .IR "EMAIL" . Example content: .I "John Doe " .TP .I "EMAIL" E-Mail address of the user. Overridden by the content of the file .I "~/.bzr.conf/email" and of the environment variable .IR "BZREMAIL" . .SH "FILES" .TP .I "~/.bzr.conf/" Directory where all the user\'s settings are stored. .TP .I "~/.bzr.conf/email" Stores name and email address of the user. Overrides content of .I "EMAIL" environment variable. Example content: .I "John Doe " .SH "SEE ALSO" .UR http://www.bazaar-ng.org/ .BR http://www.bazaar-ng.org/, .UR http://www.bazaar-ng.org/doc/ .BR http://www.bazaar-ng.org/doc/ """ def main(): t = time.time() tt = time.gmtime(t) params = \ { "bzrcmd": "bzr", "datestamp": time.strftime("%Y-%m-%d",tt), "timestamp": time.strftime("%Y-%m-%d %H:%M:%S +0000",tt), "version": bzrlib.__version__, } clp = CommandListParser(params) bzrlib.help.help("commands", outfile=HelpReader(clp)) clp.end_parse() filename = "bzr.1" if len(sys.argv) == 2: filename = sys.argv[1] if filename == "-": outfile = sys.stdout else: outfile = open(filename,"w") outfile.write(man_preamble % params) outfile.write(man_escape(man_head % params)) clp.write_to_manpage(outfile) # FIXME: # This doesn't do more than the summary so far. #outfile.write('.SH "DETAILED COMMAND DESCRIPTION"\n') #for (command,usage,descr) in clp.command_usage: # write_command_details(params, command, usage, descr, outfile = outfile) outfile.write(man_escape(man_foot % params)) if __name__ == '__main__': main() #>>> code by HUN M 644 inline .bzrignore data 130 ## *.diff ./doc/*.html *.py[oc] *~ .arch-ids .bzr.profile .arch-inventory {arch} CHANGELOG bzr-test.log bzr.1 ,,* testbzr.log api M 644 inline bzrlib/help.py data 4564 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA global_help = \ """Bazaar-NG -- a free distributed version-control tool http://bazaar-ng.org/ **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** * Metadata format is not stable yet -- you may need to discard history in the future. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. To make a branch, use 'bzr init' in an existing directory, then 'bzr add' to make files versioned. 'bzr add .' will recursively add all non-ignored files. 'bzr status' describes files that are unknown, ignored, or modified. 'bzr diff' shows the text changes to the tree or named files. 'bzr commit -m ' commits all changes in that branch. 'bzr move' and 'bzr rename' allow you to rename files or directories. 'bzr remove' makes a file unversioned but keeps the working copy; to delete that too simply delete the file. 'bzr log' shows a history of changes, and 'bzr info' gives summary statistical information. 'bzr check' validates all files are stored safely. Files can be ignored by giving a path or a glob in .bzrignore at the top of the tree. Use 'bzr ignored' to see what files are ignored and why, and 'bzr unknowns' to see files that are neither versioned or ignored. For more help on any command, type 'bzr help COMMAND', or 'bzr help commands' for a list. """ import sys def help(topic=None, outfile = None): if outfile == None: outfile = sys.stdout if topic == None: outfile.write(global_help) elif topic == 'commands': help_commands(outfile = outfile) else: help_on_command(topic, outfile = outfile) def command_usage(cmdname, cmdclass): """Return single-line grammar for command. Only describes arguments, not options. """ s = cmdname + ' ' for aname in cmdclass.takes_args: aname = aname.upper() if aname[-1] in ['$', '+']: aname = aname[:-1] + '...' elif aname[-1] == '?': aname = '[' + aname[:-1] + ']' elif aname[-1] == '*': aname = '[' + aname[:-1] + '...]' s += aname + ' ' assert s[-1] == ' ' s = s[:-1] return s def help_on_command(cmdname, outfile = None): cmdname = str(cmdname) if outfile == None: outfile = sys.stdout from inspect import getdoc import commands topic, cmdclass = commands.get_cmd_class(cmdname) doc = getdoc(cmdclass) if doc == None: raise NotImplementedError("sorry, no detailed help yet for %r" % cmdname) outfile.write('usage: ' + command_usage(topic, cmdclass) + '\n') if cmdclass.aliases: outfile.write('aliases: ' + ', '.join(cmdclass.aliases) + '\n') outfile.write(doc) help_on_option(cmdclass.takes_options, outfile = None) def help_on_option(options, outfile = None): import commands if not options: return if outfile == None: outfile = sys.stdout outfile.write('\noptions:\n') for on in options: l = ' --' + on for shortname, longname in commands.SHORT_OPTIONS.items(): if longname == on: l += ', -' + shortname break outfile.write(l + '\n') def help_commands(outfile = None): """List all commands""" import inspect import commands if outfile == None: outfile = sys.stdout accu = [] for cmdname, cmdclass in commands.get_all_cmds(): accu.append((cmdname, cmdclass)) accu.sort() for cmdname, cmdclass in accu: if cmdclass.hidden: continue outfile.write(command_usage(cmdname, cmdclass) + '\n') help = inspect.getdoc(cmdclass) if help: outfile.write(" " + help.split('\n', 1)[0] + '\n') M 644 inline contrib/add-bzr-to-baz data 205 #! /bin/sh -e # Take a file that is versioned by bzr and # add it to baz with the same file-id. if [ $# -ne 1 ] then echo "usage: $0 FILE" >&2 exit 1 fi baz add -i "$( bzr file-id "$1" )" "$1" commit refs/heads/master mark :636 committer Martin Pool 1118127860 +1000 data 30 - fix missing import in revert from :635 M 644 inline bzrlib/commands.py data 43112 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import errno br_to = Branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if errno == errno.ENOENT: raise if location is None: location = stored_loc if location is None: raise BzrCommandError("No pull location known or specified.") from branch import find_branch, DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged. Try merge.") merge(('.', -1), ('.', old_revno)) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. """ takes_args = ['from_location', 'to_location?'] def run(self, from_location, to_location=None): import errno from bzrlib.merge import merge if to_location is None: to_location = os.path.basename(from_location) # FIXME: If there's a trailing slash, keep removing them # until we find the right bit try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) from branch import find_branch, DivergedBranches try: br_from = find_branch(from_location) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not exist.' % to_location) else: raise from_location = pull_loc(br_from) br_to.update_revisions(br_from) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False) br_to.controlfile("x-pull", "wb").write(from_location + "\n") def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest if selftest(): return 0 else: return 1 class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :637 committer Martin Pool 1118135260 +1000 data 15 - add assertion from :636 M 644 inline bzrlib/statcache.py data 8873 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import stat, os, sha, time from trace import mutter from errors import BzrError, BzrCheckError """File stat cache to speed up tree comparisons. This module basically gives a quick way to find the SHA-1 and related information of a file in the working directory, without actually reading and hashing the whole file. Implementation ============== Users of this module should not need to know about how this is implemented, and in particular should not depend on the particular data which is stored or its format. This is done by maintaining a cache indexed by a file fingerprint of (path, size, mtime, ctime, ino, dev) pointing to the SHA-1. If the fingerprint has changed, we assume the file content has not changed either and the SHA-1 is therefore the same. If any of the fingerprint fields have changed then the file content *may* have changed, or it may not have. We need to reread the file contents to make sure, but this is not visible to the user or higher-level code (except as a delay of course). The mtime and ctime are stored with nanosecond fields, but not all filesystems give this level of precision. There is therefore a possible race: the file might be modified twice within a second without changing the size or mtime, and a SHA-1 cached from the first version would be wrong. We handle this by not recording a cached hash for any files which were modified in the current second and that therefore have the chance to change again before the second is up. The only known hole in this design is if the system clock jumps backwards crossing invocations of bzr. Please don't do that; use ntp to gradually adjust your clock or don't use bzr over the step. At the moment this is stored in a simple textfile; it might be nice to use a tdb instead to allow faster lookup by file-id. The cache is represented as a map from file_id to a tuple of (file_id, sha1, path, size, mtime, ctime, ino, dev). The SHA-1 is stored in memory as a hexdigest. This version of the file on disk has one line per record, and fields separated by \0 records. """ # order of fields returned by fingerprint() FP_SIZE = 0 FP_MTIME = 1 FP_CTIME = 2 FP_INO = 3 FP_DEV = 4 # order of fields in the statcache file and in the in-memory map SC_FILE_ID = 0 SC_SHA1 = 1 SC_PATH = 2 SC_SIZE = 3 SC_MTIME = 4 SC_CTIME = 5 SC_INO = 6 SC_DEV = 7 CACHE_HEADER = "### bzr statcache v4" def fingerprint(abspath): try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) def _write_cache(basedir, entries): from atomicfile import AtomicFile cachefn = os.path.join(basedir, '.bzr', 'stat-cache') outf = AtomicFile(cachefn, 'wb') try: outf.write(CACHE_HEADER + '\n') for entry in entries: if len(entry) != 8: raise ValueError("invalid statcache entry tuple %r" % entry) outf.write(entry[0].encode('utf-8')) # file id outf.write('\0') outf.write(entry[1]) # hex sha1 outf.write('\0') outf.write(entry[2].encode('utf-8')) # name for nf in entry[3:]: outf.write('\0%d' % nf) outf.write('\n') outf.commit() finally: if not outf.closed: outf.abort() def _try_write_cache(basedir, entries): try: return _write_cache(basedir, entries) except IOError, e: mutter("cannot update statcache in %s: %s" % (basedir, e)) except OSError, e: mutter("cannot update statcache in %s: %s" % (basedir, e)) def load_cache(basedir): import re cache = {} seen_paths = {} from bzrlib.trace import warning assert isinstance(basedir, basestring) sha_re = re.compile(r'[a-f0-9]{40}') try: cachefn = os.path.join(basedir, '.bzr', 'stat-cache') cachefile = open(cachefn, 'rb') except IOError: return cache line1 = cachefile.readline().rstrip('\r\n') if line1 != CACHE_HEADER: mutter('cache header marker not found at top of %s; discarding cache' % cachefn) return cache for l in cachefile: f = l.split('\0') file_id = f[0].decode('utf-8') if file_id in cache: warning("duplicated file_id in cache: {%s}" % file_id) text_sha = f[1] if len(text_sha) != 40 or not sha_re.match(text_sha): raise BzrCheckError("invalid file SHA-1 in cache: %r" % text_sha) path = f[2].decode('utf-8') if path in seen_paths: warning("duplicated path in cache: %r" % path) seen_paths[path] = True entry = (file_id, text_sha, path) + tuple([long(x) for x in f[3:]]) if len(entry) != 8: raise ValueError("invalid statcache entry tuple %r" % entry) cache[file_id] = entry return cache def _files_from_inventory(inv): for path, ie in inv.iter_entries(): if ie.kind != 'file': continue yield ie.file_id, path def update_cache(basedir, inv, flush=False): """Update and return the cache for the branch. The returned cache may contain entries that have not been written to disk for files recently touched. flush -- discard any previous cache and recalculate from scratch. """ # load the existing cache; use information there to find a list of # files ordered by inode, which is alleged to be the fastest order # to stat the files. to_update = _files_from_inventory(inv) assert isinstance(flush, bool) if flush: cache = {} else: cache = load_cache(basedir) by_inode = [] without_inode = [] for file_id, path in to_update: if file_id in cache: by_inode.append((cache[file_id][SC_INO], file_id, path)) else: without_inode.append((file_id, path)) by_inode.sort() to_update = [a[1:] for a in by_inode] + without_inode stat_cnt = missing_cnt = new_cnt = hardcheck = change_cnt = 0 # dangerfiles have been recently touched and can't be committed to # a persistent cache yet, but they are returned to the caller. dangerfiles = [] now = int(time.time()) ## mutter('update statcache under %r' % basedir) for file_id, path in to_update: abspath = os.path.join(basedir, path) fp = fingerprint(abspath) stat_cnt += 1 cacheentry = cache.get(file_id) if fp == None: # not here if cacheentry: del cache[file_id] change_cnt += 1 missing_cnt += 1 continue elif not cacheentry: new_cnt += 1 if (fp[FP_MTIME] >= now) or (fp[FP_CTIME] >= now): dangerfiles.append(file_id) if cacheentry and (cacheentry[3:] == fp): continue # all stat fields unchanged hardcheck += 1 dig = sha.new(file(abspath, 'rb').read()).hexdigest() # We update the cache even if the digest has not changed from # last time we looked, so that the fingerprint fields will # match in future. cacheentry = (file_id, dig, path) + fp cache[file_id] = cacheentry change_cnt += 1 mutter('statcache: statted %d files, read %d files, %d changed, %d dangerous, ' '%d deleted, %d new, ' '%d in cache' % (stat_cnt, hardcheck, change_cnt, len(dangerfiles), missing_cnt, new_cnt, len(cache))) if change_cnt: mutter('updating on-disk statcache') if dangerfiles: safe_cache = cache.copy() for file_id in dangerfiles: del safe_cache[file_id] else: safe_cache = cache _try_write_cache(basedir, safe_cache.itervalues()) return cache commit refs/heads/master mark :638 committer Martin Pool 1118292840 +1000 data 50 - add 'dif' as alias for 'diff' command from mpe from :637 M 644 inline bzrlib/commands.py data 43119 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import errno br_to = Branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if errno == errno.ENOENT: raise if location is None: location = stored_loc if location is None: raise BzrCommandError("No pull location known or specified.") from branch import find_branch, DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged. Try merge.") merge(('.', -1), ('.', old_revno)) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. """ takes_args = ['from_location', 'to_location?'] def run(self, from_location, to_location=None): import errno from bzrlib.merge import merge if to_location is None: to_location = os.path.basename(from_location) # FIXME: If there's a trailing slash, keep removing them # until we find the right bit try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) from branch import find_branch, DivergedBranches try: br_from = find_branch(from_location) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not exist.' % to_location) else: raise from_location = pull_loc(br_from) br_to.update_revisions(br_from) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False) br_to.controlfile("x-pull", "wb").write(from_location + "\n") def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest if selftest(): return 0 else: return 1 class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :639 committer Martin Pool 1118368032 +1000 data 20 - add TreeDelta repr from :638 M 644 inline bzrlib/diff.py data 14120 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from trace import mutter from errors import BzrError def internal_diff(old_label, oldlines, new_label, newlines, to_file): import difflib # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, fromfile=old_label, tofile=new_label) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') to_file.writelines(ud) if nonl: print >>to_file, "\\ No newline at end of file" print >>to_file def external_diff(old_label, oldlines, new_label, newlines, to_file, diff_opts): """Display a diff by calling out to the external diff program.""" import sys if to_file != sys.stdout: raise NotImplementedError("sorry, can't send external diff other than to stdout yet", to_file) # make sure our own output is properly ordered before the diff to_file.flush() from tempfile import NamedTemporaryFile import os oldtmpf = NamedTemporaryFile() newtmpf = NamedTemporaryFile() try: # TODO: perhaps a special case for comparing to or from the empty # sequence; can just use /dev/null on Unix # TODO: if either of the files being compared already exists as a # regular named file (e.g. in the working directory) then we can # compare directly to that, rather than copying it. oldtmpf.writelines(oldlines) newtmpf.writelines(newlines) oldtmpf.flush() newtmpf.flush() if not diff_opts: diff_opts = [] diffcmd = ['diff', '--label', old_label, oldtmpf.name, '--label', new_label, newtmpf.name] # diff only allows one style to be specified; they don't override. # note that some of these take optargs, and the optargs can be # directly appended to the options. # this is only an approximate parser; it doesn't properly understand # the grammar. for s in ['-c', '-u', '-C', '-U', '-e', '--ed', '-q', '--brief', '--normal', '-n', '--rcs', '-y', '--side-by-side', '-D', '--ifdef']: for j in diff_opts: if j.startswith(s): break else: continue break else: diffcmd.append('-u') if diff_opts: diffcmd.extend(diff_opts) rc = os.spawnvp(os.P_WAIT, 'diff', diffcmd) if rc != 0 and rc != 1: # returns 1 if files differ; that's OK if rc < 0: msg = 'signal %d' % (-rc) else: msg = 'exit code %d' % rc raise BzrError('external diff failed with %s; command: %r' % (rc, diffcmd)) finally: oldtmpf.close() # and delete newtmpf.close() def show_diff(b, revision, specific_files, external_diff_options=None): """Shortcut for showing the diff to the working tree. b Branch. revision None for each, or otherwise the old revision to compare against. The more general form is show_diff_trees(), where the caller supplies any two trees. """ import sys if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() show_diff_trees(old_tree, new_tree, sys.stdout, specific_files, external_diff_options) def show_diff_trees(old_tree, new_tree, to_file, specific_files=None, external_diff_options=None): """Show in text form the changes from one tree to another. to_files If set, include only changes to these files. external_diff_options If set, use an external GNU diff and pass these options. """ # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. if external_diff_options: assert isinstance(external_diff_options, basestring) opts = external_diff_options.split() def diff_file(olab, olines, nlab, nlines, to_file): external_diff(olab, olines, nlab, nlines, to_file, opts) else: diff_file = internal_diff delta = compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=specific_files) for path, file_id, kind in delta.removed: print '*** removed %s %r' % (kind, path) if kind == 'file': diff_file(old_label + path, old_tree.get_file(file_id).readlines(), DEVNULL, [], to_file) for path, file_id, kind in delta.added: print '*** added %s %r' % (kind, path) if kind == 'file': diff_file(DEVNULL, [], new_label + path, new_tree.get_file(file_id).readlines(), to_file) for old_path, new_path, file_id, kind, text_modified in delta.renamed: print '*** renamed %s %r => %r' % (kind, old_path, new_path) if text_modified: diff_file(old_label + old_path, old_tree.get_file(file_id).readlines(), new_label + new_path, new_tree.get_file(file_id).readlines(), to_file) for path, file_id, kind in delta.modified: print '*** modified %s %r' % (kind, path) if kind == 'file': diff_file(old_label + path, old_tree.get_file(file_id).readlines(), new_label + path, new_tree.get_file(file_id).readlines(), to_file) class TreeDelta(object): """Describes changes from one tree to another. Contains four lists: added (path, id, kind) removed (path, id, kind) renamed (oldpath, newpath, id, kind, text_modified) modified (path, id, kind) unchanged (path, id, kind) Each id is listed only once. Files that are both modified and renamed are listed only in renamed, with the text_modified flag true. The lists are normally sorted when the delta is created. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] self.unchanged = [] def __repr__(self): return "TreeDelta(added=%r, removed=%r, renamed=%r, modified=%r," \ " unchanged=%r)" % (self.added, self.removed, self.renamed, self.modified, self.unchanged) def has_changed(self): changes = len(self.added) + len(self.removed) + len(self.renamed) changes += len(self.modified) return (changes != 0) def touches_file_id(self, file_id): """Return True if file_id is modified by this delta.""" for l in self.added, self.removed, self.modified: for v in l: if v[1] == file_id: return True for v in self.renamed: if v[2] == file_id: return True return False def show(self, to_file, show_ids=False, show_unchanged=False): def show_list(files): for path, fid, kind in files: if kind == 'directory': path += '/' elif kind == 'symlink': path += '@' if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if self.removed: print >>to_file, 'removed:' show_list(self.removed) if self.added: print >>to_file, 'added:' show_list(self.added) if self.renamed: print >>to_file, 'renamed:' for oldpath, newpath, fid, kind, text_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if self.modified: print >>to_file, 'modified:' show_list(self.modified) if show_unchanged and self.unchanged: print >>to_file, 'unchanged:' show_list(self.unchanged) def compare_trees(old_tree, new_tree, want_unchanged, specific_files=None): """Describe changes from one tree to another. Returns a TreeDelta with details of added, modified, renamed, and deleted entries. The root entry is specifically exempt. This only considers versioned files. want_unchanged If true, also list files unchanged from one version to the next. specific_files If true, only check for changes to specified names or files within them. """ from osutils import is_inside_any old_inv = old_tree.inventory new_inv = new_tree.inventory delta = TreeDelta() mutter('start compare_trees') # TODO: match for specific files can be rather smarter by finding # the IDs of those files up front and then considering only that. for file_id in old_tree: if file_id in new_tree: kind = old_inv.get_file_kind(file_id) assert kind == new_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ 'invalid file kind %r' % kind if kind == 'root_directory': continue old_path = old_inv.id2path(file_id) new_path = new_inv.id2path(file_id) if specific_files: if (not is_inside_any(specific_files, old_path) and not is_inside_any(specific_files, new_path)): continue if kind == 'file': old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False # TODO: Can possibly avoid calculating path strings if the # two files are unchanged and their names and parents are # the same and the parents are unchanged all the way up. # May not be worthwhile. if old_path != new_path: delta.renamed.append((old_path, new_path, file_id, kind, text_modified)) elif text_modified: delta.modified.append((new_path, file_id, kind)) elif want_unchanged: delta.unchanged.append((new_path, file_id, kind)) else: kind = old_inv.get_file_kind(file_id) old_path = old_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, old_path): continue delta.removed.append((old_path, file_id, kind)) mutter('start looking for new files') for file_id in new_inv: if file_id in old_inv: continue new_path = new_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, new_path): continue kind = new_inv.get_file_kind(file_id) delta.added.append((new_path, file_id, kind)) delta.removed.sort() delta.added.sort() delta.renamed.sort() delta.modified.sort() delta.unchanged.sort() return delta commit refs/heads/master mark :640 committer Martin Pool 1118368099 +1000 data 65 - bzr pull should not check that the tree is clean (from aaron) from :639 M 644 inline bzrlib/commands.py data 43138 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def get_all_cmds(): """Return canonical name and class for all registered commands.""" for k, v in globals().iteritems(): if k.startswith("cmd_"): yield _unsquish_command_name(k), v def get_cmd_class(cmd): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name try: return cmd, globals()[_squish_command_name(cmd)] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in get_all_cmds(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(':'): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import errno br_to = Branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if errno == errno.ENOENT: raise if location is None: location = stored_loc if location is None: raise BzrCommandError("No pull location known or specified.") from branch import find_branch, DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged. Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. """ takes_args = ['from_location', 'to_location?'] def run(self, from_location, to_location=None): import errno from bzrlib.merge import merge if to_location is None: to_location = os.path.basename(from_location) # FIXME: If there's a trailing slash, keep removing them # until we find the right bit try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) from branch import find_branch, DivergedBranches try: br_from = find_branch(from_location) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not exist.' % to_location) else: raise from_location = pull_loc(br_from) br_to.update_revisions(br_from) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False) br_to.controlfile("x-pull", "wb").write(from_location + "\n") def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest if selftest(): return 0 else: return 1 class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :641 committer Martin Pool 1118370074 +1000 data 43 - improved external-command patch from john from :640 M 644 inline bzrlib/commands.py data 46524 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _find_plugins(): """Find all python files which are plugins, and load their commands to add to the list of "all commands" The environment variable BZRPATH is considered a delimited set of paths to look through. Each entry is searched for *.py files. If a directory is found, it is also searched, but they are not searched recursively. This allows you to revctl the plugins. Inside the plugin should be a series of cmd_* function, which inherit from the bzrlib.commands.Command class. """ bzrpath = os.environ.get('BZRPLUGINPATH', '') plugin_cmds = {} if not bzrpath: return plugin_cmds _platform_extensions = { 'win32':'.pyd', 'cygwin':'.dll', 'darwin':'.dylib', 'linux2':'.so' } if _platform_extensions.has_key(sys.platform): platform_extension = _platform_extensions[sys.platform] else: platform_extension = None for d in bzrpath.split(os.pathsep): plugin_names = {} # This should really be a set rather than a dict for f in os.listdir(d): if f.endswith('.py'): f = f[:-3] elif f.endswith('.pyc') or f.endswith('.pyo'): f = f[:-4] elif platform_extension and f.endswith(platform_extension): f = f[:-len(platform_extension)] if f.endswidth('module'): f = f[:-len('module')] else: continue if not plugin_names.has_key(f): plugin_names[f] = True plugin_names = plugin_names.keys() plugin_names.sort() try: sys.path.insert(0, d) for name in plugin_names: try: old_module = None try: if sys.modules.has_key(name): old_module = sys.modules[name] del sys.modules[name] plugin = __import__(name, locals()) for k in dir(plugin): if k.startswith('cmd_'): k_unsquished = _unsquish_command_name(k) if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = getattr(plugin, k) else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r in dir %r' % (name, d)) finally: if old_module: sys.modules[name] = old_module except ImportError, e: log_error('Unable to load plugin: %r from %r\n%s' % (name, d, e)) finally: sys.path.pop(0) return plugin_cmds def _get_cmd_dict(include_plugins=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v if include_plugins: d.update(_find_plugins()) return d def get_all_cmds(include_plugins=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(include_plugins=include_plugins).iteritems(): yield k,v def get_cmd_class(cmd,include_plugins=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(include_plugins=include_plugins) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import errno br_to = Branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if errno == errno.ENOENT: raise if location is None: location = stored_loc if location is None: raise BzrCommandError("No pull location known or specified.") from branch import find_branch, DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged. Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. """ takes_args = ['from_location', 'to_location?'] def run(self, from_location, to_location=None): import errno from bzrlib.merge import merge if to_location is None: to_location = os.path.basename(from_location) # FIXME: If there's a trailing slash, keep removing them # until we find the right bit try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) from branch import find_branch, DivergedBranches try: br_from = find_branch(from_location) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not exist.' % to_location) else: raise from_location = pull_loc(br_from) br_to.update_revisions(br_from) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False) br_to.controlfile("x-pull", "wb").write(from_location + "\n") def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest if selftest(): return 0 else: return 1 class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] include_plugins=True try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd,include_plugins=include_plugins) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline testbzr data 14816 #! /usr/bin/python # -*- coding: utf-8 -*- # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. usage: testbzr [-p PYTHON] [BZR] By default this tests the copy of bzr found in the same directory as testbzr, or the first one found on the $PATH. A copy of bzr may be given on the command line to override this, for example when applying a new test suite to an old copy of bzr or vice versa. testbzr normally invokes bzr using the same version of python as it would normally use to run -- that is, the system default python, unless that is older than 2.3. The -p option allows specification of a different Python interpreter, such as when testing that bzr still works on python2.3. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" # we always invoke bzr as 'python bzr' (or e.g. 'python2.3 bzr') # partly so as to cope if the bzr binary is not marked executable OVERRIDE_PYTHON = 'python' LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) class CommandFailed(Exception): pass def formcmd(cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = BZRPATH if OVERRIDE_PYTHON: cmd.insert(0, OVERRIDE_PYTHON) logfile.write('$ %r\n' % cmd) return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) start_dir = os.getcwd() logfile = open(LOGFILENAME, 'wt', buffering=1) def test_plugins(): """Run a test involving creating a plugin to load, and making sure it is seen properly. """ mkdir('plugin_test') f = open(os.path.join('plugin_test', 'myplug.py'), 'wb') f.write("""import bzrlib, bzrlib.commands class cmd_myplug(bzrlib.commands.Command): '''Just a simple test plugin.''' aliases = ['mplg'] def run(self): print 'Hello from my plugin' """) f.close() os.environ['BZRPLUGINPATH'] = os.path.abspath('plugin_test') help = backtick('bzr help commands') assert help.find('myplug') != -1 assert help.find('Just a simple test plugin.') != -1 assert backtick('bzr myplug') == 'Hello from my plugin\n' assert backtick('bzr mplg') == 'Hello from my plugin\n' f = open(os.path.join('plugin_test', 'override.py'), 'wb') f.write("""import bzrlib, bzrlib.commands class cmd_commit(bzrlib.commands.cmd_commit): '''Commit changes into a new revision.''' def run(self, *args, **kwargs): print "I'm sorry dave, you can't do that" class cmd_help(bzrlib.commands.cmd_help): '''Show help on a command or other topic.''' def run(self, *args, **kwargs): print "You have been overridden" bzrlib.commands.cmd_help.run(self, *args, **kwargs) """) f.close() newhelp = backtick('bzr help commands') assert newhelp.startswith('You have been overridden\n') # We added a line, but the rest should work assert newhelp[25:] == help assert backtick('bzr commit -m test') == "I'm sorry dave, you can't do that\n" shutil.rmtree('plugin_test') try: from getopt import getopt opts, args = getopt(sys.argv[1:], 'p:') for option, value in opts: if option == '-p': OVERRIDE_PYTHON = value mypath = os.path.abspath(sys.argv[0]) print '%-30s %s' % ('running tests from', mypath) global BZRPATH if args: BZRPATH = args[0] else: BZRPATH = os.path.join(os.path.split(mypath)[0], 'bzr') print '%-30s %s' % ('against bzr', BZRPATH) print '%-30s %s' % ('in directory', os.getcwd()) print '%-30s %s' % ('with python', (OVERRIDE_PYTHON or '(default)')) print print backtick('bzr version') runcmd(['mkdir', TESTDIR]) cd(TESTDIR) test_root = os.getcwd() progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("internal tests") runcmd("bzr selftest") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) runcmd("bzr diff --message foo", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == 'unknown:\n test.txt\n' out = backtick("bzr status --all") assert out == "unknown:\n test.txt\n" out = backtick("bzr status test.txt --all") assert out == "unknown:\n test.txt\n" f = file('test2.txt', 'wt') f.write('goodbye cruel world...\n') f.close() out = backtick("bzr status test.txt") assert out == "unknown:\n test.txt\n" out = backtick("bzr status") assert out == ("unknown:\n" " test.txt\n" " test2.txt\n") os.unlink('test2.txt') progress("command aliases") out = backtick("bzr st --all") assert out == ("unknown:\n" " test.txt\n") out = backtick("bzr stat") assert out == ("unknown:\n" " test.txt\n") progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == ("added:\n" " test.txt\n") progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == os.path.join("sub2", "hello.txt\n") assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') assert backtick('bzr relpath sub2/hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') runcmd('bzr log') runcmd('bzr log -v') progress("file with spaces in name") mkdir('sub directory') file('sub directory/file with spaces ', 'wt').write('see how this works\n') runcmd('bzr add .') runcmd('bzr diff') runcmd('bzr commit -m add-spaces') runcmd('bzr check') runcmd('bzr log') runcmd('bzr log --forward') runcmd('bzr info') cd('..') cd('..') progress('branch') # Can't create a branch if it already exists runcmd('bzr branch branch1', retcode=1) # Can't create a branch if its parent doesn't exist runcmd('bzr branch /unlikely/to/exist', retcode=1) runcmd('bzr branch branch1 branch2') progress("pull") cd('branch1') runcmd('bzr pull', retcode=1) runcmd('bzr pull ../branch2') cd('.bzr') runcmd('bzr pull') runcmd('bzr commit -m empty') runcmd('bzr pull') cd('../../branch2') runcmd('bzr pull') runcmd('bzr commit -m empty') cd('../branch1') runcmd('bzr commit -m empty') runcmd('bzr pull', retcode=1) cd ('..') progress('status after remove') mkdir('status-after-remove') # see mail from William Dodé, 2005-05-25 # $ bzr init; touch a; bzr add a; bzr commit -m "add a" # * looking for changes... # added a # * commited r1 # $ bzr remove a # $ bzr status # bzr: local variable 'kind' referenced before assignment # at /vrac/python/bazaar-ng/bzrlib/diff.py:286 in compare_trees() # see ~/.bzr.log for debug information cd('status-after-remove') runcmd('bzr init') file('a', 'w').write('foo') runcmd('bzr add a') runcmd(['bzr', 'commit', '-m', 'add a']) runcmd('bzr remove a') runcmd('bzr status') cd('..') progress('ignore patterns') mkdir('ignorebranch') cd('ignorebranch') runcmd('bzr init') assert backtick('bzr unknowns') == '' file('foo.tmp', 'wt').write('tmp files are ignored') assert backtick('bzr unknowns') == '' file('foo.c', 'wt').write('int main() {}') assert backtick('bzr unknowns') == 'foo.c\n' runcmd('bzr add foo.c') assert backtick('bzr unknowns') == '' # 'ignore' works when creating the .bzignore file file('foo.blah', 'wt').write('blah') assert backtick('bzr unknowns') == 'foo.blah\n' runcmd('bzr ignore *.blah') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\n' # 'ignore' works when then .bzrignore file already exists file('garh', 'wt').write('garh') assert backtick('bzr unknowns') == 'garh\n' runcmd('bzr ignore garh') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\ngarh\n' cd('..') progress("recursive and non-recursive add") mkdir('no-recurse') cd('no-recurse') runcmd('bzr init') mkdir('foo') fp = os.path.join('foo', 'test.txt') f = file(fp, 'w') f.write('hello!\n') f.close() runcmd('bzr add --no-recurse foo') runcmd('bzr file-id foo') runcmd('bzr file-id ' + fp, 1) # not versioned yet runcmd('bzr commit -m add-dir-only') runcmd('bzr file-id ' + fp, 1) # still not versioned runcmd('bzr add foo') runcmd('bzr file-id ' + fp) runcmd('bzr commit -m add-sub-file') cd('..') # Run any function in this g = globals() funcs = g.keys() funcs.sort() for k in funcs: if k.startswith('test_') and callable(g[k]): progress(k[5:].replace('_', ' ')) g[k]() progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) logfile.close() sys.stdout.writelines(file(os.path.join(start_dir, LOGFILENAME), 'rt').readlines()[-50:]) sys.exit(1) commit refs/heads/master mark :642 committer Martin Pool 1118371266 +1000 data 30 - notes on patches for Windows from :641 M 644 inline TODO data 12675 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * ``bzr log DIR`` should give changes to any files within DIR. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. Perhaps ~/.bzr/http-cache. Baz has a fairly simple cache under ~/.arch-cache, containing revision information encoded almost as a bunch of archives. Perhaps we could simply store full paths. * Maybe also store directories in the statcache so that we can quickly identify that they still exist. * Diff should show timestamps; for files from the working directory we can use the file itself; for files from a revision we should use the commit time of the revision. * Perhaps split command infrastructure from the actual command definitions. * Cleaner support for negative boolean options like --no-recurse. * Statcache should possibly map all file paths to / separators * quotefn doubles all backslashes on Windows; this is probably not the best thing to do. What would be a better way to safely represent filenames? Perhaps we could doublequote things containing spaces, on the principle that filenames containing quotes are unlikely? Nice for humans; less good for machine parsing. * Patches should probably use only forward slashes, even on Windows, otherwise Unix patch can't apply them. (?) Medium things ------------- * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. We should be able to just get the id for the selected files, look up their location and diff just those files. No need to traverse the entire inventories. * ``bzr status DIR`` or ``bzr diff DIR`` should report on all changes under that directory. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from ElementTree to an object when it is read in, but rather wait until the program actually wants to know about that node. * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. - Selected-file commit - Impossible selected-file commit: adding things in non-versioned directories, crossing renames, etc. * Write a reproducible benchmark, perhaps importing various kernel versions. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Paranoid mode where we never trust SHA-1 matches. * Don't commit if there are no changes unless forced. * --dry-run mode for commit? (Or maybe just run with check-command=false?) * Generally, be a bit more verbose unless --silent is specified. * Function that finds all changes to files under a given directory; perhaps log should use this if a directory is given. * XML attributes might have trouble with filenames containing \n and \r. Do we really want to support this? I think perhaps not. * Remember execute bits, so that exports will work OK. * Unify smart_add and plain Branch.add(); perhaps smart_add should just build a list of files to add and pass that to the regular add function. * Function to list a directory, saying in which revision each file was last modified. Useful for web and gui interfaces, and slow to compute one file at a time. * unittest is standard, but the results are kind of ugly; would be nice to make it cleaner. * Check locking is correct during merge-related operations. * Perhaps attempts to get locks should timeout after some period of time, or at least display a progress message. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. Possibly this should be done by splitting the commit function into several parts (under a single interface). It is already rather large. Decomposition: - find tree modifications and prepare in-memory inventory - export that inventory to a temporary directory - run the test in that temporary directory - if that succeeded, continue to actually finish the commit What should be done with the text of modified files while this is underway? I don't think we want to count on holding them in memory and we can't trust the working files to stay in one place so I suppose we need to move them into the text store, or otherwise into a temporary directory. If the commit does not actually complete, we would rather the content was not left behind in the stores. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :643 committer Martin Pool 1118384531 +1000 data 68 - fix redirection of messages to file in diff (from Johan Rydberg) from :642 M 644 inline bzrlib/diff.py data 14164 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from trace import mutter from errors import BzrError def internal_diff(old_label, oldlines, new_label, newlines, to_file): import difflib # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, fromfile=old_label, tofile=new_label) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') to_file.writelines(ud) if nonl: print >>to_file, "\\ No newline at end of file" print >>to_file def external_diff(old_label, oldlines, new_label, newlines, to_file, diff_opts): """Display a diff by calling out to the external diff program.""" import sys if to_file != sys.stdout: raise NotImplementedError("sorry, can't send external diff other than to stdout yet", to_file) # make sure our own output is properly ordered before the diff to_file.flush() from tempfile import NamedTemporaryFile import os oldtmpf = NamedTemporaryFile() newtmpf = NamedTemporaryFile() try: # TODO: perhaps a special case for comparing to or from the empty # sequence; can just use /dev/null on Unix # TODO: if either of the files being compared already exists as a # regular named file (e.g. in the working directory) then we can # compare directly to that, rather than copying it. oldtmpf.writelines(oldlines) newtmpf.writelines(newlines) oldtmpf.flush() newtmpf.flush() if not diff_opts: diff_opts = [] diffcmd = ['diff', '--label', old_label, oldtmpf.name, '--label', new_label, newtmpf.name] # diff only allows one style to be specified; they don't override. # note that some of these take optargs, and the optargs can be # directly appended to the options. # this is only an approximate parser; it doesn't properly understand # the grammar. for s in ['-c', '-u', '-C', '-U', '-e', '--ed', '-q', '--brief', '--normal', '-n', '--rcs', '-y', '--side-by-side', '-D', '--ifdef']: for j in diff_opts: if j.startswith(s): break else: continue break else: diffcmd.append('-u') if diff_opts: diffcmd.extend(diff_opts) rc = os.spawnvp(os.P_WAIT, 'diff', diffcmd) if rc != 0 and rc != 1: # returns 1 if files differ; that's OK if rc < 0: msg = 'signal %d' % (-rc) else: msg = 'exit code %d' % rc raise BzrError('external diff failed with %s; command: %r' % (rc, diffcmd)) finally: oldtmpf.close() # and delete newtmpf.close() def show_diff(b, revision, specific_files, external_diff_options=None): """Shortcut for showing the diff to the working tree. b Branch. revision None for each, or otherwise the old revision to compare against. The more general form is show_diff_trees(), where the caller supplies any two trees. """ import sys if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() show_diff_trees(old_tree, new_tree, sys.stdout, specific_files, external_diff_options) def show_diff_trees(old_tree, new_tree, to_file, specific_files=None, external_diff_options=None): """Show in text form the changes from one tree to another. to_files If set, include only changes to these files. external_diff_options If set, use an external GNU diff and pass these options. """ # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. if external_diff_options: assert isinstance(external_diff_options, basestring) opts = external_diff_options.split() def diff_file(olab, olines, nlab, nlines, to_file): external_diff(olab, olines, nlab, nlines, to_file, opts) else: diff_file = internal_diff delta = compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=specific_files) for path, file_id, kind in delta.removed: print >>to_file, '*** removed %s %r' % (kind, path) if kind == 'file': diff_file(old_label + path, old_tree.get_file(file_id).readlines(), DEVNULL, [], to_file) for path, file_id, kind in delta.added: print >>to_file, '*** added %s %r' % (kind, path) if kind == 'file': diff_file(DEVNULL, [], new_label + path, new_tree.get_file(file_id).readlines(), to_file) for old_path, new_path, file_id, kind, text_modified in delta.renamed: print >>to_file, '*** renamed %s %r => %r' % (kind, old_path, new_path) if text_modified: diff_file(old_label + old_path, old_tree.get_file(file_id).readlines(), new_label + new_path, new_tree.get_file(file_id).readlines(), to_file) for path, file_id, kind in delta.modified: print >>to_file, '*** modified %s %r' % (kind, path) if kind == 'file': diff_file(old_label + path, old_tree.get_file(file_id).readlines(), new_label + path, new_tree.get_file(file_id).readlines(), to_file) class TreeDelta(object): """Describes changes from one tree to another. Contains four lists: added (path, id, kind) removed (path, id, kind) renamed (oldpath, newpath, id, kind, text_modified) modified (path, id, kind) unchanged (path, id, kind) Each id is listed only once. Files that are both modified and renamed are listed only in renamed, with the text_modified flag true. The lists are normally sorted when the delta is created. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] self.unchanged = [] def __repr__(self): return "TreeDelta(added=%r, removed=%r, renamed=%r, modified=%r," \ " unchanged=%r)" % (self.added, self.removed, self.renamed, self.modified, self.unchanged) def has_changed(self): changes = len(self.added) + len(self.removed) + len(self.renamed) changes += len(self.modified) return (changes != 0) def touches_file_id(self, file_id): """Return True if file_id is modified by this delta.""" for l in self.added, self.removed, self.modified: for v in l: if v[1] == file_id: return True for v in self.renamed: if v[2] == file_id: return True return False def show(self, to_file, show_ids=False, show_unchanged=False): def show_list(files): for path, fid, kind in files: if kind == 'directory': path += '/' elif kind == 'symlink': path += '@' if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if self.removed: print >>to_file, 'removed:' show_list(self.removed) if self.added: print >>to_file, 'added:' show_list(self.added) if self.renamed: print >>to_file, 'renamed:' for oldpath, newpath, fid, kind, text_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if self.modified: print >>to_file, 'modified:' show_list(self.modified) if show_unchanged and self.unchanged: print >>to_file, 'unchanged:' show_list(self.unchanged) def compare_trees(old_tree, new_tree, want_unchanged, specific_files=None): """Describe changes from one tree to another. Returns a TreeDelta with details of added, modified, renamed, and deleted entries. The root entry is specifically exempt. This only considers versioned files. want_unchanged If true, also list files unchanged from one version to the next. specific_files If true, only check for changes to specified names or files within them. """ from osutils import is_inside_any old_inv = old_tree.inventory new_inv = new_tree.inventory delta = TreeDelta() mutter('start compare_trees') # TODO: match for specific files can be rather smarter by finding # the IDs of those files up front and then considering only that. for file_id in old_tree: if file_id in new_tree: kind = old_inv.get_file_kind(file_id) assert kind == new_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ 'invalid file kind %r' % kind if kind == 'root_directory': continue old_path = old_inv.id2path(file_id) new_path = new_inv.id2path(file_id) if specific_files: if (not is_inside_any(specific_files, old_path) and not is_inside_any(specific_files, new_path)): continue if kind == 'file': old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False # TODO: Can possibly avoid calculating path strings if the # two files are unchanged and their names and parents are # the same and the parents are unchanged all the way up. # May not be worthwhile. if old_path != new_path: delta.renamed.append((old_path, new_path, file_id, kind, text_modified)) elif text_modified: delta.modified.append((new_path, file_id, kind)) elif want_unchanged: delta.unchanged.append((new_path, file_id, kind)) else: kind = old_inv.get_file_kind(file_id) old_path = old_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, old_path): continue delta.removed.append((old_path, file_id, kind)) mutter('start looking for new files') for file_id in new_inv: if file_id in old_inv: continue new_path = new_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, new_path): continue kind = new_inv.get_file_kind(file_id) delta.added.append((new_path, file_id, kind)) delta.removed.sort() delta.added.sort() delta.renamed.sort() delta.modified.sort() delta.unchanged.sort() return delta commit refs/heads/master mark :644 committer Martin Pool 1118384791 +1000 data 40 - add aaron's pending patch for annotate from :643 M 644 inline patches/annotate3.patch data 272300 *** added file 'bzrlib/patches.py' --- /dev/null +++ bzrlib/patches.py @@ -0,0 +1,497 @@ +# Copyright (C) 2004, 2005 Aaron Bentley +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +import sys +import progress +class PatchSyntax(Exception): + def __init__(self, msg): + Exception.__init__(self, msg) + + +class MalformedPatchHeader(PatchSyntax): + def __init__(self, desc, line): + self.desc = desc + self.line = line + msg = "Malformed patch header. %s\n%s" % (self.desc, self.line) + PatchSyntax.__init__(self, msg) + +class MalformedHunkHeader(PatchSyntax): + def __init__(self, desc, line): + self.desc = desc + self.line = line + msg = "Malformed hunk header. %s\n%s" % (self.desc, self.line) + PatchSyntax.__init__(self, msg) + +class MalformedLine(PatchSyntax): + def __init__(self, desc, line): + self.desc = desc + self.line = line + msg = "Malformed line. %s\n%s" % (self.desc, self.line) + PatchSyntax.__init__(self, msg) + +def get_patch_names(iter_lines): + try: + line = iter_lines.next() + if not line.startswith("--- "): + raise MalformedPatchHeader("No orig name", line) + else: + orig_name = line[4:].rstrip("\n") + except StopIteration: + raise MalformedPatchHeader("No orig line", "") + try: + line = iter_lines.next() + if not line.startswith("+++ "): + raise PatchSyntax("No mod name") + else: + mod_name = line[4:].rstrip("\n") + except StopIteration: + raise MalformedPatchHeader("No mod line", "") + return (orig_name, mod_name) + +def parse_range(textrange): + """Parse a patch range, handling the "1" special-case + + :param textrange: The text to parse + :type textrange: str + :return: the position and range, as a tuple + :rtype: (int, int) + """ + tmp = textrange.split(',') + if len(tmp) == 1: + pos = tmp[0] + range = "1" + else: + (pos, range) = tmp + pos = int(pos) + range = int(range) + return (pos, range) + + +def hunk_from_header(line): + if not line.startswith("@@") or not line.endswith("@@\n") \ + or not len(line) > 4: + raise MalformedHunkHeader("Does not start and end with @@.", line) + try: + (orig, mod) = line[3:-4].split(" ") + except Exception, e: + raise MalformedHunkHeader(str(e), line) + if not orig.startswith('-') or not mod.startswith('+'): + raise MalformedHunkHeader("Positions don't start with + or -.", line) + try: + (orig_pos, orig_range) = parse_range(orig[1:]) + (mod_pos, mod_range) = parse_range(mod[1:]) + except Exception, e: + raise MalformedHunkHeader(str(e), line) + if mod_range < 0 or orig_range < 0: + raise MalformedHunkHeader("Hunk range is negative", line) + return Hunk(orig_pos, orig_range, mod_pos, mod_range) + + +class HunkLine: + def __init__(self, contents): + self.contents = contents + + def get_str(self, leadchar): + if self.contents == "\n" and leadchar == " " and False: + return "\n" + return leadchar + self.contents + +class ContextLine(HunkLine): + def __init__(self, contents): + HunkLine.__init__(self, contents) + + def __str__(self): + return self.get_str(" ") + + +class InsertLine(HunkLine): + def __init__(self, contents): + HunkLine.__init__(self, contents) + + def __str__(self): + return self.get_str("+") + + +class RemoveLine(HunkLine): + def __init__(self, contents): + HunkLine.__init__(self, contents) + + def __str__(self): + return self.get_str("-") + +__pychecker__="no-returnvalues" +def parse_line(line): + if line.startswith("\n"): + return ContextLine(line) + elif line.startswith(" "): + return ContextLine(line[1:]) + elif line.startswith("+"): + return InsertLine(line[1:]) + elif line.startswith("-"): + return RemoveLine(line[1:]) + else: + raise MalformedLine("Unknown line type", line) +__pychecker__="" + + +class Hunk: + def __init__(self, orig_pos, orig_range, mod_pos, mod_range): + self.orig_pos = orig_pos + self.orig_range = orig_range + self.mod_pos = mod_pos + self.mod_range = mod_range + self.lines = [] + + def get_header(self): + return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos, + self.orig_range), + self.range_str(self.mod_pos, + self.mod_range)) + + def range_str(self, pos, range): + """Return a file range, special-casing for 1-line files. + + :param pos: The position in the file + :type pos: int + :range: The range in the file + :type range: int + :return: a string in the format 1,4 except when range == pos == 1 + """ + if range == 1: + return "%i" % pos + else: + return "%i,%i" % (pos, range) + + def __str__(self): + lines = [self.get_header()] + for line in self.lines: + lines.append(str(line)) + return "".join(lines) + + def shift_to_mod(self, pos): + if pos < self.orig_pos-1: + return 0 + elif pos > self.orig_pos+self.orig_range: + return self.mod_range - self.orig_range + else: + return self.shift_to_mod_lines(pos) + + def shift_to_mod_lines(self, pos): + assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range) + position = self.orig_pos-1 + shift = 0 + for line in self.lines: + if isinstance(line, InsertLine): + shift += 1 + elif isinstance(line, RemoveLine): + if position == pos: + return None + shift -= 1 + position += 1 + elif isinstance(line, ContextLine): + position += 1 + if position > pos: + break + return shift + +def iter_hunks(iter_lines): + hunk = None + for line in iter_lines: + if line.startswith("@@"): + if hunk is not None: + yield hunk + hunk = hunk_from_header(line) + else: + hunk.lines.append(parse_line(line)) + + if hunk is not None: + yield hunk + +class Patch: + def __init__(self, oldname, newname): + self.oldname = oldname + self.newname = newname + self.hunks = [] + + def __str__(self): + ret = "--- %s\n+++ %s\n" % (self.oldname, self.newname) + ret += "".join([str(h) for h in self.hunks]) + return ret + + def stats_str(self): + """Return a string of patch statistics""" + removes = 0 + inserts = 0 + for hunk in self.hunks: + for line in hunk.lines: + if isinstance(line, InsertLine): + inserts+=1; + elif isinstance(line, RemoveLine): + removes+=1; + return "%i inserts, %i removes in %i hunks" % \ + (inserts, removes, len(self.hunks)) + + def pos_in_mod(self, position): + newpos = position + for hunk in self.hunks: + shift = hunk.shift_to_mod(position) + if shift is None: + return None + newpos += shift + return newpos + + def iter_inserted(self): + """Iteraties through inserted lines + + :return: Pair of line number, line + :rtype: iterator of (int, InsertLine) + """ + for hunk in self.hunks: + pos = hunk.mod_pos - 1; + for line in hunk.lines: + if isinstance(line, InsertLine): + yield (pos, line) + pos += 1 + if isinstance(line, ContextLine): + pos += 1 + +def parse_patch(iter_lines): + (orig_name, mod_name) = get_patch_names(iter_lines) + patch = Patch(orig_name, mod_name) + for hunk in iter_hunks(iter_lines): + patch.hunks.append(hunk) + return patch + + +class AnnotateLine: + """A line associated with the log that produced it""" + def __init__(self, text, log=None): + self.text = text + self.log = log + +class CantGetRevisionData(Exception): + def __init__(self, revision): + Exception.__init__(self, "Can't get data for revision %s" % revision) + +def annotate_file2(file_lines, anno_iter): + for result in iter_annotate_file(file_lines, anno_iter): + pass + return result + + +def iter_annotate_file(file_lines, anno_iter): + lines = [AnnotateLine(f) for f in file_lines] + patches = [] + try: + for result in anno_iter: + if isinstance(result, progress.Progress): + yield result + continue + log, iter_inserted, patch = result + for (num, line) in iter_inserted: + old_num = num + + for cur_patch in patches: + num = cur_patch.pos_in_mod(num) + if num == None: + break + + if num >= len(lines): + continue + if num is not None and lines[num].log is None: + lines[num].log = log + patches=[patch]+patches + except CantGetRevisionData: + pass + yield lines + + +def difference_index(atext, btext): + """Find the indext of the first character that differs betweeen two texts + + :param atext: The first text + :type atext: str + :param btext: The second text + :type str: str + :return: The index, or None if there are no differences within the range + :rtype: int or NoneType + """ + length = len(atext) + if len(btext) < length: + length = len(btext) + for i in range(length): + if atext[i] != btext[i]: + return i; + return None + + +def test(): + import unittest + class PatchesTester(unittest.TestCase): + def testValidPatchHeader(self): + """Parse a valid patch header""" + lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n') + (orig, mod) = get_patch_names(lines.__iter__()) + assert(orig == "orig/commands.py") + assert(mod == "mod/dommands.py") + + def testInvalidPatchHeader(self): + """Parse an invalid patch header""" + lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n') + self.assertRaises(MalformedPatchHeader, get_patch_names, + lines.__iter__()) + + def testValidHunkHeader(self): + """Parse a valid hunk header""" + header = "@@ -34,11 +50,6 @@\n" + hunk = hunk_from_header(header); + assert (hunk.orig_pos == 34) + assert (hunk.orig_range == 11) + assert (hunk.mod_pos == 50) + assert (hunk.mod_range == 6) + assert (str(hunk) == header) + + def testValidHunkHeader2(self): + """Parse a tricky, valid hunk header""" + header = "@@ -1 +0,0 @@\n" + hunk = hunk_from_header(header); + assert (hunk.orig_pos == 1) + assert (hunk.orig_range == 1) + assert (hunk.mod_pos == 0) + assert (hunk.mod_range == 0) + assert (str(hunk) == header) + + def makeMalformed(self, header): + self.assertRaises(MalformedHunkHeader, hunk_from_header, header) + + def testInvalidHeader(self): + """Parse an invalid hunk header""" + self.makeMalformed(" -34,11 +50,6 \n") + self.makeMalformed("@@ +50,6 -34,11 @@\n") + self.makeMalformed("@@ -34,11 +50,6 @@") + self.makeMalformed("@@ -34.5,11 +50,6 @@\n") + self.makeMalformed("@@-34,11 +50,6@@\n") + self.makeMalformed("@@ 34,11 50,6 @@\n") + self.makeMalformed("@@ -34,11 @@\n") + self.makeMalformed("@@ -34,11 +50,6.5 @@\n") + self.makeMalformed("@@ -34,11 +50,-6 @@\n") + + def lineThing(self,text, type): + line = parse_line(text) + assert(isinstance(line, type)) + assert(str(line)==text) + + def makeMalformedLine(self, text): + self.assertRaises(MalformedLine, parse_line, text) + + def testValidLine(self): + """Parse a valid hunk line""" + self.lineThing(" hello\n", ContextLine) + self.lineThing("+hello\n", InsertLine) + self.lineThing("-hello\n", RemoveLine) + + def testMalformedLine(self): + """Parse invalid valid hunk lines""" + self.makeMalformedLine("hello\n") + + def compare_parsed(self, patchtext): + lines = patchtext.splitlines(True) + patch = parse_patch(lines.__iter__()) + pstr = str(patch) + i = difference_index(patchtext, pstr) + if i is not None: + print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i]) + assert (patchtext == str(patch)) + + def testAll(self): + """Test parsing a whole patch""" + patchtext = """--- orig/commands.py ++++ mod/commands.py +@@ -1337,7 +1337,8 @@ + + def set_title(self, command=None): + try: +- version = self.tree.tree_version.nonarch ++ version = pylon.alias_or_version(self.tree.tree_version, self.tree, ++ full=False) + except: + version = "[no version]" + if command is None: +@@ -1983,7 +1984,11 @@ + version) + if len(new_merges) > 0: + if cmdutil.prompt("Log for merge"): +- mergestuff = cmdutil.log_for_merge(tree, comp_version) ++ if cmdutil.prompt("changelog for merge"): ++ mergestuff = "Patches applied:\\n" ++ mergestuff += pylon.changelog_for_merge(new_merges) ++ else: ++ mergestuff = cmdutil.log_for_merge(tree, comp_version) + log.description += mergestuff + log.save() + try: +""" + self.compare_parsed(patchtext) + + def testInit(self): + """Handle patches missing half the position, range tuple""" + patchtext = \ +"""--- orig/__init__.py ++++ mod/__init__.py +@@ -1 +1,2 @@ + __docformat__ = "restructuredtext en" ++__doc__ = An alternate Arch commandline interface""" + self.compare_parsed(patchtext) + + + + def testLineLookup(self): + """Make sure we can accurately look up mod line from orig""" + patch = parse_patch(open("testdata/diff")) + orig = list(open("testdata/orig")) + mod = list(open("testdata/mod")) + removals = [] + for i in range(len(orig)): + mod_pos = patch.pos_in_mod(i) + if mod_pos is None: + removals.append(orig[i]) + continue + assert(mod[mod_pos]==orig[i]) + rem_iter = removals.__iter__() + for hunk in patch.hunks: + for line in hunk.lines: + if isinstance(line, RemoveLine): + next = rem_iter.next() + if line.contents != next: + sys.stdout.write(" orig:%spatch:%s" % (next, + line.contents)) + assert(line.contents == next) + self.assertRaises(StopIteration, rem_iter.next) + + def testFirstLineRenumber(self): + """Make sure we handle lines at the beginning of the hunk""" + patch = parse_patch(open("testdata/insert_top.patch")) + assert (patch.pos_in_mod(0)==1) + + + patchesTestSuite = unittest.makeSuite(PatchesTester,'test') + runner = unittest.TextTestRunner(verbosity=0) + return runner.run(patchesTestSuite) + + +if __name__ == "__main__": + test() +# arch-tag: d1541a25-eac5-4de9-a476-08a7cecd5683 *** added file 'bzrlib/progress.py' --- /dev/null +++ bzrlib/progress.py @@ -0,0 +1,91 @@ +# Copyright (C) 2005 Aaron Bentley +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import sys + +class Progress(object): + def __init__(self, units, current, total=None): + self.units = units + self.current = current + self.total = total + self.percent = None + if self.total is not None: + self.percent = 100.0 * current / total + + def __str__(self): + if self.total is not None: + return "%i of %i %s %.1f%%" % (self.current, self.total, self.units, + self.percent) + else: + return "%i %s" (self.current, self.units) + + +def progress_bar(progress): + fmt = " %i of %i %s (%.1f%%)" + f = fmt % (progress.total, progress.total, progress.units, 100.0) + max = len(f) + cols = 77 - max + markers = int (float(cols) * progress.current / progress.total) + txt = fmt % (progress.current, progress.total, progress.units, + progress.percent) + sys.stderr.write("\r[%s%s]%s" % ('='*markers, ' '*(cols-markers), txt)) + +def clear_progress_bar(): + sys.stderr.write('\r%s\r' % (' '*79)) + +def spinner_str(progress, show_text=False): + """ + Produces the string for a textual "spinner" progress indicator + :param progress: an object represinting current progress + :param show_text: If true, show progress text as well + :return: The spinner string + + >>> spinner_str(Progress("baloons", 0)) + '|' + >>> spinner_str(Progress("baloons", 5)) + '/' + >>> spinner_str(Progress("baloons", 6), show_text=True) + '- 6 baloons' + """ + positions = ('|', '/', '-', '\\') + text = positions[progress.current % 4] + if show_text: + text+=" %i %s" % (progress.current, progress.units) + return text + +def spinner(progress, show_text=False, output=sys.stderr): + """ + Update a spinner progress indicator on an output + :param progress: The progress to display + :param show_text: If true, show text as well as spinner + :param output: The output to write to + + >>> spinner(Progress("baloons", 6), show_text=True, output=sys.stdout) + \r- 6 baloons + """ + output.write('\r%s' % spinner_str(progress, show_text)) + +def run_tests(): + import doctest + result = doctest.testmod() + if result[1] > 0: + if result[0] == 0: + print "All tests passed" + else: + print "No tests to run" +if __name__ == "__main__": + run_tests() *** added directory 'testdata' *** added file 'testdata/diff' --- /dev/null +++ testdata/diff @@ -0,0 +1,1154 @@ +--- orig/commands.py ++++ mod/commands.py +@@ -19,25 +19,31 @@ + import arch + import arch.util + import arch.arch ++ ++import pylon.errors ++from pylon.errors import * ++from pylon import errors ++from pylon import util ++from pylon import arch_core ++from pylon import arch_compound ++from pylon import ancillary ++from pylon import misc ++from pylon import paths ++ + import abacmds + import cmdutil + import shutil + import os + import options +-import paths + import time + import cmd + import readline + import re + import string +-import arch_core +-from errors import * +-import errors + import terminal +-import ancillary +-import misc + import email + import smtplib ++import textwrap + + __docformat__ = "restructuredtext" + __doc__ = "Implementation of user (sub) commands" +@@ -257,7 +263,7 @@ + + tree=arch.tree_root() + if len(args) == 0: +- a_spec = cmdutil.comp_revision(tree) ++ a_spec = ancillary.comp_revision(tree) + else: + a_spec = cmdutil.determine_revision_tree(tree, args[0]) + cmdutil.ensure_archive_registered(a_spec.archive) +@@ -284,7 +290,7 @@ + changeset=options.changeset + tmpdir = None + else: +- tmpdir=cmdutil.tmpdir() ++ tmpdir=util.tmpdir() + changeset=tmpdir+"/changeset" + try: + delta=arch.iter_delta(a_spec, b_spec, changeset) +@@ -304,14 +310,14 @@ + if status > 1: + return + if (options.perform_diff): +- chan = cmdutil.ChangesetMunger(changeset) ++ chan = arch_compound.ChangesetMunger(changeset) + chan.read_indices() +- if isinstance(b_spec, arch.Revision): +- b_dir = b_spec.library_find() +- else: +- b_dir = b_spec +- a_dir = a_spec.library_find() + if options.diffopts is not None: ++ if isinstance(b_spec, arch.Revision): ++ b_dir = b_spec.library_find() ++ else: ++ b_dir = b_spec ++ a_dir = a_spec.library_find() + diffopts = options.diffopts.split() + cmdutil.show_custom_diffs(chan, diffopts, a_dir, b_dir) + else: +@@ -517,7 +523,7 @@ + except arch.errors.TreeRootError, e: + print e + return +- from_revision=cmdutil.tree_latest(tree) ++ from_revision = arch_compound.tree_latest(tree) + if from_revision==to_revision: + print "Tree is already up to date with:\n"+str(to_revision)+"." + return +@@ -592,6 +598,9 @@ + + if len(args) == 0: + args = None ++ if options.version is None: ++ return options, tree.tree_version, args ++ + revision=cmdutil.determine_revision_arch(tree, options.version) + return options, revision.get_version(), args + +@@ -601,11 +610,16 @@ + """ + tree=arch.tree_root() + options, version, files = self.parse_commandline(cmdargs, tree) ++ ancestor = None + if options.__dict__.has_key("base") and options.base: + base = cmdutil.determine_revision_tree(tree, options.base) ++ ancestor = base + else: +- base = cmdutil.submit_revision(tree) +- ++ base = ancillary.submit_revision(tree) ++ ancestor = base ++ if ancestor is None: ++ ancestor = arch_compound.tree_latest(tree, version) ++ + writeversion=version + archive=version.archive + source=cmdutil.get_mirror_source(archive) +@@ -625,18 +639,26 @@ + try: + last_revision=tree.iter_logs(version, True).next().revision + except StopIteration, e: +- if cmdutil.prompt("Import from commit"): +- return do_import(version) +- else: +- raise NoVersionLogs(version) +- if last_revision!=version.iter_revisions(True).next(): ++ last_revision = None ++ if ancestor is None: ++ if cmdutil.prompt("Import from commit"): ++ return do_import(version) ++ else: ++ raise NoVersionLogs(version) ++ try: ++ arch_last_revision = version.iter_revisions(True).next() ++ except StopIteration, e: ++ arch_last_revision = None ++ ++ if last_revision != arch_last_revision: ++ print "Tree is not up to date with %s" % str(version) + if not cmdutil.prompt("Out of date"): + raise OutOfDate + else: + allow_old=True + + try: +- if not cmdutil.has_changed(version): ++ if not cmdutil.has_changed(ancestor): + if not cmdutil.prompt("Empty commit"): + raise EmptyCommit + except arch.util.ExecProblem, e: +@@ -645,15 +667,15 @@ + raise MissingID(e) + else: + raise +- log = tree.log_message(create=False) ++ log = tree.log_message(create=False, version=version) + if log is None: + try: + if cmdutil.prompt("Create log"): +- edit_log(tree) ++ edit_log(tree, version) + + except cmdutil.NoEditorSpecified, e: + raise CommandFailed(e) +- log = tree.log_message(create=False) ++ log = tree.log_message(create=False, version=version) + if log is None: + raise NoLogMessage + if log["Summary"] is None or len(log["Summary"].strip()) == 0: +@@ -837,23 +859,24 @@ + if spec is not None: + revision = cmdutil.determine_revision_tree(tree, spec) + else: +- revision = cmdutil.comp_revision(tree) ++ revision = ancillary.comp_revision(tree) + except cmdutil.CantDetermineRevision, e: + raise CommandFailedWrapper(e) + munger = None + + if options.file_contents or options.file_perms or options.deletions\ + or options.additions or options.renames or options.hunk_prompt: +- munger = cmdutil.MungeOpts() +- munger.hunk_prompt = options.hunk_prompt ++ munger = arch_compound.MungeOpts() ++ munger.set_hunk_prompt(cmdutil.colorize, cmdutil.user_hunk_confirm, ++ options.hunk_prompt) + + if len(args) > 0 or options.logs or options.pattern_files or \ + options.control: + if munger is None: +- munger = cmdutil.MungeOpts(True) ++ munger = cmdutil.arch_compound.MungeOpts(True) + munger.all_types(True) + if len(args) > 0: +- t_cwd = cmdutil.tree_cwd(tree) ++ t_cwd = arch_compound.tree_cwd(tree) + for name in args: + if len(t_cwd) > 0: + t_cwd += "/" +@@ -878,7 +901,7 @@ + if options.pattern_files: + munger.add_keep_pattern(options.pattern_files) + +- for line in cmdutil.revert(tree, revision, munger, ++ for line in arch_compound.revert(tree, revision, munger, + not options.no_output): + cmdutil.colorize(line) + +@@ -1042,18 +1065,13 @@ + help_tree_spec() + return + +-def require_version_exists(version, spec): +- if not version.exists(): +- raise cmdutil.CantDetermineVersion(spec, +- "The version %s does not exist." \ +- % version) +- + class Revisions(BaseCommand): + """ + Print a revision name based on a revision specifier + """ + def __init__(self): + self.description="Lists revisions" ++ self.cl_revisions = [] + + def do_command(self, cmdargs): + """ +@@ -1066,224 +1084,68 @@ + self.tree = arch.tree_root() + except arch.errors.TreeRootError: + self.tree = None ++ if options.type == "default": ++ options.type = "archive" + try: +- iter = self.get_iterator(options.type, args, options.reverse, +- options.modified) ++ iter = cmdutil.revision_iterator(self.tree, options.type, args, ++ options.reverse, options.modified, ++ options.shallow) + except cmdutil.CantDetermineRevision, e: + raise CommandFailedWrapper(e) +- ++ except cmdutil.CantDetermineVersion, e: ++ raise CommandFailedWrapper(e) + if options.skip is not None: + iter = cmdutil.iter_skip(iter, int(options.skip)) + +- for revision in iter: +- log = None +- if isinstance(revision, arch.Patchlog): +- log = revision +- revision=revision.revision +- print options.display(revision) +- if log is None and (options.summary or options.creator or +- options.date or options.merges): +- log = revision.patchlog +- if options.creator: +- print " %s" % log.creator +- if options.date: +- print " %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date) +- if options.summary: +- print " %s" % log.summary +- if options.merges: +- showed_title = False +- for revision in log.merged_patches: +- if not showed_title: +- print " Merged:" +- showed_title = True +- print " %s" % revision +- +- def get_iterator(self, type, args, reverse, modified): +- if len(args) > 0: +- spec = args[0] +- else: +- spec = None +- if modified is not None: +- iter = cmdutil.modified_iter(modified, self.tree) +- if reverse: +- return iter +- else: +- return cmdutil.iter_reverse(iter) +- elif type == "archive": +- if spec is None: +- if self.tree is None: +- raise cmdutil.CantDetermineRevision("", +- "Not in a project tree") +- version = cmdutil.determine_version_tree(spec, self.tree) +- else: +- version = cmdutil.determine_version_arch(spec, self.tree) +- cmdutil.ensure_archive_registered(version.archive) +- require_version_exists(version, spec) +- return version.iter_revisions(reverse) +- elif type == "cacherevs": +- if spec is None: +- if self.tree is None: +- raise cmdutil.CantDetermineRevision("", +- "Not in a project tree") +- version = cmdutil.determine_version_tree(spec, self.tree) +- else: +- version = cmdutil.determine_version_arch(spec, self.tree) +- cmdutil.ensure_archive_registered(version.archive) +- require_version_exists(version, spec) +- return cmdutil.iter_cacherevs(version, reverse) +- elif type == "library": +- if spec is None: +- if self.tree is None: +- raise cmdutil.CantDetermineRevision("", +- "Not in a project tree") +- version = cmdutil.determine_version_tree(spec, self.tree) +- else: +- version = cmdutil.determine_version_arch(spec, self.tree) +- return version.iter_library_revisions(reverse) +- elif type == "logs": +- if self.tree is None: +- raise cmdutil.CantDetermineRevision("", "Not in a project tree") +- return self.tree.iter_logs(cmdutil.determine_version_tree(spec, \ +- self.tree), reverse) +- elif type == "missing" or type == "skip-present": +- if self.tree is None: +- raise cmdutil.CantDetermineRevision("", "Not in a project tree") +- skip = (type == "skip-present") +- version = cmdutil.determine_version_tree(spec, self.tree) +- cmdutil.ensure_archive_registered(version.archive) +- require_version_exists(version, spec) +- return cmdutil.iter_missing(self.tree, version, reverse, +- skip_present=skip) +- +- elif type == "present": +- if self.tree is None: +- raise cmdutil.CantDetermineRevision("", "Not in a project tree") +- version = cmdutil.determine_version_tree(spec, self.tree) +- cmdutil.ensure_archive_registered(version.archive) +- require_version_exists(version, spec) +- return cmdutil.iter_present(self.tree, version, reverse) +- +- elif type == "new-merges" or type == "direct-merges": +- if self.tree is None: +- raise cmdutil.CantDetermineRevision("", "Not in a project tree") +- version = cmdutil.determine_version_tree(spec, self.tree) +- cmdutil.ensure_archive_registered(version.archive) +- require_version_exists(version, spec) +- iter = cmdutil.iter_new_merges(self.tree, version, reverse) +- if type == "new-merges": +- return iter +- elif type == "direct-merges": +- return cmdutil.direct_merges(iter) +- +- elif type == "missing-from": +- if self.tree is None: +- raise cmdutil.CantDetermineRevision("", "Not in a project tree") +- revision = cmdutil.determine_revision_tree(self.tree, spec) +- libtree = cmdutil.find_or_make_local_revision(revision) +- return cmdutil.iter_missing(libtree, self.tree.tree_version, +- reverse) +- +- elif type == "partner-missing": +- return cmdutil.iter_partner_missing(self.tree, reverse) +- +- elif type == "ancestry": +- revision = cmdutil.determine_revision_tree(self.tree, spec) +- iter = cmdutil._iter_ancestry(self.tree, revision) +- if reverse: +- return iter +- else: +- return cmdutil.iter_reverse(iter) +- +- elif type == "dependencies" or type == "non-dependencies": +- nondeps = (type == "non-dependencies") +- revision = cmdutil.determine_revision_tree(self.tree, spec) +- anc_iter = cmdutil._iter_ancestry(self.tree, revision) +- iter_depends = cmdutil.iter_depends(anc_iter, nondeps) +- if reverse: +- return iter_depends +- else: +- return cmdutil.iter_reverse(iter_depends) +- elif type == "micro": +- return cmdutil.iter_micro(self.tree) +- +- ++ try: ++ for revision in iter: ++ log = None ++ if isinstance(revision, arch.Patchlog): ++ log = revision ++ revision=revision.revision ++ out = options.display(revision) ++ if out is not None: ++ print out ++ if log is None and (options.summary or options.creator or ++ options.date or options.merges): ++ log = revision.patchlog ++ if options.creator: ++ print " %s" % log.creator ++ if options.date: ++ print " %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date) ++ if options.summary: ++ print " %s" % log.summary ++ if options.merges: ++ showed_title = False ++ for revision in log.merged_patches: ++ if not showed_title: ++ print " Merged:" ++ showed_title = True ++ print " %s" % revision ++ if len(self.cl_revisions) > 0: ++ print pylon.changelog_for_merge(self.cl_revisions) ++ except pylon.errors.TreeRootNone: ++ raise CommandFailedWrapper( ++ Exception("This option can only be used in a project tree.")) ++ ++ def changelog_append(self, revision): ++ if isinstance(revision, arch.Revision): ++ revision=arch.Patchlog(revision) ++ self.cl_revisions.append(revision) ++ + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ +- parser=cmdutil.CmdOptionParser("fai revisions [revision]") ++ parser=cmdutil.CmdOptionParser("fai revisions [version/revision]") + select = cmdutil.OptionGroup(parser, "Selection options", + "Control which revisions are listed. These options" + " are mutually exclusive. If more than one is" + " specified, the last is used.") +- select.add_option("", "--archive", action="store_const", +- const="archive", dest="type", default="archive", +- help="List all revisions in the archive") +- select.add_option("", "--cacherevs", action="store_const", +- const="cacherevs", dest="type", +- help="List all revisions stored in the archive as " +- "complete copies") +- select.add_option("", "--logs", action="store_const", +- const="logs", dest="type", +- help="List revisions that have a patchlog in the " +- "tree") +- select.add_option("", "--missing", action="store_const", +- const="missing", dest="type", +- help="List revisions from the specified version that" +- " have no patchlog in the tree") +- select.add_option("", "--skip-present", action="store_const", +- const="skip-present", dest="type", +- help="List revisions from the specified version that" +- " have no patchlogs at all in the tree") +- select.add_option("", "--present", action="store_const", +- const="present", dest="type", +- help="List revisions from the specified version that" +- " have no patchlog in the tree, but can't be merged") +- select.add_option("", "--missing-from", action="store_const", +- const="missing-from", dest="type", +- help="List revisions from the specified revision " +- "that have no patchlog for the tree version") +- select.add_option("", "--partner-missing", action="store_const", +- const="partner-missing", dest="type", +- help="List revisions in partner versions that are" +- " missing") +- select.add_option("", "--new-merges", action="store_const", +- const="new-merges", dest="type", +- help="List revisions that have had patchlogs added" +- " to the tree since the last commit") +- select.add_option("", "--direct-merges", action="store_const", +- const="direct-merges", dest="type", +- help="List revisions that have been directly added" +- " to tree since the last commit ") +- select.add_option("", "--library", action="store_const", +- const="library", dest="type", +- help="List revisions in the revision library") +- select.add_option("", "--ancestry", action="store_const", +- const="ancestry", dest="type", +- help="List revisions that are ancestors of the " +- "current tree version") +- +- select.add_option("", "--dependencies", action="store_const", +- const="dependencies", dest="type", +- help="List revisions that the given revision " +- "depends on") +- +- select.add_option("", "--non-dependencies", action="store_const", +- const="non-dependencies", dest="type", +- help="List revisions that the given revision " +- "does not depend on") +- +- select.add_option("--micro", action="store_const", +- const="micro", dest="type", +- help="List partner revisions aimed for this " +- "micro-branch") +- +- select.add_option("", "--modified", dest="modified", +- help="List tree ancestor revisions that modified a " +- "given file", metavar="FILE[:LINE]") + ++ cmdutil.add_revision_iter_options(select) + parser.add_option("", "--skip", dest="skip", + help="Skip revisions. Positive numbers skip from " + "beginning, negative skip from end.", +@@ -1312,6 +1174,9 @@ + format.add_option("--cacherev", action="store_const", + const=paths.determine_cacherev_path, dest="display", + help="Show location of cacherev file") ++ format.add_option("--changelog", action="store_const", ++ const=self.changelog_append, dest="display", ++ help="Show location of cacherev file") + parser.add_option_group(format) + display = cmdutil.OptionGroup(parser, "Display format options", + "These control the display of data") +@@ -1448,6 +1313,7 @@ + if os.access(self.history_file, os.R_OK) and \ + os.path.isfile(self.history_file): + readline.read_history_file(self.history_file) ++ self.cwd = os.getcwd() + + def write_history(self): + readline.write_history_file(self.history_file) +@@ -1470,16 +1336,21 @@ + def set_prompt(self): + if self.tree is not None: + try: +- version = " "+self.tree.tree_version.nonarch ++ prompt = pylon.alias_or_version(self.tree.tree_version, ++ self.tree, ++ full=False) ++ if prompt is not None: ++ prompt = " " + prompt + except: +- version = "" ++ prompt = "" + else: +- version = "" +- self.prompt = "Fai%s> " % version ++ prompt = "" ++ self.prompt = "Fai%s> " % prompt + + def set_title(self, command=None): + try: +- version = self.tree.tree_version.nonarch ++ version = pylon.alias_or_version(self.tree.tree_version, self.tree, ++ full=False) + except: + version = "[no version]" + if command is None: +@@ -1489,8 +1360,15 @@ + def do_cd(self, line): + if line == "": + line = "~" ++ line = os.path.expanduser(line) ++ if os.path.isabs(line): ++ newcwd = line ++ else: ++ newcwd = self.cwd+'/'+line ++ newcwd = os.path.normpath(newcwd) + try: +- os.chdir(os.path.expanduser(line)) ++ os.chdir(newcwd) ++ self.cwd = newcwd + except Exception, e: + print e + try: +@@ -1523,7 +1401,7 @@ + except cmdutil.CantDetermineRevision, e: + print e + except Exception, e: +- print "Unhandled error:\n%s" % cmdutil.exception_str(e) ++ print "Unhandled error:\n%s" % errors.exception_str(e) + + elif suggestions.has_key(args[0]): + print suggestions[args[0]] +@@ -1574,7 +1452,7 @@ + arg = line.split()[-1] + else: + arg = "" +- iter = iter_munged_completions(iter, arg, text) ++ iter = cmdutil.iter_munged_completions(iter, arg, text) + except Exception, e: + print e + return list(iter) +@@ -1604,10 +1482,11 @@ + else: + arg = "" + if arg.startswith("-"): +- return list(iter_munged_completions(iter, arg, text)) ++ return list(cmdutil.iter_munged_completions(iter, arg, ++ text)) + else: +- return list(iter_munged_completions( +- iter_file_completions(arg), arg, text)) ++ return list(cmdutil.iter_munged_completions( ++ cmdutil.iter_file_completions(arg), arg, text)) + + + elif cmd == "cd": +@@ -1615,13 +1494,13 @@ + arg = args.split()[-1] + else: + arg = "" +- iter = iter_dir_completions(arg) +- iter = iter_munged_completions(iter, arg, text) ++ iter = cmdutil.iter_dir_completions(arg) ++ iter = cmdutil.iter_munged_completions(iter, arg, text) + return list(iter) + elif len(args)>0: + arg = args.split()[-1] +- return list(iter_munged_completions(iter_file_completions(arg), +- arg, text)) ++ iter = cmdutil.iter_file_completions(arg) ++ return list(cmdutil.iter_munged_completions(iter, arg, text)) + else: + return self.completenames(text, line, begidx, endidx) + except Exception, e: +@@ -1636,44 +1515,8 @@ + yield entry + + +-def iter_file_completions(arg, only_dirs = False): +- """Generate an iterator that iterates through filename completions. +- +- :param arg: The filename fragment to match +- :type arg: str +- :param only_dirs: If true, match only directories +- :type only_dirs: bool +- """ +- cwd = os.getcwd() +- if cwd != "/": +- extras = [".", ".."] +- else: +- extras = [] +- (dir, file) = os.path.split(arg) +- if dir != "": +- listingdir = os.path.expanduser(dir) +- else: +- listingdir = cwd +- for file in cmdutil.iter_combine([os.listdir(listingdir), extras]): +- if dir != "": +- userfile = dir+'/'+file +- else: +- userfile = file +- if userfile.startswith(arg): +- if os.path.isdir(listingdir+'/'+file): +- userfile+='/' +- yield userfile +- elif not only_dirs: +- yield userfile +- +-def iter_munged_completions(iter, arg, text): +- for completion in iter: +- completion = str(completion) +- if completion.startswith(arg): +- yield completion[len(arg)-len(text):] +- + def iter_source_file_completions(tree, arg): +- treepath = cmdutil.tree_cwd(tree) ++ treepath = arch_compound.tree_cwd(tree) + if len(treepath) > 0: + dirs = [treepath] + else: +@@ -1701,7 +1544,7 @@ + :return: An iterator of all matching untagged files + :rtype: iterator of str + """ +- treepath = cmdutil.tree_cwd(tree) ++ treepath = arch_compound.tree_cwd(tree) + if len(treepath) > 0: + dirs = [treepath] + else: +@@ -1743,8 +1586,8 @@ + :param arg: The prefix to match + :type arg: str + """ +- treepath = cmdutil.tree_cwd(tree) +- tmpdir = cmdutil.tmpdir() ++ treepath = arch_compound.tree_cwd(tree) ++ tmpdir = util.tmpdir() + changeset = tmpdir+"/changeset" + completions = [] + revision = cmdutil.determine_revision_tree(tree) +@@ -1756,14 +1599,6 @@ + shutil.rmtree(tmpdir) + return completions + +-def iter_dir_completions(arg): +- """Generate an iterator that iterates through directory name completions. +- +- :param arg: The directory name fragment to match +- :type arg: str +- """ +- return iter_file_completions(arg, True) +- + class Shell(BaseCommand): + def __init__(self): + self.description = "Runs Fai as a shell" +@@ -1795,7 +1630,11 @@ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + +- tree = arch.tree_root() ++ try: ++ tree = arch.tree_root() ++ except arch.errors.TreeRootError, e: ++ raise pylon.errors.CommandFailedWrapper(e) ++ + + if (len(args) == 0) == (options.untagged == False): + raise cmdutil.GetHelp +@@ -1809,13 +1648,22 @@ + if options.id_type == "tagline": + if method != "tagline": + if not cmdutil.prompt("Tagline in other tree"): +- if method == "explicit": +- options.id_type == explicit ++ if method == "explicit" or method == "implicit": ++ options.id_type == method + else: + print "add-id not supported for \"%s\" tagging method"\ + % method + return + ++ elif options.id_type == "implicit": ++ if method != "implicit": ++ if not cmdutil.prompt("Implicit in other tree"): ++ if method == "explicit" or method == "tagline": ++ options.id_type == method ++ else: ++ print "add-id not supported for \"%s\" tagging method"\ ++ % method ++ return + elif options.id_type == "explicit": + if method != "tagline" and method != explicit: + if not prompt("Explicit in other tree"): +@@ -1824,7 +1672,8 @@ + return + + if options.id_type == "auto": +- if method != "tagline" and method != "explicit": ++ if method != "tagline" and method != "explicit" \ ++ and method !="implicit": + print "add-id not supported for \"%s\" tagging method" % method + return + else: +@@ -1852,10 +1701,12 @@ + previous_files.extend(files) + if id_type == "explicit": + cmdutil.add_id(files) +- elif id_type == "tagline": ++ elif id_type == "tagline" or id_type == "implicit": + for file in files: + try: +- cmdutil.add_tagline_or_explicit_id(file) ++ implicit = (id_type == "implicit") ++ cmdutil.add_tagline_or_explicit_id(file, False, ++ implicit) + except cmdutil.AlreadyTagged: + print "\"%s\" already has a tagline." % file + except cmdutil.NoCommentSyntax: +@@ -1888,6 +1739,9 @@ + parser.add_option("--tagline", action="store_const", + const="tagline", dest="id_type", + help="Use a tagline id") ++ parser.add_option("--implicit", action="store_const", ++ const="implicit", dest="id_type", ++ help="Use an implicit id (deprecated)") + parser.add_option("--untagged", action="store_true", + dest="untagged", default=False, + help="tag all untagged files") +@@ -1926,27 +1780,7 @@ + def get_completer(self, arg, index): + if self.tree is None: + raise arch.errors.TreeRootError +- completions = list(ancillary.iter_partners(self.tree, +- self.tree.tree_version)) +- if len(completions) == 0: +- completions = list(self.tree.iter_log_versions()) +- +- aliases = [] +- try: +- for completion in completions: +- alias = ancillary.compact_alias(str(completion), self.tree) +- if alias: +- aliases.extend(alias) +- +- for completion in completions: +- if completion.archive == self.tree.tree_version.archive: +- aliases.append(completion.nonarch) +- +- except Exception, e: +- print e +- +- completions.extend(aliases) +- return completions ++ return cmdutil.merge_completions(self.tree, arg, index) + + def do_command(self, cmdargs): + """ +@@ -1961,7 +1795,7 @@ + + if self.tree is None: + raise arch.errors.TreeRootError(os.getcwd()) +- if cmdutil.has_changed(self.tree.tree_version): ++ if cmdutil.has_changed(ancillary.comp_revision(self.tree)): + raise UncommittedChanges(self.tree) + + if len(args) > 0: +@@ -2027,14 +1861,14 @@ + :type other_revision: `arch.Revision` + :return: 0 if the merge was skipped, 1 if it was applied + """ +- other_tree = cmdutil.find_or_make_local_revision(other_revision) ++ other_tree = arch_compound.find_or_make_local_revision(other_revision) + try: + if action == "native-merge": +- ancestor = cmdutil.merge_ancestor2(self.tree, other_tree, +- other_revision) ++ ancestor = arch_compound.merge_ancestor2(self.tree, other_tree, ++ other_revision) + elif action == "update": +- ancestor = cmdutil.tree_latest(self.tree, +- other_revision.version) ++ ancestor = arch_compound.tree_latest(self.tree, ++ other_revision.version) + except CantDetermineRevision, e: + raise CommandFailedWrapper(e) + cmdutil.colorize(arch.Chatter("* Found common ancestor %s" % ancestor)) +@@ -2104,7 +1938,10 @@ + if self.tree is None: + raise arch.errors.TreeRootError + +- edit_log(self.tree) ++ try: ++ edit_log(self.tree, self.tree.tree_version) ++ except pylon.errors.NoEditorSpecified, e: ++ raise pylon.errors.CommandFailedWrapper(e) + + def get_parser(self): + """ +@@ -2132,7 +1969,7 @@ + """ + return + +-def edit_log(tree): ++def edit_log(tree, version): + """Makes and edits the log for a tree. Does all kinds of fancy things + like log templates and merge summaries and log-for-merge + +@@ -2141,28 +1978,29 @@ + """ + #ensure we have an editor before preparing the log + cmdutil.find_editor() +- log = tree.log_message(create=False) ++ log = tree.log_message(create=False, version=version) + log_is_new = False + if log is None or cmdutil.prompt("Overwrite log"): + if log is not None: + os.remove(log.name) +- log = tree.log_message(create=True) ++ log = tree.log_message(create=True, version=version) + log_is_new = True + tmplog = log.name +- template = tree+"/{arch}/=log-template" +- if not os.path.exists(template): +- template = os.path.expanduser("~/.arch-params/=log-template") +- if not os.path.exists(template): +- template = None ++ template = pylon.log_template_path(tree) + if template: + shutil.copyfile(template, tmplog) +- +- new_merges = list(cmdutil.iter_new_merges(tree, +- tree.tree_version)) +- log["Summary"] = merge_summary(new_merges, tree.tree_version) ++ comp_version = ancillary.comp_revision(tree).version ++ new_merges = cmdutil.iter_new_merges(tree, comp_version) ++ new_merges = cmdutil.direct_merges(new_merges) ++ log["Summary"] = pylon.merge_summary(new_merges, ++ version) + if len(new_merges) > 0: + if cmdutil.prompt("Log for merge"): +- mergestuff = cmdutil.log_for_merge(tree) ++ if cmdutil.prompt("changelog for merge"): ++ mergestuff = "Patches applied:\n" ++ mergestuff += pylon.changelog_for_merge(new_merges) ++ else: ++ mergestuff = cmdutil.log_for_merge(tree, comp_version) + log.description += mergestuff + log.save() + try: +@@ -2172,29 +2010,6 @@ + os.remove(log.name) + raise + +-def merge_summary(new_merges, tree_version): +- if len(new_merges) == 0: +- return "" +- if len(new_merges) == 1: +- summary = new_merges[0].summary +- else: +- summary = "Merge" +- +- credits = [] +- for merge in new_merges: +- if arch.my_id() != merge.creator: +- name = re.sub("<.*>", "", merge.creator).rstrip(" "); +- if not name in credits: +- credits.append(name) +- else: +- version = merge.revision.version +- if version.archive == tree_version.archive: +- if not version.nonarch in credits: +- credits.append(version.nonarch) +- elif not str(version) in credits: +- credits.append(str(version)) +- +- return ("%s (%s)") % (summary, ", ".join(credits)) + + class MirrorArchive(BaseCommand): + """ +@@ -2268,31 +2083,73 @@ + + Use "alias" to list available (user and automatic) aliases.""" + ++auto_alias = [ ++"acur", ++"The latest revision in the archive of the tree-version. You can specify \ ++a different version like so: acur:foo--bar--0 (aliases can be used)", ++"tcur", ++"""(tree current) The latest revision in the tree of the tree-version. \ ++You can specify a different version like so: tcur:foo--bar--0 (aliases can be \ ++used).""", ++"tprev" , ++"""(tree previous) The previous revision in the tree of the tree-version. To \ ++specify an older revision, use a number, e.g. "tprev:4" """, ++"tanc" , ++"""(tree ancestor) The ancestor revision of the tree To specify an older \ ++revision, use a number, e.g. "tanc:4".""", ++"tdate" , ++"""(tree date) The latest revision from a given date, e.g. "tdate:July 6".""", ++"tmod" , ++""" (tree modified) The latest revision to modify a given file, e.g. \ ++"tmod:engine.cpp" or "tmod:engine.cpp:16".""", ++"ttag" , ++"""(tree tag) The revision that was tagged into the current tree revision, \ ++according to the tree""", ++"tagcur", ++"""(tag current) The latest revision of the version that the current tree \ ++was tagged from.""", ++"mergeanc" , ++"""The common ancestor of the current tree and the specified revision. \ ++Defaults to the first partner-version's latest revision or to tagcur.""", ++] ++ ++ ++def is_auto_alias(name): ++ """Determine whether a name is an auto alias name ++ ++ :param name: the name to check ++ :type name: str ++ :return: True if the name is an auto alias, false if not ++ :rtype: bool ++ """ ++ return name in [f for (f, v) in pylon.util.iter_pairs(auto_alias)] ++ ++ ++def display_def(iter, wrap = 80): ++ """Display a list of definitions ++ ++ :param iter: iter of name, definition pairs ++ :type iter: iter of (str, str) ++ :param wrap: The width for text wrapping ++ :type wrap: int ++ """ ++ vals = list(iter) ++ maxlen = 0 ++ for (key, value) in vals: ++ if len(key) > maxlen: ++ maxlen = len(key) ++ for (key, value) in vals: ++ tw=textwrap.TextWrapper(width=wrap, ++ initial_indent=key.rjust(maxlen)+" : ", ++ subsequent_indent="".rjust(maxlen+3)) ++ print tw.fill(value) ++ ++ + def help_aliases(tree): +- print """Auto-generated aliases +- acur : The latest revision in the archive of the tree-version. You can specfy +- a different version like so: acur:foo--bar--0 (aliases can be used) +- tcur : (tree current) The latest revision in the tree of the tree-version. +- You can specify a different version like so: tcur:foo--bar--0 (aliases +- can be used). +-tprev : (tree previous) The previous revision in the tree of the tree-version. +- To specify an older revision, use a number, e.g. "tprev:4" +- tanc : (tree ancestor) The ancestor revision of the tree +- To specify an older revision, use a number, e.g. "tanc:4" +-tdate : (tree date) The latest revision from a given date (e.g. "tdate:July 6") +- tmod : (tree modified) The latest revision to modify a given file +- (e.g. "tmod:engine.cpp" or "tmod:engine.cpp:16") +- ttag : (tree tag) The revision that was tagged into the current tree revision, +- according to the tree. +-tagcur: (tag current) The latest revision of the version that the current tree +- was tagged from. +-mergeanc : The common ancestor of the current tree and the specified revision. +- Defaults to the first partner-version's latest revision or to tagcur. +- """ ++ print """Auto-generated aliases""" ++ display_def(pylon.util.iter_pairs(auto_alias)) + print "User aliases" +- for parts in ancillary.iter_all_alias(tree): +- print parts[0].rjust(10)+" : "+parts[1] +- ++ display_def(ancillary.iter_all_alias(tree)) + + class Inventory(BaseCommand): + """List the status of files in the tree""" +@@ -2428,6 +2285,11 @@ + except cmdutil.ForbiddenAliasSyntax, e: + raise CommandFailedWrapper(e) + ++ def no_prefix(self, alias): ++ if alias.startswith("^"): ++ alias = alias[1:] ++ return alias ++ + def arg_dispatch(self, args, options): + """Add, modify, or list aliases, depending on number of arguments + +@@ -2438,15 +2300,20 @@ + if len(args) == 0: + help_aliases(self.tree) + return +- elif len(args) == 1: +- self.print_alias(args[0]) +- elif (len(args)) == 2: +- self.add(args[0], args[1], options) + else: +- raise cmdutil.GetHelp ++ alias = self.no_prefix(args[0]) ++ if len(args) == 1: ++ self.print_alias(alias) ++ elif (len(args)) == 2: ++ self.add(alias, args[1], options) ++ else: ++ raise cmdutil.GetHelp + + def print_alias(self, alias): + answer = None ++ if is_auto_alias(alias): ++ raise pylon.errors.IsAutoAlias(alias, "\"%s\" is an auto alias." ++ " Use \"revision\" to expand auto aliases." % alias) + for pair in ancillary.iter_all_alias(self.tree): + if pair[0] == alias: + answer = pair[1] +@@ -2464,6 +2331,8 @@ + :type expansion: str + :param options: The commandline options + """ ++ if is_auto_alias(alias): ++ raise IsAutoAlias(alias) + newlist = "" + written = False + new_line = "%s=%s\n" % (alias, cmdutil.expand_alias(expansion, +@@ -2490,14 +2359,17 @@ + deleted = False + if len(args) != 1: + raise cmdutil.GetHelp ++ alias = self.no_prefix(args[0]) ++ if is_auto_alias(alias): ++ raise IsAutoAlias(alias) + newlist = "" + for pair in self.get_iterator(options): +- if pair[0] != args[0]: ++ if pair[0] != alias: + newlist+="%s=%s\n" % (pair[0], pair[1]) + else: + deleted = True + if not deleted: +- raise errors.NoSuchAlias(args[0]) ++ raise errors.NoSuchAlias(alias) + self.write_aliases(newlist, options) + + def get_alias_file(self, options): +@@ -2526,7 +2398,7 @@ + :param options: The commandline options + """ + filename = os.path.expanduser(self.get_alias_file(options)) +- file = cmdutil.NewFileVersion(filename) ++ file = util.NewFileVersion(filename) + file.write(newlist) + file.commit() + +@@ -2588,10 +2460,13 @@ + :param cmdargs: The commandline arguments + :type cmdargs: list of str + """ +- cmdutil.find_editor() + parser = self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: ++ cmdutil.find_editor() ++ except pylon.errors.NoEditorSpecified, e: ++ raise pylon.errors.CommandFailedWrapper(e) ++ try: + self.tree=arch.tree_root() + except: + self.tree=None +@@ -2655,7 +2530,7 @@ + target_revision = cmdutil.determine_revision_arch(self.tree, + args[0]) + else: +- target_revision = cmdutil.tree_latest(self.tree) ++ target_revision = arch_compound.tree_latest(self.tree) + if len(args) > 1: + merges = [ arch.Patchlog(cmdutil.determine_revision_arch( + self.tree, f)) for f in args[1:] ] +@@ -2711,7 +2586,7 @@ + + :param message: The message to send + :type message: `email.Message`""" +- server = smtplib.SMTP() ++ server = smtplib.SMTP("localhost") + server.sendmail(message['From'], message['To'], message.as_string()) + server.quit() + +@@ -2763,6 +2638,22 @@ + 'alias' : Alias, + 'request-merge': RequestMerge, + } ++ ++def my_import(mod_name): ++ module = __import__(mod_name) ++ components = mod_name.split('.') ++ for comp in components[1:]: ++ module = getattr(module, comp) ++ return module ++ ++def plugin(mod_name): ++ module = my_import(mod_name) ++ module.add_command(commands) ++ ++for file in os.listdir(sys.path[0]+"/command"): ++ if len(file) > 3 and file[-3:] == ".py" and file != "__init__.py": ++ plugin("command."+file[:-3]) ++ + suggestions = { + 'apply-delta' : "Try \"apply-changes\".", + 'delta' : "To compare two revisions, use \"changes\".", +@@ -2784,6 +2675,7 @@ + 'tagline' : "Use add-id. It uses taglines in tagline trees", + 'emlog' : "Use elog. It automatically adds log-for-merge text, if any", + 'library-revisions' : "Use revisions --library", +-'file-revert' : "Use revert FILE" ++'file-revert' : "Use revert FILE", ++'join-branch' : "Use replay --logs-only" + } + # arch-tag: 19d5739d-3708-486c-93ba-deecc3027fc7 *** added file 'testdata/insert_top.patch' --- /dev/null +++ testdata/insert_top.patch @@ -0,0 +1,7 @@ +--- orig/pylon/patches.py ++++ mod/pylon/patches.py +@@ -1,3 +1,4 @@ ++#test + import util + import sys + class PatchSyntax(Exception): *** added file 'testdata/mod' --- /dev/null +++ testdata/mod @@ -0,0 +1,2681 @@ +# Copyright (C) 2004 Aaron Bentley +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import sys +import arch +import arch.util +import arch.arch + +import pylon.errors +from pylon.errors import * +from pylon import errors +from pylon import util +from pylon import arch_core +from pylon import arch_compound +from pylon import ancillary +from pylon import misc +from pylon import paths + +import abacmds +import cmdutil +import shutil +import os +import options +import time +import cmd +import readline +import re +import string +import terminal +import email +import smtplib +import textwrap + +__docformat__ = "restructuredtext" +__doc__ = "Implementation of user (sub) commands" +commands = {} + +def find_command(cmd): + """ + Return an instance of a command type. Return None if the type isn't + registered. + + :param cmd: the name of the command to look for + :type cmd: the type of the command + """ + if commands.has_key(cmd): + return commands[cmd]() + else: + return None + +class BaseCommand: + def __call__(self, cmdline): + try: + self.do_command(cmdline.split()) + except cmdutil.GetHelp, e: + self.help() + except Exception, e: + print e + + def get_completer(index): + return None + + def complete(self, args, text): + """ + Returns a list of possible completions for the given text. + + :param args: The complete list of arguments + :type args: List of str + :param text: text to complete (may be shorter than args[-1]) + :type text: str + :rtype: list of str + """ + matches = [] + candidates = None + + if len(args) > 0: + realtext = args[-1] + else: + realtext = "" + + try: + parser=self.get_parser() + if realtext.startswith('-'): + candidates = parser.iter_options() + else: + (options, parsed_args) = parser.parse_args(args) + + if len (parsed_args) > 0: + candidates = self.get_completer(parsed_args[-1], len(parsed_args) -1) + else: + candidates = self.get_completer("", 0) + except: + pass + if candidates is None: + return + for candidate in candidates: + candidate = str(candidate) + if candidate.startswith(realtext): + matches.append(candidate[len(realtext)- len(text):]) + return matches + + +class Help(BaseCommand): + """ + Lists commands, prints help messages. + """ + def __init__(self): + self.description="Prints help mesages" + self.parser = None + + def do_command(self, cmdargs): + """ + Prints a help message. + """ + options, args = self.get_parser().parse_args(cmdargs) + if len(args) > 1: + raise cmdutil.GetHelp + + if options.native or options.suggestions or options.external: + native = options.native + suggestions = options.suggestions + external = options.external + else: + native = True + suggestions = False + external = True + + if len(args) == 0: + self.list_commands(native, suggestions, external) + return + elif len(args) == 1: + command_help(args[0]) + return + + def help(self): + self.get_parser().print_help() + print """ +If no command is specified, commands are listed. If a command is +specified, help for that command is listed. + """ + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + if self.parser is not None: + return self.parser + parser=cmdutil.CmdOptionParser("fai help [command]") + parser.add_option("-n", "--native", action="store_true", + dest="native", help="Show native commands") + parser.add_option("-e", "--external", action="store_true", + dest="external", help="Show external commands") + parser.add_option("-s", "--suggest", action="store_true", + dest="suggestions", help="Show suggestions") + self.parser = parser + return parser + + def list_commands(self, native=True, suggest=False, external=True): + """ + Lists supported commands. + + :param native: list native, python-based commands + :type native: bool + :param external: list external aba-style commands + :type external: bool + """ + if native: + print "Native Fai commands" + keys=commands.keys() + keys.sort() + for k in keys: + space="" + for i in range(28-len(k)): + space+=" " + print space+k+" : "+commands[k]().description + print + if suggest: + print "Unavailable commands and suggested alternatives" + key_list = suggestions.keys() + key_list.sort() + for key in key_list: + print "%28s : %s" % (key, suggestions[key]) + print + if external: + fake_aba = abacmds.AbaCmds() + if (fake_aba.abadir == ""): + return + print "External commands" + fake_aba.list_commands() + print + if not suggest: + print "Use help --suggest to list alternatives to tla and aba"\ + " commands." + if options.tla_fallthrough and (native or external): + print "Fai also supports tla commands." + +def command_help(cmd): + """ + Prints help for a command. + + :param cmd: The name of the command to print help for + :type cmd: str + """ + fake_aba = abacmds.AbaCmds() + cmdobj = find_command(cmd) + if cmdobj != None: + cmdobj.help() + elif suggestions.has_key(cmd): + print "Not available\n" + suggestions[cmd] + else: + abacmd = fake_aba.is_command(cmd) + if abacmd: + abacmd.help() + else: + print "No help is available for \""+cmd+"\". Maybe try \"tla "+cmd+" -H\"?" + + + +class Changes(BaseCommand): + """ + the "changes" command: lists differences between trees/revisions: + """ + + def __init__(self): + self.description="Lists what files have changed in the project tree" + + def get_completer(self, arg, index): + if index > 1: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def parse_commandline(self, cmdline): + """ + Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. + + :param cmdline: A list of arguments to parse + :rtype: (options, Revision, Revision/WorkingTree) + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdline) + if len(args) > 2: + raise cmdutil.GetHelp + + tree=arch.tree_root() + if len(args) == 0: + a_spec = ancillary.comp_revision(tree) + else: + a_spec = cmdutil.determine_revision_tree(tree, args[0]) + cmdutil.ensure_archive_registered(a_spec.archive) + if len(args) == 2: + b_spec = cmdutil.determine_revision_tree(tree, args[1]) + cmdutil.ensure_archive_registered(b_spec.archive) + else: + b_spec=tree + return options, a_spec, b_spec + + def do_command(self, cmdargs): + """ + Master function that perfoms the "changes" command. + """ + try: + options, a_spec, b_spec = self.parse_commandline(cmdargs); + except cmdutil.CantDetermineRevision, e: + print e + return + except arch.errors.TreeRootError, e: + print e + return + if options.changeset: + changeset=options.changeset + tmpdir = None + else: + tmpdir=util.tmpdir() + changeset=tmpdir+"/changeset" + try: + delta=arch.iter_delta(a_spec, b_spec, changeset) + try: + for line in delta: + if cmdutil.chattermatch(line, "changeset:"): + pass + else: + cmdutil.colorize(line, options.suppress_chatter) + except arch.util.ExecProblem, e: + if e.proc.error and e.proc.error.startswith( + "missing explicit id for file"): + raise MissingID(e) + else: + raise + status=delta.status + if status > 1: + return + if (options.perform_diff): + chan = arch_compound.ChangesetMunger(changeset) + chan.read_indices() + if options.diffopts is not None: + if isinstance(b_spec, arch.Revision): + b_dir = b_spec.library_find() + else: + b_dir = b_spec + a_dir = a_spec.library_find() + diffopts = options.diffopts.split() + cmdutil.show_custom_diffs(chan, diffopts, a_dir, b_dir) + else: + cmdutil.show_diffs(delta.changeset) + finally: + if tmpdir and (os.access(tmpdir, os.X_OK)): + shutil.rmtree(tmpdir) + + def get_parser(self): + """ + Returns the options parser to use for the "changes" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai changes [options] [revision]" + " [revision]") + parser.add_option("-d", "--diff", action="store_true", + dest="perform_diff", default=False, + help="Show diffs in summary") + parser.add_option("-c", "--changeset", dest="changeset", + help="Store a changeset in the given directory", + metavar="DIRECTORY") + parser.add_option("-s", "--silent", action="store_true", + dest="suppress_chatter", default=False, + help="Suppress chatter messages") + parser.add_option("--diffopts", dest="diffopts", + help="Use the specified diff options", + metavar="OPTIONS") + + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser is None: + parser=self.get_parser() + parser.print_help() + print """ +Performs source-tree comparisons + +If no revision is specified, the current project tree is compared to the +last-committed revision. If one revision is specified, the current project +tree is compared to that revision. If two revisions are specified, they are +compared to each other. + """ + help_tree_spec() + return + + +class ApplyChanges(BaseCommand): + """ + Apply differences between two revisions to a tree + """ + + def __init__(self): + self.description="Applies changes to a project tree" + + def get_completer(self, arg, index): + if index > 1: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def parse_commandline(self, cmdline, tree): + """ + Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. + + :param cmdline: A list of arguments to parse + :rtype: (options, Revision, Revision/WorkingTree) + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdline) + if len(args) != 2: + raise cmdutil.GetHelp + + a_spec = cmdutil.determine_revision_tree(tree, args[0]) + cmdutil.ensure_archive_registered(a_spec.archive) + b_spec = cmdutil.determine_revision_tree(tree, args[1]) + cmdutil.ensure_archive_registered(b_spec.archive) + return options, a_spec, b_spec + + def do_command(self, cmdargs): + """ + Master function that performs "apply-changes". + """ + try: + tree = arch.tree_root() + options, a_spec, b_spec = self.parse_commandline(cmdargs, tree); + except cmdutil.CantDetermineRevision, e: + print e + return + except arch.errors.TreeRootError, e: + print e + return + delta=cmdutil.apply_delta(a_spec, b_spec, tree) + for line in cmdutil.iter_apply_delta_filter(delta): + cmdutil.colorize(line, options.suppress_chatter) + + def get_parser(self): + """ + Returns the options parser to use for the "apply-changes" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai apply-changes [options] revision" + " revision") + parser.add_option("-d", "--diff", action="store_true", + dest="perform_diff", default=False, + help="Show diffs in summary") + parser.add_option("-c", "--changeset", dest="changeset", + help="Store a changeset in the given directory", + metavar="DIRECTORY") + parser.add_option("-s", "--silent", action="store_true", + dest="suppress_chatter", default=False, + help="Suppress chatter messages") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser is None: + parser=self.get_parser() + parser.print_help() + print """ +Applies changes to a project tree + +Compares two revisions and applies the difference between them to the current +tree. + """ + help_tree_spec() + return + +class Update(BaseCommand): + """ + Updates a project tree to a given revision, preserving un-committed hanges. + """ + + def __init__(self): + self.description="Apply the latest changes to the current directory" + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def parse_commandline(self, cmdline, tree): + """ + Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. + + :param cmdline: A list of arguments to parse + :rtype: (options, Revision, Revision/WorkingTree) + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdline) + if len(args) > 2: + raise cmdutil.GetHelp + + spec=None + if len(args)>0: + spec=args[0] + revision=cmdutil.determine_revision_arch(tree, spec) + cmdutil.ensure_archive_registered(revision.archive) + + mirror_source = cmdutil.get_mirror_source(revision.archive) + if mirror_source != None: + if cmdutil.prompt("Mirror update"): + cmd=cmdutil.mirror_archive(mirror_source, + revision.archive, arch.NameParser(revision).get_package_version()) + for line in arch.chatter_classifier(cmd): + cmdutil.colorize(line, options.suppress_chatter) + + revision=cmdutil.determine_revision_arch(tree, spec) + + return options, revision + + def do_command(self, cmdargs): + """ + Master function that perfoms the "update" command. + """ + tree=arch.tree_root() + try: + options, to_revision = self.parse_commandline(cmdargs, tree); + except cmdutil.CantDetermineRevision, e: + print e + return + except arch.errors.TreeRootError, e: + print e + return + from_revision = arch_compound.tree_latest(tree) + if from_revision==to_revision: + print "Tree is already up to date with:\n"+str(to_revision)+"." + return + cmdutil.ensure_archive_registered(from_revision.archive) + cmd=cmdutil.apply_delta(from_revision, to_revision, tree, + options.patch_forward) + for line in cmdutil.iter_apply_delta_filter(cmd): + cmdutil.colorize(line) + if to_revision.version != tree.tree_version: + if cmdutil.prompt("Update version"): + tree.tree_version = to_revision.version + + def get_parser(self): + """ + Returns the options parser to use for the "update" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai update [options]" + " [revision/version]") + parser.add_option("-f", "--forward", action="store_true", + dest="patch_forward", default=False, + help="pass the --forward option to 'patch'") + parser.add_option("-s", "--silent", action="store_true", + dest="suppress_chatter", default=False, + help="Suppress chatter messages") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser is None: + parser=self.get_parser() + parser.print_help() + print """ +Updates a working tree to the current archive revision + +If a revision or version is specified, that is used instead + """ + help_tree_spec() + return + + +class Commit(BaseCommand): + """ + Create a revision based on the changes in the current tree. + """ + + def __init__(self): + self.description="Write local changes to the archive" + + def get_completer(self, arg, index): + if arg is None: + arg = "" + return iter_modified_file_completions(arch.tree_root(), arg) +# return iter_source_file_completions(arch.tree_root(), arg) + + def parse_commandline(self, cmdline, tree): + """ + Parse commandline arguments. Raise cmtutil.GetHelp if help is needed. + + :param cmdline: A list of arguments to parse + :rtype: (options, Revision, Revision/WorkingTree) + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdline) + + if len(args) == 0: + args = None + if options.version is None: + return options, tree.tree_version, args + + revision=cmdutil.determine_revision_arch(tree, options.version) + return options, revision.get_version(), args + + def do_command(self, cmdargs): + """ + Master function that perfoms the "commit" command. + """ + tree=arch.tree_root() + options, version, files = self.parse_commandline(cmdargs, tree) + ancestor = None + if options.__dict__.has_key("base") and options.base: + base = cmdutil.determine_revision_tree(tree, options.base) + ancestor = base + else: + base = ancillary.submit_revision(tree) + ancestor = base + if ancestor is None: + ancestor = arch_compound.tree_latest(tree, version) + + writeversion=version + archive=version.archive + source=cmdutil.get_mirror_source(archive) + allow_old=False + writethrough="implicit" + + if source!=None: + if writethrough=="explicit" and \ + cmdutil.prompt("Writethrough"): + writeversion=arch.Version(str(source)+"/"+str(version.get_nonarch())) + elif writethrough=="none": + raise CommitToMirror(archive) + + elif archive.is_mirror: + raise CommitToMirror(archive) + + try: + last_revision=tree.iter_logs(version, True).next().revision + except StopIteration, e: + last_revision = None + if ancestor is None: + if cmdutil.prompt("Import from commit"): + return do_import(version) + else: + raise NoVersionLogs(version) + try: + arch_last_revision = version.iter_revisions(True).next() + except StopIteration, e: + arch_last_revision = None + + if last_revision != arch_last_revision: + print "Tree is not up to date with %s" % str(version) + if not cmdutil.prompt("Out of date"): + raise OutOfDate + else: + allow_old=True + + try: + if not cmdutil.has_changed(ancestor): + if not cmdutil.prompt("Empty commit"): + raise EmptyCommit + except arch.util.ExecProblem, e: + if e.proc.error and e.proc.error.startswith( + "missing explicit id for file"): + raise MissingID(e) + else: + raise + log = tree.log_message(create=False, version=version) + if log is None: + try: + if cmdutil.prompt("Create log"): + edit_log(tree, version) + + except cmdutil.NoEditorSpecified, e: + raise CommandFailed(e) + log = tree.log_message(create=False, version=version) + if log is None: + raise NoLogMessage + if log["Summary"] is None or len(log["Summary"].strip()) == 0: + if not cmdutil.prompt("Omit log summary"): + raise errors.NoLogSummary + try: + for line in tree.iter_commit(version, seal=options.seal_version, + base=base, out_of_date_ok=allow_old, file_list=files): + cmdutil.colorize(line, options.suppress_chatter) + + except arch.util.ExecProblem, e: + if e.proc.error and e.proc.error.startswith( + "These files violate naming conventions:"): + raise LintFailure(e.proc.error) + else: + raise + + def get_parser(self): + """ + Returns the options parser to use for the "commit" command. + + :rtype: cmdutil.CmdOptionParser + """ + + parser=cmdutil.CmdOptionParser("fai commit [options] [file1]" + " [file2...]") + parser.add_option("--seal", action="store_true", + dest="seal_version", default=False, + help="seal this version") + parser.add_option("-v", "--version", dest="version", + help="Use the specified version", + metavar="VERSION") + parser.add_option("-s", "--silent", action="store_true", + dest="suppress_chatter", default=False, + help="Suppress chatter messages") + if cmdutil.supports_switch("commit", "--base"): + parser.add_option("--base", dest="base", help="", + metavar="REVISION") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser is None: + parser=self.get_parser() + parser.print_help() + print """ +Updates a working tree to the current archive revision + +If a version is specified, that is used instead + """ +# help_tree_spec() + return + + + +class CatLog(BaseCommand): + """ + Print the log of a given file (from current tree) + """ + def __init__(self): + self.description="Prints the patch log for a revision" + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "cat-log" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: + tree = arch.tree_root() + except arch.errors.TreeRootError, e: + tree = None + spec=None + if len(args) > 0: + spec=args[0] + if len(args) > 1: + raise cmdutil.GetHelp() + try: + if tree: + revision = cmdutil.determine_revision_tree(tree, spec) + else: + revision = cmdutil.determine_revision_arch(tree, spec) + except cmdutil.CantDetermineRevision, e: + raise CommandFailedWrapper(e) + log = None + + use_tree = (options.source == "tree" or \ + (options.source == "any" and tree)) + use_arch = (options.source == "archive" or options.source == "any") + + log = None + if use_tree: + for log in tree.iter_logs(revision.get_version()): + if log.revision == revision: + break + else: + log = None + if log is None and use_arch: + cmdutil.ensure_revision_exists(revision) + log = arch.Patchlog(revision) + if log is not None: + for item in log.items(): + print "%s: %s" % item + print log.description + + def get_parser(self): + """ + Returns the options parser to use for the "cat-log" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai cat-log [revision]") + parser.add_option("--archive", action="store_const", dest="source", + const="archive", default="any", + help="Always get the log from the archive") + parser.add_option("--tree", action="store_const", dest="source", + const="tree", help="Always get the log from the tree") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Prints the log for the specified revision + """ + help_tree_spec() + return + +class Revert(BaseCommand): + """ Reverts a tree (or aspects of it) to a revision + """ + def __init__(self): + self.description="Reverts a tree (or aspects of it) to a revision " + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return iter_modified_file_completions(tree, arg) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revert" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: + tree = arch.tree_root() + except arch.errors.TreeRootError, e: + raise CommandFailed(e) + spec=None + if options.revision is not None: + spec=options.revision + try: + if spec is not None: + revision = cmdutil.determine_revision_tree(tree, spec) + else: + revision = ancillary.comp_revision(tree) + except cmdutil.CantDetermineRevision, e: + raise CommandFailedWrapper(e) + munger = None + + if options.file_contents or options.file_perms or options.deletions\ + or options.additions or options.renames or options.hunk_prompt: + munger = arch_compound.MungeOpts() + munger.set_hunk_prompt(cmdutil.colorize, cmdutil.user_hunk_confirm, + options.hunk_prompt) + + if len(args) > 0 or options.logs or options.pattern_files or \ + options.control: + if munger is None: + munger = cmdutil.arch_compound.MungeOpts(True) + munger.all_types(True) + if len(args) > 0: + t_cwd = arch_compound.tree_cwd(tree) + for name in args: + if len(t_cwd) > 0: + t_cwd += "/" + name = "./" + t_cwd + name + munger.add_keep_file(name); + + if options.file_perms: + munger.file_perms = True + if options.file_contents: + munger.file_contents = True + if options.deletions: + munger.deletions = True + if options.additions: + munger.additions = True + if options.renames: + munger.renames = True + if options.logs: + munger.add_keep_pattern('^\./\{arch\}/[^=].*') + if options.control: + munger.add_keep_pattern("/\.arch-ids|^\./\{arch\}|"\ + "/\.arch-inventory$") + if options.pattern_files: + munger.add_keep_pattern(options.pattern_files) + + for line in arch_compound.revert(tree, revision, munger, + not options.no_output): + cmdutil.colorize(line) + + + def get_parser(self): + """ + Returns the options parser to use for the "cat-log" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai revert [options] [FILE...]") + parser.add_option("", "--contents", action="store_true", + dest="file_contents", + help="Revert file content changes") + parser.add_option("", "--permissions", action="store_true", + dest="file_perms", + help="Revert file permissions changes") + parser.add_option("", "--deletions", action="store_true", + dest="deletions", + help="Restore deleted files") + parser.add_option("", "--additions", action="store_true", + dest="additions", + help="Remove added files") + parser.add_option("", "--renames", action="store_true", + dest="renames", + help="Revert file names") + parser.add_option("--hunks", action="store_true", + dest="hunk_prompt", default=False, + help="Prompt which hunks to revert") + parser.add_option("--pattern-files", dest="pattern_files", + help="Revert files that match this pattern", + metavar="REGEX") + parser.add_option("--logs", action="store_true", + dest="logs", default=False, + help="Revert only logs") + parser.add_option("--control-files", action="store_true", + dest="control", default=False, + help="Revert logs and other control files") + parser.add_option("-n", "--no-output", action="store_true", + dest="no_output", + help="Don't keep an undo changeset") + parser.add_option("--revision", dest="revision", + help="Revert to the specified revision", + metavar="REVISION") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Reverts changes in the current working tree. If no flags are specified, all +types of changes are reverted. Otherwise, only selected types of changes are +reverted. + +If a revision is specified on the commandline, differences between the current +tree and that revision are reverted. If a version is specified, the current +tree is used to determine the revision. + +If files are specified, only those files listed will have any changes applied. +To specify a renamed file, you can use either the old or new name. (or both!) + +Unless "-n" is specified, reversions can be undone with "redo". + """ + return + +class Revision(BaseCommand): + """ + Print a revision name based on a revision specifier + """ + def __init__(self): + self.description="Prints the name of a revision" + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + tree = None + + spec=None + if len(args) > 0: + spec=args[0] + if len(args) > 1: + raise cmdutil.GetHelp + try: + if tree: + revision = cmdutil.determine_revision_tree(tree, spec) + else: + revision = cmdutil.determine_revision_arch(tree, spec) + except cmdutil.CantDetermineRevision, e: + print str(e) + return + print options.display(revision) + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai revision [revision]") + parser.add_option("", "--location", action="store_const", + const=paths.determine_path, dest="display", + help="Show location instead of name", default=str) + parser.add_option("--import", action="store_const", + const=paths.determine_import_path, dest="display", + help="Show location of import file") + parser.add_option("--log", action="store_const", + const=paths.determine_log_path, dest="display", + help="Show location of log file") + parser.add_option("--patch", action="store_const", + dest="display", const=paths.determine_patch_path, + help="Show location of patchfile") + parser.add_option("--continuation", action="store_const", + const=paths.determine_continuation_path, + dest="display", + help="Show location of continuation file") + parser.add_option("--cacherev", action="store_const", + const=paths.determine_cacherev_path, dest="display", + help="Show location of cacherev file") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Expands aliases and prints the name of the specified revision. Instead of +the name, several options can be used to print locations. If more than one is +specified, the last one is used. + """ + help_tree_spec() + return + +class Revisions(BaseCommand): + """ + Print a revision name based on a revision specifier + """ + def __init__(self): + self.description="Lists revisions" + self.cl_revisions = [] + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + (options, args) = self.get_parser().parse_args(cmdargs) + if len(args) > 1: + raise cmdutil.GetHelp + try: + self.tree = arch.tree_root() + except arch.errors.TreeRootError: + self.tree = None + if options.type == "default": + options.type = "archive" + try: + iter = cmdutil.revision_iterator(self.tree, options.type, args, + options.reverse, options.modified, + options.shallow) + except cmdutil.CantDetermineRevision, e: + raise CommandFailedWrapper(e) + except cmdutil.CantDetermineVersion, e: + raise CommandFailedWrapper(e) + if options.skip is not None: + iter = cmdutil.iter_skip(iter, int(options.skip)) + + try: + for revision in iter: + log = None + if isinstance(revision, arch.Patchlog): + log = revision + revision=revision.revision + out = options.display(revision) + if out is not None: + print out + if log is None and (options.summary or options.creator or + options.date or options.merges): + log = revision.patchlog + if options.creator: + print " %s" % log.creator + if options.date: + print " %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date) + if options.summary: + print " %s" % log.summary + if options.merges: + showed_title = False + for revision in log.merged_patches: + if not showed_title: + print " Merged:" + showed_title = True + print " %s" % revision + if len(self.cl_revisions) > 0: + print pylon.changelog_for_merge(self.cl_revisions) + except pylon.errors.TreeRootNone: + raise CommandFailedWrapper( + Exception("This option can only be used in a project tree.")) + + def changelog_append(self, revision): + if isinstance(revision, arch.Revision): + revision=arch.Patchlog(revision) + self.cl_revisions.append(revision) + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai revisions [version/revision]") + select = cmdutil.OptionGroup(parser, "Selection options", + "Control which revisions are listed. These options" + " are mutually exclusive. If more than one is" + " specified, the last is used.") + + cmdutil.add_revision_iter_options(select) + parser.add_option("", "--skip", dest="skip", + help="Skip revisions. Positive numbers skip from " + "beginning, negative skip from end.", + metavar="NUMBER") + + parser.add_option_group(select) + + format = cmdutil.OptionGroup(parser, "Revision format options", + "These control the appearance of listed revisions") + format.add_option("", "--location", action="store_const", + const=paths.determine_path, dest="display", + help="Show location instead of name", default=str) + format.add_option("--import", action="store_const", + const=paths.determine_import_path, dest="display", + help="Show location of import file") + format.add_option("--log", action="store_const", + const=paths.determine_log_path, dest="display", + help="Show location of log file") + format.add_option("--patch", action="store_const", + dest="display", const=paths.determine_patch_path, + help="Show location of patchfile") + format.add_option("--continuation", action="store_const", + const=paths.determine_continuation_path, + dest="display", + help="Show location of continuation file") + format.add_option("--cacherev", action="store_const", + const=paths.determine_cacherev_path, dest="display", + help="Show location of cacherev file") + format.add_option("--changelog", action="store_const", + const=self.changelog_append, dest="display", + help="Show location of cacherev file") + parser.add_option_group(format) + display = cmdutil.OptionGroup(parser, "Display format options", + "These control the display of data") + display.add_option("-r", "--reverse", action="store_true", + dest="reverse", help="Sort from newest to oldest") + display.add_option("-s", "--summary", action="store_true", + dest="summary", help="Show patchlog summary") + display.add_option("-D", "--date", action="store_true", + dest="date", help="Show patchlog date") + display.add_option("-c", "--creator", action="store_true", + dest="creator", help="Show the id that committed the" + " revision") + display.add_option("-m", "--merges", action="store_true", + dest="merges", help="Show the revisions that were" + " merged") + parser.add_option_group(display) + return parser + def help(self, parser=None): + """Attempt to explain the revisions command + + :param parser: If supplied, used to determine options + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """List revisions. + """ + help_tree_spec() + + +class Get(BaseCommand): + """ + Retrieve a revision from the archive + """ + def __init__(self): + self.description="Retrieve a revision from the archive" + self.parser=self.get_parser() + + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + + def do_command(self, cmdargs): + """ + Master function that perfoms the "get" command. + """ + (options, args) = self.parser.parse_args(cmdargs) + if len(args) < 1: + return self.help() + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + tree = None + + arch_loc = None + try: + revision, arch_loc = paths.full_path_decode(args[0]) + except Exception, e: + revision = cmdutil.determine_revision_arch(tree, args[0], + check_existence=False, allow_package=True) + if len(args) > 1: + directory = args[1] + else: + directory = str(revision.nonarch) + if os.path.exists(directory): + raise DirectoryExists(directory) + cmdutil.ensure_archive_registered(revision.archive, arch_loc) + try: + cmdutil.ensure_revision_exists(revision) + except cmdutil.NoSuchRevision, e: + raise CommandFailedWrapper(e) + + link = cmdutil.prompt ("get link") + for line in cmdutil.iter_get(revision, directory, link, + options.no_pristine, + options.no_greedy_add): + cmdutil.colorize(line) + + def get_parser(self): + """ + Returns the options parser to use for the "get" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai get revision [dir]") + parser.add_option("--no-pristine", action="store_true", + dest="no_pristine", + help="Do not make pristine copy for reference") + parser.add_option("--no-greedy-add", action="store_true", + dest="no_greedy_add", + help="Never add to greedy libraries") + + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Expands aliases and constructs a project tree for a revision. If the optional +"dir" argument is provided, the project tree will be stored in this directory. + """ + help_tree_spec() + return + +class PromptCmd(cmd.Cmd): + def __init__(self): + cmd.Cmd.__init__(self) + self.prompt = "Fai> " + try: + self.tree = arch.tree_root() + except: + self.tree = None + self.set_title() + self.set_prompt() + self.fake_aba = abacmds.AbaCmds() + self.identchars += '-' + self.history_file = os.path.expanduser("~/.fai-history") + readline.set_completer_delims(string.whitespace) + if os.access(self.history_file, os.R_OK) and \ + os.path.isfile(self.history_file): + readline.read_history_file(self.history_file) + self.cwd = os.getcwd() + + def write_history(self): + readline.write_history_file(self.history_file) + + def do_quit(self, args): + self.write_history() + sys.exit(0) + + def do_exit(self, args): + self.do_quit(args) + + def do_EOF(self, args): + print + self.do_quit(args) + + def postcmd(self, line, bar): + self.set_title() + self.set_prompt() + + def set_prompt(self): + if self.tree is not None: + try: + prompt = pylon.alias_or_version(self.tree.tree_version, + self.tree, + full=False) + if prompt is not None: + prompt = " " + prompt + except: + prompt = "" + else: + prompt = "" + self.prompt = "Fai%s> " % prompt + + def set_title(self, command=None): + try: + version = pylon.alias_or_version(self.tree.tree_version, self.tree, + full=False) + except: + version = "[no version]" + if command is None: + command = "" + sys.stdout.write(terminal.term_title("Fai %s %s" % (command, version))) + + def do_cd(self, line): + if line == "": + line = "~" + line = os.path.expanduser(line) + if os.path.isabs(line): + newcwd = line + else: + newcwd = self.cwd+'/'+line + newcwd = os.path.normpath(newcwd) + try: + os.chdir(newcwd) + self.cwd = newcwd + except Exception, e: + print e + try: + self.tree = arch.tree_root() + except: + self.tree = None + + def do_help(self, line): + Help()(line) + + def default(self, line): + args = line.split() + if find_command(args[0]): + try: + find_command(args[0]).do_command(args[1:]) + except cmdutil.BadCommandOption, e: + print e + except cmdutil.GetHelp, e: + find_command(args[0]).help() + except CommandFailed, e: + print e + except arch.errors.ArchiveNotRegistered, e: + print e + except KeyboardInterrupt, e: + print "Interrupted" + except arch.util.ExecProblem, e: + print e.proc.error.rstrip('\n') + except cmdutil.CantDetermineVersion, e: + print e + except cmdutil.CantDetermineRevision, e: + print e + except Exception, e: + print "Unhandled error:\n%s" % errors.exception_str(e) + + elif suggestions.has_key(args[0]): + print suggestions[args[0]] + + elif self.fake_aba.is_command(args[0]): + tree = None + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + pass + cmd = self.fake_aba.is_command(args[0]) + try: + cmd.run(cmdutil.expand_prefix_alias(args[1:], tree)) + except KeyboardInterrupt, e: + print "Interrupted" + + elif options.tla_fallthrough and args[0] != "rm" and \ + cmdutil.is_tla_command(args[0]): + try: + tree = None + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + pass + args = cmdutil.expand_prefix_alias(args, tree) + arch.util.exec_safe('tla', args, stderr=sys.stderr, + expected=(0, 1)) + except arch.util.ExecProblem, e: + pass + except KeyboardInterrupt, e: + print "Interrupted" + else: + try: + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + tree = None + args=line.split() + os.system(" ".join(cmdutil.expand_prefix_alias(args, tree))) + except KeyboardInterrupt, e: + print "Interrupted" + + def completenames(self, text, line, begidx, endidx): + completions = [] + iter = iter_command_names(self.fake_aba) + try: + if len(line) > 0: + arg = line.split()[-1] + else: + arg = "" + iter = cmdutil.iter_munged_completions(iter, arg, text) + except Exception, e: + print e + return list(iter) + + def completedefault(self, text, line, begidx, endidx): + """Perform completion for native commands. + + :param text: The text to complete + :type text: str + :param line: The entire line to complete + :type line: str + :param begidx: The start of the text in the line + :type begidx: int + :param endidx: The end of the text in the line + :type endidx: int + """ + try: + (cmd, args, foo) = self.parseline(line) + command_obj=find_command(cmd) + if command_obj is not None: + return command_obj.complete(args.split(), text) + elif not self.fake_aba.is_command(cmd) and \ + cmdutil.is_tla_command(cmd): + iter = cmdutil.iter_supported_switches(cmd) + if len(args) > 0: + arg = args.split()[-1] + else: + arg = "" + if arg.startswith("-"): + return list(cmdutil.iter_munged_completions(iter, arg, + text)) + else: + return list(cmdutil.iter_munged_completions( + cmdutil.iter_file_completions(arg), arg, text)) + + + elif cmd == "cd": + if len(args) > 0: + arg = args.split()[-1] + else: + arg = "" + iter = cmdutil.iter_dir_completions(arg) + iter = cmdutil.iter_munged_completions(iter, arg, text) + return list(iter) + elif len(args)>0: + arg = args.split()[-1] + iter = cmdutil.iter_file_completions(arg) + return list(cmdutil.iter_munged_completions(iter, arg, text)) + else: + return self.completenames(text, line, begidx, endidx) + except Exception, e: + print e + + +def iter_command_names(fake_aba): + for entry in cmdutil.iter_combine([commands.iterkeys(), + fake_aba.get_commands(), + cmdutil.iter_tla_commands(False)]): + if not suggestions.has_key(str(entry)): + yield entry + + +def iter_source_file_completions(tree, arg): + treepath = arch_compound.tree_cwd(tree) + if len(treepath) > 0: + dirs = [treepath] + else: + dirs = None + for file in tree.iter_inventory(dirs, source=True, both=True): + file = file_completion_match(file, treepath, arg) + if file is not None: + yield file + + +def iter_untagged(tree, dirs): + for file in arch_core.iter_inventory_filter(tree, dirs, tagged=False, + categories=arch_core.non_root, + control_files=True): + yield file.name + + +def iter_untagged_completions(tree, arg): + """Generate an iterator for all visible untagged files that match arg. + + :param tree: The tree to look for untagged files in + :type tree: `arch.WorkingTree` + :param arg: The argument to match + :type arg: str + :return: An iterator of all matching untagged files + :rtype: iterator of str + """ + treepath = arch_compound.tree_cwd(tree) + if len(treepath) > 0: + dirs = [treepath] + else: + dirs = None + + for file in iter_untagged(tree, dirs): + file = file_completion_match(file, treepath, arg) + if file is not None: + yield file + + +def file_completion_match(file, treepath, arg): + """Determines whether a file within an arch tree matches the argument. + + :param file: The rooted filename + :type file: str + :param treepath: The path to the cwd within the tree + :type treepath: str + :param arg: The prefix to match + :return: The completion name, or None if not a match + :rtype: str + """ + if not file.startswith(treepath): + return None + if treepath != "": + file = file[len(treepath)+1:] + + if not file.startswith(arg): + return None + if os.path.isdir(file): + file += '/' + return file + +def iter_modified_file_completions(tree, arg): + """Returns a list of modified files that match the specified prefix. + + :param tree: The current tree + :type tree: `arch.WorkingTree` + :param arg: The prefix to match + :type arg: str + """ + treepath = arch_compound.tree_cwd(tree) + tmpdir = util.tmpdir() + changeset = tmpdir+"/changeset" + completions = [] + revision = cmdutil.determine_revision_tree(tree) + for line in arch.iter_delta(revision, tree, changeset): + if isinstance(line, arch.FileModification): + file = file_completion_match(line.name[1:], treepath, arg) + if file is not None: + completions.append(file) + shutil.rmtree(tmpdir) + return completions + +class Shell(BaseCommand): + def __init__(self): + self.description = "Runs Fai as a shell" + + def do_command(self, cmdargs): + if len(cmdargs)!=0: + raise cmdutil.GetHelp + prompt = PromptCmd() + try: + prompt.cmdloop() + finally: + prompt.write_history() + +class AddID(BaseCommand): + """ + Adds an inventory id for the given file + """ + def __init__(self): + self.description="Add an inventory id for a given file" + + def get_completer(self, arg, index): + tree = arch.tree_root() + return iter_untagged_completions(tree, arg) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + + try: + tree = arch.tree_root() + except arch.errors.TreeRootError, e: + raise pylon.errors.CommandFailedWrapper(e) + + + if (len(args) == 0) == (options.untagged == False): + raise cmdutil.GetHelp + + #if options.id and len(args) != 1: + # print "If --id is specified, only one file can be named." + # return + + method = tree.tagging_method + + if options.id_type == "tagline": + if method != "tagline": + if not cmdutil.prompt("Tagline in other tree"): + if method == "explicit" or method == "implicit": + options.id_type == method + else: + print "add-id not supported for \"%s\" tagging method"\ + % method + return + + elif options.id_type == "implicit": + if method != "implicit": + if not cmdutil.prompt("Implicit in other tree"): + if method == "explicit" or method == "tagline": + options.id_type == method + else: + print "add-id not supported for \"%s\" tagging method"\ + % method + return + elif options.id_type == "explicit": + if method != "tagline" and method != explicit: + if not prompt("Explicit in other tree"): + print "add-id not supported for \"%s\" tagging method" % \ + method + return + + if options.id_type == "auto": + if method != "tagline" and method != "explicit" \ + and method !="implicit": + print "add-id not supported for \"%s\" tagging method" % method + return + else: + options.id_type = method + if options.untagged: + args = None + self.add_ids(tree, options.id_type, args) + + def add_ids(self, tree, id_type, files=()): + """Add inventory ids to files. + + :param tree: the tree the files are in + :type tree: `arch.WorkingTree` + :param id_type: the type of id to add: "explicit" or "tagline" + :type id_type: str + :param files: The list of files to add. If None do all untagged. + :type files: tuple of str + """ + + untagged = (files is None) + if untagged: + files = list(iter_untagged(tree, None)) + previous_files = [] + while len(files) > 0: + previous_files.extend(files) + if id_type == "explicit": + cmdutil.add_id(files) + elif id_type == "tagline" or id_type == "implicit": + for file in files: + try: + implicit = (id_type == "implicit") + cmdutil.add_tagline_or_explicit_id(file, False, + implicit) + except cmdutil.AlreadyTagged: + print "\"%s\" already has a tagline." % file + except cmdutil.NoCommentSyntax: + pass + #do inventory after tagging until no untagged files are encountered + if untagged: + files = [] + for file in iter_untagged(tree, None): + if not file in previous_files: + files.append(file) + + else: + break + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai add-id file1 [file2] [file3]...") +# ddaa suggests removing this to promote GUIDs. Let's see who squalks. +# parser.add_option("-i", "--id", dest="id", +# help="Specify id for a single file", default=None) + parser.add_option("--tltl", action="store_true", + dest="lord_style", help="Use Tom Lord's style of id.") + parser.add_option("--explicit", action="store_const", + const="explicit", dest="id_type", + help="Use an explicit id", default="auto") + parser.add_option("--tagline", action="store_const", + const="tagline", dest="id_type", + help="Use a tagline id") + parser.add_option("--implicit", action="store_const", + const="implicit", dest="id_type", + help="Use an implicit id (deprecated)") + parser.add_option("--untagged", action="store_true", + dest="untagged", default=False, + help="tag all untagged files") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Adds an inventory to the specified file(s) and directories. If --untagged is +specified, adds inventory to all untagged files and directories. + """ + return + + +class Merge(BaseCommand): + """ + Merges changes from other versions into the current tree + """ + def __init__(self): + self.description="Merges changes from other versions" + try: + self.tree = arch.tree_root() + except: + self.tree = None + + + def get_completer(self, arg, index): + if self.tree is None: + raise arch.errors.TreeRootError + return cmdutil.merge_completions(self.tree, arg, index) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "merge" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + if options.diff3: + action="star-merge" + else: + action = options.action + + if self.tree is None: + raise arch.errors.TreeRootError(os.getcwd()) + if cmdutil.has_changed(ancillary.comp_revision(self.tree)): + raise UncommittedChanges(self.tree) + + if len(args) > 0: + revisions = [] + for arg in args: + revisions.append(cmdutil.determine_revision_arch(self.tree, + arg)) + source = "from commandline" + else: + revisions = ancillary.iter_partner_revisions(self.tree, + self.tree.tree_version) + source = "from partner version" + revisions = misc.rewind_iterator(revisions) + try: + revisions.next() + revisions.rewind() + except StopIteration, e: + revision = cmdutil.tag_cur(self.tree) + if revision is None: + raise CantDetermineRevision("", "No version specified, no " + "partner-versions, and no tag" + " source") + revisions = [revision] + source = "from tag source" + for revision in revisions: + cmdutil.ensure_archive_registered(revision.archive) + cmdutil.colorize(arch.Chatter("* Merging %s [%s]" % + (revision, source))) + if action=="native-merge" or action=="update": + if self.native_merge(revision, action) == 0: + continue + elif action=="star-merge": + try: + self.star_merge(revision, options.diff3) + except errors.MergeProblem, e: + break + if cmdutil.has_changed(self.tree.tree_version): + break + + def star_merge(self, revision, diff3): + """Perform a star-merge on the current tree. + + :param revision: The revision to use for the merge + :type revision: `arch.Revision` + :param diff3: If true, do a diff3 merge + :type diff3: bool + """ + try: + for line in self.tree.iter_star_merge(revision, diff3=diff3): + cmdutil.colorize(line) + except arch.util.ExecProblem, e: + if e.proc.status is not None and e.proc.status == 1: + if e.proc.error: + print e.proc.error + raise MergeProblem + else: + raise + + def native_merge(self, other_revision, action): + """Perform a native-merge on the current tree. + + :param other_revision: The revision to use for the merge + :type other_revision: `arch.Revision` + :return: 0 if the merge was skipped, 1 if it was applied + """ + other_tree = arch_compound.find_or_make_local_revision(other_revision) + try: + if action == "native-merge": + ancestor = arch_compound.merge_ancestor2(self.tree, other_tree, + other_revision) + elif action == "update": + ancestor = arch_compound.tree_latest(self.tree, + other_revision.version) + except CantDetermineRevision, e: + raise CommandFailedWrapper(e) + cmdutil.colorize(arch.Chatter("* Found common ancestor %s" % ancestor)) + if (ancestor == other_revision): + cmdutil.colorize(arch.Chatter("* Skipping redundant merge" + % ancestor)) + return 0 + delta = cmdutil.apply_delta(ancestor, other_tree, self.tree) + for line in cmdutil.iter_apply_delta_filter(delta): + cmdutil.colorize(line) + return 1 + + + + def get_parser(self): + """ + Returns the options parser to use for the "merge" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai merge [VERSION]") + parser.add_option("-s", "--star-merge", action="store_const", + dest="action", help="Use star-merge", + const="star-merge", default="native-merge") + parser.add_option("--update", action="store_const", + dest="action", help="Use update picker", + const="update") + parser.add_option("--diff3", action="store_true", + dest="diff3", + help="Use diff3 for merge (implies star-merge)") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Performs a merge operation using the specified version. + """ + return + +class ELog(BaseCommand): + """ + Produces a raw patchlog and invokes the user's editor + """ + def __init__(self): + self.description="Edit a patchlog to commit" + try: + self.tree = arch.tree_root() + except: + self.tree = None + + + def do_command(self, cmdargs): + """ + Master function that perfoms the "elog" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + if self.tree is None: + raise arch.errors.TreeRootError + + try: + edit_log(self.tree, self.tree.tree_version) + except pylon.errors.NoEditorSpecified, e: + raise pylon.errors.CommandFailedWrapper(e) + + def get_parser(self): + """ + Returns the options parser to use for the "merge" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai elog") + return parser + + + def help(self, parser=None): + """ + Invokes $EDITOR to produce a log for committing. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Invokes $EDITOR to produce a log for committing. + """ + return + +def edit_log(tree, version): + """Makes and edits the log for a tree. Does all kinds of fancy things + like log templates and merge summaries and log-for-merge + + :param tree: The tree to edit the log for + :type tree: `arch.WorkingTree` + """ + #ensure we have an editor before preparing the log + cmdutil.find_editor() + log = tree.log_message(create=False, version=version) + log_is_new = False + if log is None or cmdutil.prompt("Overwrite log"): + if log is not None: + os.remove(log.name) + log = tree.log_message(create=True, version=version) + log_is_new = True + tmplog = log.name + template = pylon.log_template_path(tree) + if template: + shutil.copyfile(template, tmplog) + comp_version = ancillary.comp_revision(tree).version + new_merges = cmdutil.iter_new_merges(tree, comp_version) + new_merges = cmdutil.direct_merges(new_merges) + log["Summary"] = pylon.merge_summary(new_merges, + version) + if len(new_merges) > 0: + if cmdutil.prompt("Log for merge"): + if cmdutil.prompt("changelog for merge"): + mergestuff = "Patches applied:\n" + mergestuff += pylon.changelog_for_merge(new_merges) + else: + mergestuff = cmdutil.log_for_merge(tree, comp_version) + log.description += mergestuff + log.save() + try: + cmdutil.invoke_editor(log.name) + except: + if log_is_new: + os.remove(log.name) + raise + + +class MirrorArchive(BaseCommand): + """ + Updates a mirror from an archive + """ + def __init__(self): + self.description="Update a mirror from an archive" + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + if len(args) > 1: + raise GetHelp + try: + tree = arch.tree_root() + except: + tree = None + + if len(args) == 0: + if tree is not None: + name = tree.tree_version() + else: + name = cmdutil.expand_alias(args[0], tree) + name = arch.NameParser(name) + + to_arch = name.get_archive() + from_arch = cmdutil.get_mirror_source(arch.Archive(to_arch)) + limit = name.get_nonarch() + + iter = arch_core.mirror_archive(from_arch,to_arch, limit) + for line in arch.chatter_classifier(iter): + cmdutil.colorize(line) + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai mirror-archive ARCHIVE") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Updates a mirror from an archive. If a branch, package, or version is +supplied, only changes under it are mirrored. + """ + return + +def help_tree_spec(): + print """Specifying revisions (default: tree) +Revisions may be specified by alias, revision, version or patchlevel. +Revisions or versions may be fully qualified. Unqualified revisions, versions, +or patchlevels use the archive of the current project tree. Versions will +use the latest patchlevel in the tree. Patchlevels will use the current tree- +version. + +Use "alias" to list available (user and automatic) aliases.""" + +auto_alias = [ +"acur", +"The latest revision in the archive of the tree-version. You can specify \ +a different version like so: acur:foo--bar--0 (aliases can be used)", +"tcur", +"""(tree current) The latest revision in the tree of the tree-version. \ +You can specify a different version like so: tcur:foo--bar--0 (aliases can be \ +used).""", +"tprev" , +"""(tree previous) The previous revision in the tree of the tree-version. To \ +specify an older revision, use a number, e.g. "tprev:4" """, +"tanc" , +"""(tree ancestor) The ancestor revision of the tree To specify an older \ +revision, use a number, e.g. "tanc:4".""", +"tdate" , +"""(tree date) The latest revision from a given date, e.g. "tdate:July 6".""", +"tmod" , +""" (tree modified) The latest revision to modify a given file, e.g. \ +"tmod:engine.cpp" or "tmod:engine.cpp:16".""", +"ttag" , +"""(tree tag) The revision that was tagged into the current tree revision, \ +according to the tree""", +"tagcur", +"""(tag current) The latest revision of the version that the current tree \ +was tagged from.""", +"mergeanc" , +"""The common ancestor of the current tree and the specified revision. \ +Defaults to the first partner-version's latest revision or to tagcur.""", +] + + +def is_auto_alias(name): + """Determine whether a name is an auto alias name + + :param name: the name to check + :type name: str + :return: True if the name is an auto alias, false if not + :rtype: bool + """ + return name in [f for (f, v) in pylon.util.iter_pairs(auto_alias)] + + +def display_def(iter, wrap = 80): + """Display a list of definitions + + :param iter: iter of name, definition pairs + :type iter: iter of (str, str) + :param wrap: The width for text wrapping + :type wrap: int + """ + vals = list(iter) + maxlen = 0 + for (key, value) in vals: + if len(key) > maxlen: + maxlen = len(key) + for (key, value) in vals: + tw=textwrap.TextWrapper(width=wrap, + initial_indent=key.rjust(maxlen)+" : ", + subsequent_indent="".rjust(maxlen+3)) + print tw.fill(value) + + +def help_aliases(tree): + print """Auto-generated aliases""" + display_def(pylon.util.iter_pairs(auto_alias)) + print "User aliases" + display_def(ancillary.iter_all_alias(tree)) + +class Inventory(BaseCommand): + """List the status of files in the tree""" + def __init__(self): + self.description=self.__doc__ + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + tree = arch.tree_root() + categories = [] + + if (options.source): + categories.append(arch_core.SourceFile) + if (options.precious): + categories.append(arch_core.PreciousFile) + if (options.backup): + categories.append(arch_core.BackupFile) + if (options.junk): + categories.append(arch_core.JunkFile) + + if len(categories) == 1: + show_leading = False + else: + show_leading = True + + if len(categories) == 0: + categories = None + + if options.untagged: + categories = arch_core.non_root + show_leading = False + tagged = False + else: + tagged = None + + for file in arch_core.iter_inventory_filter(tree, None, + control_files=options.control_files, + categories = categories, tagged=tagged): + print arch_core.file_line(file, + category = show_leading, + untagged = show_leading, + id = options.ids) + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai inventory [options]") + parser.add_option("--ids", action="store_true", dest="ids", + help="Show file ids") + parser.add_option("--control", action="store_true", + dest="control_files", help="include control files") + parser.add_option("--source", action="store_true", dest="source", + help="List source files") + parser.add_option("--backup", action="store_true", dest="backup", + help="List backup files") + parser.add_option("--precious", action="store_true", dest="precious", + help="List precious files") + parser.add_option("--junk", action="store_true", dest="junk", + help="List junk files") + parser.add_option("--unrecognized", action="store_true", + dest="unrecognized", help="List unrecognized files") + parser.add_option("--untagged", action="store_true", + dest="untagged", help="List only untagged files") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Lists the status of files in the archive: +S source +P precious +B backup +J junk +U unrecognized +T tree root +? untagged-source +Leading letter are not displayed if only one kind of file is shown + """ + return + + +class Alias(BaseCommand): + """List or adjust aliases""" + def __init__(self): + self.description=self.__doc__ + + def get_completer(self, arg, index): + if index > 2: + return () + try: + self.tree = arch.tree_root() + except: + self.tree = None + + if index == 0: + return [part[0]+" " for part in ancillary.iter_all_alias(self.tree)] + elif index == 1: + return cmdutil.iter_revision_completions(arg, self.tree) + + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: + self.tree = arch.tree_root() + except: + self.tree = None + + + try: + options.action(args, options) + except cmdutil.ForbiddenAliasSyntax, e: + raise CommandFailedWrapper(e) + + def no_prefix(self, alias): + if alias.startswith("^"): + alias = alias[1:] + return alias + + def arg_dispatch(self, args, options): + """Add, modify, or list aliases, depending on number of arguments + + :param args: The list of commandline arguments + :type args: list of str + :param options: The commandline options + """ + if len(args) == 0: + help_aliases(self.tree) + return + else: + alias = self.no_prefix(args[0]) + if len(args) == 1: + self.print_alias(alias) + elif (len(args)) == 2: + self.add(alias, args[1], options) + else: + raise cmdutil.GetHelp + + def print_alias(self, alias): + answer = None + if is_auto_alias(alias): + raise pylon.errors.IsAutoAlias(alias, "\"%s\" is an auto alias." + " Use \"revision\" to expand auto aliases." % alias) + for pair in ancillary.iter_all_alias(self.tree): + if pair[0] == alias: + answer = pair[1] + if answer is not None: + print answer + else: + print "The alias %s is not assigned." % alias + + def add(self, alias, expansion, options): + """Add or modify aliases + + :param alias: The alias name to create/modify + :type alias: str + :param expansion: The expansion to assign to the alias name + :type expansion: str + :param options: The commandline options + """ + if is_auto_alias(alias): + raise IsAutoAlias(alias) + newlist = "" + written = False + new_line = "%s=%s\n" % (alias, cmdutil.expand_alias(expansion, + self.tree)) + ancillary.check_alias(new_line.rstrip("\n"), [alias, expansion]) + + for pair in self.get_iterator(options): + if pair[0] != alias: + newlist+="%s=%s\n" % (pair[0], pair[1]) + elif not written: + newlist+=new_line + written = True + if not written: + newlist+=new_line + self.write_aliases(newlist, options) + + def delete(self, args, options): + """Delete the specified alias + + :param args: The list of arguments + :type args: list of str + :param options: The commandline options + """ + deleted = False + if len(args) != 1: + raise cmdutil.GetHelp + alias = self.no_prefix(args[0]) + if is_auto_alias(alias): + raise IsAutoAlias(alias) + newlist = "" + for pair in self.get_iterator(options): + if pair[0] != alias: + newlist+="%s=%s\n" % (pair[0], pair[1]) + else: + deleted = True + if not deleted: + raise errors.NoSuchAlias(alias) + self.write_aliases(newlist, options) + + def get_alias_file(self, options): + """Return the name of the alias file to use + + :param options: The commandline options + """ + if options.tree: + if self.tree is None: + self.tree == arch.tree_root() + return str(self.tree)+"/{arch}/+aliases" + else: + return "~/.aba/aliases" + + def get_iterator(self, options): + """Return the alias iterator to use + + :param options: The commandline options + """ + return ancillary.iter_alias(self.get_alias_file(options)) + + def write_aliases(self, newlist, options): + """Safely rewrite the alias file + :param newlist: The new list of aliases + :type newlist: str + :param options: The commandline options + """ + filename = os.path.expanduser(self.get_alias_file(options)) + file = util.NewFileVersion(filename) + file.write(newlist) + file.commit() + + + def get_parser(self): + """ + Returns the options parser to use for the "alias" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai alias [ALIAS] [NAME]") + parser.add_option("-d", "--delete", action="store_const", dest="action", + const=self.delete, default=self.arg_dispatch, + help="Delete an alias") + parser.add_option("--tree", action="store_true", dest="tree", + help="Create a per-tree alias", default=False) + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Lists current aliases or modifies the list of aliases. + +If no arguments are supplied, aliases will be listed. If two arguments are +supplied, the specified alias will be created or modified. If -d or --delete +is supplied, the specified alias will be deleted. + +You can create aliases that refer to any fully-qualified part of the +Arch namespace, e.g. +archive, +archive/category, +archive/category--branch, +archive/category--branch--version (my favourite) +archive/category--branch--version--patchlevel + +Aliases can be used automatically by native commands. To use them +with external or tla commands, prefix them with ^ (you can do this +with native commands, too). +""" + + +class RequestMerge(BaseCommand): + """Submit a merge request to Bug Goo""" + def __init__(self): + self.description=self.__doc__ + + def do_command(self, cmdargs): + """Submit a merge request + + :param cmdargs: The commandline arguments + :type cmdargs: list of str + """ + parser = self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: + cmdutil.find_editor() + except pylon.errors.NoEditorSpecified, e: + raise pylon.errors.CommandFailedWrapper(e) + try: + self.tree=arch.tree_root() + except: + self.tree=None + base, revisions = self.revision_specs(args) + message = self.make_headers(base, revisions) + message += self.make_summary(revisions) + path = self.edit_message(message) + message = self.tidy_message(path) + if cmdutil.prompt("Send merge"): + self.send_message(message) + print "Merge request sent" + + def make_headers(self, base, revisions): + """Produce email and Bug Goo header strings + + :param base: The base revision to apply merges to + :type base: `arch.Revision` + :param revisions: The revisions to replay into the base + :type revisions: list of `arch.Patchlog` + :return: The headers + :rtype: str + """ + headers = "To: gnu-arch-users@gnu.org\n" + headers += "From: %s\n" % options.fromaddr + if len(revisions) == 1: + headers += "Subject: [MERGE REQUEST] %s\n" % revisions[0].summary + else: + headers += "Subject: [MERGE REQUEST]\n" + headers += "\n" + headers += "Base-Revision: %s\n" % base + for revision in revisions: + headers += "Revision: %s\n" % revision.revision + headers += "Bug: \n\n" + return headers + + def make_summary(self, logs): + """Generate a summary of merges + + :param logs: the patchlogs that were directly added by the merges + :type logs: list of `arch.Patchlog` + :return: the summary + :rtype: str + """ + summary = "" + for log in logs: + summary+=str(log.revision)+"\n" + summary+=log.summary+"\n" + if log.description.strip(): + summary+=log.description.strip('\n')+"\n\n" + return summary + + def revision_specs(self, args): + """Determine the base and merge revisions from tree and arguments. + + :param args: The parsed arguments + :type args: list of str + :return: The base revision and merge revisions + :rtype: `arch.Revision`, list of `arch.Patchlog` + """ + if len(args) > 0: + target_revision = cmdutil.determine_revision_arch(self.tree, + args[0]) + else: + target_revision = arch_compound.tree_latest(self.tree) + if len(args) > 1: + merges = [ arch.Patchlog(cmdutil.determine_revision_arch( + self.tree, f)) for f in args[1:] ] + else: + if self.tree is None: + raise CantDetermineRevision("", "Not in a project tree") + merge_iter = cmdutil.iter_new_merges(self.tree, + target_revision.version, + False) + merges = [f for f in cmdutil.direct_merges(merge_iter)] + return (target_revision, merges) + + def edit_message(self, message): + """Edit an email message in the user's standard editor + + :param message: The message to edit + :type message: str + :return: the path of the edited message + :rtype: str + """ + if self.tree is None: + path = os.get_cwd() + else: + path = self.tree + path += "/,merge-request" + file = open(path, 'w') + file.write(message) + file.flush() + cmdutil.invoke_editor(path) + return path + + def tidy_message(self, path): + """Validate and clean up message. + + :param path: The path to the message to clean up + :type path: str + :return: The parsed message + :rtype: `email.Message` + """ + mail = email.message_from_file(open(path)) + if mail["Subject"].strip() == "[MERGE REQUEST]": + raise BlandSubject + + request = email.message_from_string(mail.get_payload()) + if request.has_key("Bug"): + if request["Bug"].strip()=="": + del request["Bug"] + mail.set_payload(request.as_string()) + return mail + + def send_message(self, message): + """Send a message, using its headers to address it. + + :param message: The message to send + :type message: `email.Message`""" + server = smtplib.SMTP("localhost") + server.sendmail(message['From'], message['To'], message.as_string()) + server.quit() + + def help(self, parser=None): + """Print a usage message + + :param parser: The options parser to use + :type parser: `cmdutil.CmdOptionParser` + """ + if parser is None: + parser = self.get_parser() + parser.print_help() + print """ +Sends a merge request formatted for Bug Goo. Intended use: get the tree +you'd like to merge into. Apply the merges you want. Invoke request-merge. +The merge request will open in your $EDITOR. + +When no TARGET is specified, it uses the current tree revision. When +no MERGE is specified, it uses the direct merges (as in "revisions +--direct-merges"). But you can specify just the TARGET, or all the MERGE +revisions. +""" + + def get_parser(self): + """Produce a commandline parser for this command. + + :rtype: `cmdutil.CmdOptionParser` + """ + parser=cmdutil.CmdOptionParser("request-merge [TARGET] [MERGE1...]") + return parser + +commands = { +'changes' : Changes, +'help' : Help, +'update': Update, +'apply-changes':ApplyChanges, +'cat-log': CatLog, +'commit': Commit, +'revision': Revision, +'revisions': Revisions, +'get': Get, +'revert': Revert, +'shell': Shell, +'add-id': AddID, +'merge': Merge, +'elog': ELog, +'mirror-archive': MirrorArchive, +'ninventory': Inventory, +'alias' : Alias, +'request-merge': RequestMerge, +} + +def my_import(mod_name): + module = __import__(mod_name) + components = mod_name.split('.') + for comp in components[1:]: + module = getattr(module, comp) + return module + +def plugin(mod_name): + module = my_import(mod_name) + module.add_command(commands) + +for file in os.listdir(sys.path[0]+"/command"): + if len(file) > 3 and file[-3:] == ".py" and file != "__init__.py": + plugin("command."+file[:-3]) + +suggestions = { +'apply-delta' : "Try \"apply-changes\".", +'delta' : "To compare two revisions, use \"changes\".", +'diff-rev' : "To compare two revisions, use \"changes\".", +'undo' : "To undo local changes, use \"revert\".", +'undelete' : "To undo only deletions, use \"revert --deletions\"", +'missing-from' : "Try \"revisions --missing-from\".", +'missing' : "Try \"revisions --missing\".", +'missing-merge' : "Try \"revisions --partner-missing\".", +'new-merges' : "Try \"revisions --new-merges\".", +'cachedrevs' : "Try \"revisions --cacherevs\". (no 'd')", +'logs' : "Try \"revisions --logs\"", +'tree-source' : "Use the \"^ttag\" alias (\"revision ^ttag\")", +'latest-revision' : "Use the \"^acur\" alias (\"revision ^acur\")", +'change-version' : "Try \"update REVISION\"", +'tree-revision' : "Use the \"^tcur\" alias (\"revision ^tcur\")", +'rev-depends' : "Use revisions --dependencies", +'auto-get' : "Plain get will do archive lookups", +'tagline' : "Use add-id. It uses taglines in tagline trees", +'emlog' : "Use elog. It automatically adds log-for-merge text, if any", +'library-revisions' : "Use revisions --library", +'file-revert' : "Use revert FILE", +'join-branch' : "Use replay --logs-only" +} +# arch-tag: 19d5739d-3708-486c-93ba-deecc3027fc7 *** added file 'testdata/orig' --- /dev/null +++ testdata/orig @@ -0,0 +1,2789 @@ +# Copyright (C) 2004 Aaron Bentley +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import sys +import arch +import arch.util +import arch.arch +import abacmds +import cmdutil +import shutil +import os +import options +import paths +import time +import cmd +import readline +import re +import string +import arch_core +from errors import * +import errors +import terminal +import ancillary +import misc +import email +import smtplib + +__docformat__ = "restructuredtext" +__doc__ = "Implementation of user (sub) commands" +commands = {} + +def find_command(cmd): + """ + Return an instance of a command type. Return None if the type isn't + registered. + + :param cmd: the name of the command to look for + :type cmd: the type of the command + """ + if commands.has_key(cmd): + return commands[cmd]() + else: + return None + +class BaseCommand: + def __call__(self, cmdline): + try: + self.do_command(cmdline.split()) + except cmdutil.GetHelp, e: + self.help() + except Exception, e: + print e + + def get_completer(index): + return None + + def complete(self, args, text): + """ + Returns a list of possible completions for the given text. + + :param args: The complete list of arguments + :type args: List of str + :param text: text to complete (may be shorter than args[-1]) + :type text: str + :rtype: list of str + """ + matches = [] + candidates = None + + if len(args) > 0: + realtext = args[-1] + else: + realtext = "" + + try: + parser=self.get_parser() + if realtext.startswith('-'): + candidates = parser.iter_options() + else: + (options, parsed_args) = parser.parse_args(args) + + if len (parsed_args) > 0: + candidates = self.get_completer(parsed_args[-1], len(parsed_args) -1) + else: + candidates = self.get_completer("", 0) + except: + pass + if candidates is None: + return + for candidate in candidates: + candidate = str(candidate) + if candidate.startswith(realtext): + matches.append(candidate[len(realtext)- len(text):]) + return matches + + +class Help(BaseCommand): + """ + Lists commands, prints help messages. + """ + def __init__(self): + self.description="Prints help mesages" + self.parser = None + + def do_command(self, cmdargs): + """ + Prints a help message. + """ + options, args = self.get_parser().parse_args(cmdargs) + if len(args) > 1: + raise cmdutil.GetHelp + + if options.native or options.suggestions or options.external: + native = options.native + suggestions = options.suggestions + external = options.external + else: + native = True + suggestions = False + external = True + + if len(args) == 0: + self.list_commands(native, suggestions, external) + return + elif len(args) == 1: + command_help(args[0]) + return + + def help(self): + self.get_parser().print_help() + print """ +If no command is specified, commands are listed. If a command is +specified, help for that command is listed. + """ + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + if self.parser is not None: + return self.parser + parser=cmdutil.CmdOptionParser("fai help [command]") + parser.add_option("-n", "--native", action="store_true", + dest="native", help="Show native commands") + parser.add_option("-e", "--external", action="store_true", + dest="external", help="Show external commands") + parser.add_option("-s", "--suggest", action="store_true", + dest="suggestions", help="Show suggestions") + self.parser = parser + return parser + + def list_commands(self, native=True, suggest=False, external=True): + """ + Lists supported commands. + + :param native: list native, python-based commands + :type native: bool + :param external: list external aba-style commands + :type external: bool + """ + if native: + print "Native Fai commands" + keys=commands.keys() + keys.sort() + for k in keys: + space="" + for i in range(28-len(k)): + space+=" " + print space+k+" : "+commands[k]().description + print + if suggest: + print "Unavailable commands and suggested alternatives" + key_list = suggestions.keys() + key_list.sort() + for key in key_list: + print "%28s : %s" % (key, suggestions[key]) + print + if external: + fake_aba = abacmds.AbaCmds() + if (fake_aba.abadir == ""): + return + print "External commands" + fake_aba.list_commands() + print + if not suggest: + print "Use help --suggest to list alternatives to tla and aba"\ + " commands." + if options.tla_fallthrough and (native or external): + print "Fai also supports tla commands." + +def command_help(cmd): + """ + Prints help for a command. + + :param cmd: The name of the command to print help for + :type cmd: str + """ + fake_aba = abacmds.AbaCmds() + cmdobj = find_command(cmd) + if cmdobj != None: + cmdobj.help() + elif suggestions.has_key(cmd): + print "Not available\n" + suggestions[cmd] + else: + abacmd = fake_aba.is_command(cmd) + if abacmd: + abacmd.help() + else: + print "No help is available for \""+cmd+"\". Maybe try \"tla "+cmd+" -H\"?" + + + +class Changes(BaseCommand): + """ + the "changes" command: lists differences between trees/revisions: + """ + + def __init__(self): + self.description="Lists what files have changed in the project tree" + + def get_completer(self, arg, index): + if index > 1: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def parse_commandline(self, cmdline): + """ + Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. + + :param cmdline: A list of arguments to parse + :rtype: (options, Revision, Revision/WorkingTree) + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdline) + if len(args) > 2: + raise cmdutil.GetHelp + + tree=arch.tree_root() + if len(args) == 0: + a_spec = cmdutil.comp_revision(tree) + else: + a_spec = cmdutil.determine_revision_tree(tree, args[0]) + cmdutil.ensure_archive_registered(a_spec.archive) + if len(args) == 2: + b_spec = cmdutil.determine_revision_tree(tree, args[1]) + cmdutil.ensure_archive_registered(b_spec.archive) + else: + b_spec=tree + return options, a_spec, b_spec + + def do_command(self, cmdargs): + """ + Master function that perfoms the "changes" command. + """ + try: + options, a_spec, b_spec = self.parse_commandline(cmdargs); + except cmdutil.CantDetermineRevision, e: + print e + return + except arch.errors.TreeRootError, e: + print e + return + if options.changeset: + changeset=options.changeset + tmpdir = None + else: + tmpdir=cmdutil.tmpdir() + changeset=tmpdir+"/changeset" + try: + delta=arch.iter_delta(a_spec, b_spec, changeset) + try: + for line in delta: + if cmdutil.chattermatch(line, "changeset:"): + pass + else: + cmdutil.colorize(line, options.suppress_chatter) + except arch.util.ExecProblem, e: + if e.proc.error and e.proc.error.startswith( + "missing explicit id for file"): + raise MissingID(e) + else: + raise + status=delta.status + if status > 1: + return + if (options.perform_diff): + chan = cmdutil.ChangesetMunger(changeset) + chan.read_indices() + if isinstance(b_spec, arch.Revision): + b_dir = b_spec.library_find() + else: + b_dir = b_spec + a_dir = a_spec.library_find() + if options.diffopts is not None: + diffopts = options.diffopts.split() + cmdutil.show_custom_diffs(chan, diffopts, a_dir, b_dir) + else: + cmdutil.show_diffs(delta.changeset) + finally: + if tmpdir and (os.access(tmpdir, os.X_OK)): + shutil.rmtree(tmpdir) + + def get_parser(self): + """ + Returns the options parser to use for the "changes" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai changes [options] [revision]" + " [revision]") + parser.add_option("-d", "--diff", action="store_true", + dest="perform_diff", default=False, + help="Show diffs in summary") + parser.add_option("-c", "--changeset", dest="changeset", + help="Store a changeset in the given directory", + metavar="DIRECTORY") + parser.add_option("-s", "--silent", action="store_true", + dest="suppress_chatter", default=False, + help="Suppress chatter messages") + parser.add_option("--diffopts", dest="diffopts", + help="Use the specified diff options", + metavar="OPTIONS") + + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser is None: + parser=self.get_parser() + parser.print_help() + print """ +Performs source-tree comparisons + +If no revision is specified, the current project tree is compared to the +last-committed revision. If one revision is specified, the current project +tree is compared to that revision. If two revisions are specified, they are +compared to each other. + """ + help_tree_spec() + return + + +class ApplyChanges(BaseCommand): + """ + Apply differences between two revisions to a tree + """ + + def __init__(self): + self.description="Applies changes to a project tree" + + def get_completer(self, arg, index): + if index > 1: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def parse_commandline(self, cmdline, tree): + """ + Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. + + :param cmdline: A list of arguments to parse + :rtype: (options, Revision, Revision/WorkingTree) + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdline) + if len(args) != 2: + raise cmdutil.GetHelp + + a_spec = cmdutil.determine_revision_tree(tree, args[0]) + cmdutil.ensure_archive_registered(a_spec.archive) + b_spec = cmdutil.determine_revision_tree(tree, args[1]) + cmdutil.ensure_archive_registered(b_spec.archive) + return options, a_spec, b_spec + + def do_command(self, cmdargs): + """ + Master function that performs "apply-changes". + """ + try: + tree = arch.tree_root() + options, a_spec, b_spec = self.parse_commandline(cmdargs, tree); + except cmdutil.CantDetermineRevision, e: + print e + return + except arch.errors.TreeRootError, e: + print e + return + delta=cmdutil.apply_delta(a_spec, b_spec, tree) + for line in cmdutil.iter_apply_delta_filter(delta): + cmdutil.colorize(line, options.suppress_chatter) + + def get_parser(self): + """ + Returns the options parser to use for the "apply-changes" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai apply-changes [options] revision" + " revision") + parser.add_option("-d", "--diff", action="store_true", + dest="perform_diff", default=False, + help="Show diffs in summary") + parser.add_option("-c", "--changeset", dest="changeset", + help="Store a changeset in the given directory", + metavar="DIRECTORY") + parser.add_option("-s", "--silent", action="store_true", + dest="suppress_chatter", default=False, + help="Suppress chatter messages") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser is None: + parser=self.get_parser() + parser.print_help() + print """ +Applies changes to a project tree + +Compares two revisions and applies the difference between them to the current +tree. + """ + help_tree_spec() + return + +class Update(BaseCommand): + """ + Updates a project tree to a given revision, preserving un-committed hanges. + """ + + def __init__(self): + self.description="Apply the latest changes to the current directory" + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def parse_commandline(self, cmdline, tree): + """ + Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. + + :param cmdline: A list of arguments to parse + :rtype: (options, Revision, Revision/WorkingTree) + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdline) + if len(args) > 2: + raise cmdutil.GetHelp + + spec=None + if len(args)>0: + spec=args[0] + revision=cmdutil.determine_revision_arch(tree, spec) + cmdutil.ensure_archive_registered(revision.archive) + + mirror_source = cmdutil.get_mirror_source(revision.archive) + if mirror_source != None: + if cmdutil.prompt("Mirror update"): + cmd=cmdutil.mirror_archive(mirror_source, + revision.archive, arch.NameParser(revision).get_package_version()) + for line in arch.chatter_classifier(cmd): + cmdutil.colorize(line, options.suppress_chatter) + + revision=cmdutil.determine_revision_arch(tree, spec) + + return options, revision + + def do_command(self, cmdargs): + """ + Master function that perfoms the "update" command. + """ + tree=arch.tree_root() + try: + options, to_revision = self.parse_commandline(cmdargs, tree); + except cmdutil.CantDetermineRevision, e: + print e + return + except arch.errors.TreeRootError, e: + print e + return + from_revision=cmdutil.tree_latest(tree) + if from_revision==to_revision: + print "Tree is already up to date with:\n"+str(to_revision)+"." + return + cmdutil.ensure_archive_registered(from_revision.archive) + cmd=cmdutil.apply_delta(from_revision, to_revision, tree, + options.patch_forward) + for line in cmdutil.iter_apply_delta_filter(cmd): + cmdutil.colorize(line) + if to_revision.version != tree.tree_version: + if cmdutil.prompt("Update version"): + tree.tree_version = to_revision.version + + def get_parser(self): + """ + Returns the options parser to use for the "update" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai update [options]" + " [revision/version]") + parser.add_option("-f", "--forward", action="store_true", + dest="patch_forward", default=False, + help="pass the --forward option to 'patch'") + parser.add_option("-s", "--silent", action="store_true", + dest="suppress_chatter", default=False, + help="Suppress chatter messages") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser is None: + parser=self.get_parser() + parser.print_help() + print """ +Updates a working tree to the current archive revision + +If a revision or version is specified, that is used instead + """ + help_tree_spec() + return + + +class Commit(BaseCommand): + """ + Create a revision based on the changes in the current tree. + """ + + def __init__(self): + self.description="Write local changes to the archive" + + def get_completer(self, arg, index): + if arg is None: + arg = "" + return iter_modified_file_completions(arch.tree_root(), arg) +# return iter_source_file_completions(arch.tree_root(), arg) + + def parse_commandline(self, cmdline, tree): + """ + Parse commandline arguments. Raise cmtutil.GetHelp if help is needed. + + :param cmdline: A list of arguments to parse + :rtype: (options, Revision, Revision/WorkingTree) + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdline) + + if len(args) == 0: + args = None + revision=cmdutil.determine_revision_arch(tree, options.version) + return options, revision.get_version(), args + + def do_command(self, cmdargs): + """ + Master function that perfoms the "commit" command. + """ + tree=arch.tree_root() + options, version, files = self.parse_commandline(cmdargs, tree) + if options.__dict__.has_key("base") and options.base: + base = cmdutil.determine_revision_tree(tree, options.base) + else: + base = cmdutil.submit_revision(tree) + + writeversion=version + archive=version.archive + source=cmdutil.get_mirror_source(archive) + allow_old=False + writethrough="implicit" + + if source!=None: + if writethrough=="explicit" and \ + cmdutil.prompt("Writethrough"): + writeversion=arch.Version(str(source)+"/"+str(version.get_nonarch())) + elif writethrough=="none": + raise CommitToMirror(archive) + + elif archive.is_mirror: + raise CommitToMirror(archive) + + try: + last_revision=tree.iter_logs(version, True).next().revision + except StopIteration, e: + if cmdutil.prompt("Import from commit"): + return do_import(version) + else: + raise NoVersionLogs(version) + if last_revision!=version.iter_revisions(True).next(): + if not cmdutil.prompt("Out of date"): + raise OutOfDate + else: + allow_old=True + + try: + if not cmdutil.has_changed(version): + if not cmdutil.prompt("Empty commit"): + raise EmptyCommit + except arch.util.ExecProblem, e: + if e.proc.error and e.proc.error.startswith( + "missing explicit id for file"): + raise MissingID(e) + else: + raise + log = tree.log_message(create=False) + if log is None: + try: + if cmdutil.prompt("Create log"): + edit_log(tree) + + except cmdutil.NoEditorSpecified, e: + raise CommandFailed(e) + log = tree.log_message(create=False) + if log is None: + raise NoLogMessage + if log["Summary"] is None or len(log["Summary"].strip()) == 0: + if not cmdutil.prompt("Omit log summary"): + raise errors.NoLogSummary + try: + for line in tree.iter_commit(version, seal=options.seal_version, + base=base, out_of_date_ok=allow_old, file_list=files): + cmdutil.colorize(line, options.suppress_chatter) + + except arch.util.ExecProblem, e: + if e.proc.error and e.proc.error.startswith( + "These files violate naming conventions:"): + raise LintFailure(e.proc.error) + else: + raise + + def get_parser(self): + """ + Returns the options parser to use for the "commit" command. + + :rtype: cmdutil.CmdOptionParser + """ + + parser=cmdutil.CmdOptionParser("fai commit [options] [file1]" + " [file2...]") + parser.add_option("--seal", action="store_true", + dest="seal_version", default=False, + help="seal this version") + parser.add_option("-v", "--version", dest="version", + help="Use the specified version", + metavar="VERSION") + parser.add_option("-s", "--silent", action="store_true", + dest="suppress_chatter", default=False, + help="Suppress chatter messages") + if cmdutil.supports_switch("commit", "--base"): + parser.add_option("--base", dest="base", help="", + metavar="REVISION") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser is None: + parser=self.get_parser() + parser.print_help() + print """ +Updates a working tree to the current archive revision + +If a version is specified, that is used instead + """ +# help_tree_spec() + return + + + +class CatLog(BaseCommand): + """ + Print the log of a given file (from current tree) + """ + def __init__(self): + self.description="Prints the patch log for a revision" + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "cat-log" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: + tree = arch.tree_root() + except arch.errors.TreeRootError, e: + tree = None + spec=None + if len(args) > 0: + spec=args[0] + if len(args) > 1: + raise cmdutil.GetHelp() + try: + if tree: + revision = cmdutil.determine_revision_tree(tree, spec) + else: + revision = cmdutil.determine_revision_arch(tree, spec) + except cmdutil.CantDetermineRevision, e: + raise CommandFailedWrapper(e) + log = None + + use_tree = (options.source == "tree" or \ + (options.source == "any" and tree)) + use_arch = (options.source == "archive" or options.source == "any") + + log = None + if use_tree: + for log in tree.iter_logs(revision.get_version()): + if log.revision == revision: + break + else: + log = None + if log is None and use_arch: + cmdutil.ensure_revision_exists(revision) + log = arch.Patchlog(revision) + if log is not None: + for item in log.items(): + print "%s: %s" % item + print log.description + + def get_parser(self): + """ + Returns the options parser to use for the "cat-log" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai cat-log [revision]") + parser.add_option("--archive", action="store_const", dest="source", + const="archive", default="any", + help="Always get the log from the archive") + parser.add_option("--tree", action="store_const", dest="source", + const="tree", help="Always get the log from the tree") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Prints the log for the specified revision + """ + help_tree_spec() + return + +class Revert(BaseCommand): + """ Reverts a tree (or aspects of it) to a revision + """ + def __init__(self): + self.description="Reverts a tree (or aspects of it) to a revision " + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return iter_modified_file_completions(tree, arg) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revert" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: + tree = arch.tree_root() + except arch.errors.TreeRootError, e: + raise CommandFailed(e) + spec=None + if options.revision is not None: + spec=options.revision + try: + if spec is not None: + revision = cmdutil.determine_revision_tree(tree, spec) + else: + revision = cmdutil.comp_revision(tree) + except cmdutil.CantDetermineRevision, e: + raise CommandFailedWrapper(e) + munger = None + + if options.file_contents or options.file_perms or options.deletions\ + or options.additions or options.renames or options.hunk_prompt: + munger = cmdutil.MungeOpts() + munger.hunk_prompt = options.hunk_prompt + + if len(args) > 0 or options.logs or options.pattern_files or \ + options.control: + if munger is None: + munger = cmdutil.MungeOpts(True) + munger.all_types(True) + if len(args) > 0: + t_cwd = cmdutil.tree_cwd(tree) + for name in args: + if len(t_cwd) > 0: + t_cwd += "/" + name = "./" + t_cwd + name + munger.add_keep_file(name); + + if options.file_perms: + munger.file_perms = True + if options.file_contents: + munger.file_contents = True + if options.deletions: + munger.deletions = True + if options.additions: + munger.additions = True + if options.renames: + munger.renames = True + if options.logs: + munger.add_keep_pattern('^\./\{arch\}/[^=].*') + if options.control: + munger.add_keep_pattern("/\.arch-ids|^\./\{arch\}|"\ + "/\.arch-inventory$") + if options.pattern_files: + munger.add_keep_pattern(options.pattern_files) + + for line in cmdutil.revert(tree, revision, munger, + not options.no_output): + cmdutil.colorize(line) + + + def get_parser(self): + """ + Returns the options parser to use for the "cat-log" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai revert [options] [FILE...]") + parser.add_option("", "--contents", action="store_true", + dest="file_contents", + help="Revert file content changes") + parser.add_option("", "--permissions", action="store_true", + dest="file_perms", + help="Revert file permissions changes") + parser.add_option("", "--deletions", action="store_true", + dest="deletions", + help="Restore deleted files") + parser.add_option("", "--additions", action="store_true", + dest="additions", + help="Remove added files") + parser.add_option("", "--renames", action="store_true", + dest="renames", + help="Revert file names") + parser.add_option("--hunks", action="store_true", + dest="hunk_prompt", default=False, + help="Prompt which hunks to revert") + parser.add_option("--pattern-files", dest="pattern_files", + help="Revert files that match this pattern", + metavar="REGEX") + parser.add_option("--logs", action="store_true", + dest="logs", default=False, + help="Revert only logs") + parser.add_option("--control-files", action="store_true", + dest="control", default=False, + help="Revert logs and other control files") + parser.add_option("-n", "--no-output", action="store_true", + dest="no_output", + help="Don't keep an undo changeset") + parser.add_option("--revision", dest="revision", + help="Revert to the specified revision", + metavar="REVISION") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Reverts changes in the current working tree. If no flags are specified, all +types of changes are reverted. Otherwise, only selected types of changes are +reverted. + +If a revision is specified on the commandline, differences between the current +tree and that revision are reverted. If a version is specified, the current +tree is used to determine the revision. + +If files are specified, only those files listed will have any changes applied. +To specify a renamed file, you can use either the old or new name. (or both!) + +Unless "-n" is specified, reversions can be undone with "redo". + """ + return + +class Revision(BaseCommand): + """ + Print a revision name based on a revision specifier + """ + def __init__(self): + self.description="Prints the name of a revision" + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + tree = None + + spec=None + if len(args) > 0: + spec=args[0] + if len(args) > 1: + raise cmdutil.GetHelp + try: + if tree: + revision = cmdutil.determine_revision_tree(tree, spec) + else: + revision = cmdutil.determine_revision_arch(tree, spec) + except cmdutil.CantDetermineRevision, e: + print str(e) + return + print options.display(revision) + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai revision [revision]") + parser.add_option("", "--location", action="store_const", + const=paths.determine_path, dest="display", + help="Show location instead of name", default=str) + parser.add_option("--import", action="store_const", + const=paths.determine_import_path, dest="display", + help="Show location of import file") + parser.add_option("--log", action="store_const", + const=paths.determine_log_path, dest="display", + help="Show location of log file") + parser.add_option("--patch", action="store_const", + dest="display", const=paths.determine_patch_path, + help="Show location of patchfile") + parser.add_option("--continuation", action="store_const", + const=paths.determine_continuation_path, + dest="display", + help="Show location of continuation file") + parser.add_option("--cacherev", action="store_const", + const=paths.determine_cacherev_path, dest="display", + help="Show location of cacherev file") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Expands aliases and prints the name of the specified revision. Instead of +the name, several options can be used to print locations. If more than one is +specified, the last one is used. + """ + help_tree_spec() + return + +def require_version_exists(version, spec): + if not version.exists(): + raise cmdutil.CantDetermineVersion(spec, + "The version %s does not exist." \ + % version) + +class Revisions(BaseCommand): + """ + Print a revision name based on a revision specifier + """ + def __init__(self): + self.description="Lists revisions" + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + (options, args) = self.get_parser().parse_args(cmdargs) + if len(args) > 1: + raise cmdutil.GetHelp + try: + self.tree = arch.tree_root() + except arch.errors.TreeRootError: + self.tree = None + try: + iter = self.get_iterator(options.type, args, options.reverse, + options.modified) + except cmdutil.CantDetermineRevision, e: + raise CommandFailedWrapper(e) + + if options.skip is not None: + iter = cmdutil.iter_skip(iter, int(options.skip)) + + for revision in iter: + log = None + if isinstance(revision, arch.Patchlog): + log = revision + revision=revision.revision + print options.display(revision) + if log is None and (options.summary or options.creator or + options.date or options.merges): + log = revision.patchlog + if options.creator: + print " %s" % log.creator + if options.date: + print " %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date) + if options.summary: + print " %s" % log.summary + if options.merges: + showed_title = False + for revision in log.merged_patches: + if not showed_title: + print " Merged:" + showed_title = True + print " %s" % revision + + def get_iterator(self, type, args, reverse, modified): + if len(args) > 0: + spec = args[0] + else: + spec = None + if modified is not None: + iter = cmdutil.modified_iter(modified, self.tree) + if reverse: + return iter + else: + return cmdutil.iter_reverse(iter) + elif type == "archive": + if spec is None: + if self.tree is None: + raise cmdutil.CantDetermineRevision("", + "Not in a project tree") + version = cmdutil.determine_version_tree(spec, self.tree) + else: + version = cmdutil.determine_version_arch(spec, self.tree) + cmdutil.ensure_archive_registered(version.archive) + require_version_exists(version, spec) + return version.iter_revisions(reverse) + elif type == "cacherevs": + if spec is None: + if self.tree is None: + raise cmdutil.CantDetermineRevision("", + "Not in a project tree") + version = cmdutil.determine_version_tree(spec, self.tree) + else: + version = cmdutil.determine_version_arch(spec, self.tree) + cmdutil.ensure_archive_registered(version.archive) + require_version_exists(version, spec) + return cmdutil.iter_cacherevs(version, reverse) + elif type == "library": + if spec is None: + if self.tree is None: + raise cmdutil.CantDetermineRevision("", + "Not in a project tree") + version = cmdutil.determine_version_tree(spec, self.tree) + else: + version = cmdutil.determine_version_arch(spec, self.tree) + return version.iter_library_revisions(reverse) + elif type == "logs": + if self.tree is None: + raise cmdutil.CantDetermineRevision("", "Not in a project tree") + return self.tree.iter_logs(cmdutil.determine_version_tree(spec, \ + self.tree), reverse) + elif type == "missing" or type == "skip-present": + if self.tree is None: + raise cmdutil.CantDetermineRevision("", "Not in a project tree") + skip = (type == "skip-present") + version = cmdutil.determine_version_tree(spec, self.tree) + cmdutil.ensure_archive_registered(version.archive) + require_version_exists(version, spec) + return cmdutil.iter_missing(self.tree, version, reverse, + skip_present=skip) + + elif type == "present": + if self.tree is None: + raise cmdutil.CantDetermineRevision("", "Not in a project tree") + version = cmdutil.determine_version_tree(spec, self.tree) + cmdutil.ensure_archive_registered(version.archive) + require_version_exists(version, spec) + return cmdutil.iter_present(self.tree, version, reverse) + + elif type == "new-merges" or type == "direct-merges": + if self.tree is None: + raise cmdutil.CantDetermineRevision("", "Not in a project tree") + version = cmdutil.determine_version_tree(spec, self.tree) + cmdutil.ensure_archive_registered(version.archive) + require_version_exists(version, spec) + iter = cmdutil.iter_new_merges(self.tree, version, reverse) + if type == "new-merges": + return iter + elif type == "direct-merges": + return cmdutil.direct_merges(iter) + + elif type == "missing-from": + if self.tree is None: + raise cmdutil.CantDetermineRevision("", "Not in a project tree") + revision = cmdutil.determine_revision_tree(self.tree, spec) + libtree = cmdutil.find_or_make_local_revision(revision) + return cmdutil.iter_missing(libtree, self.tree.tree_version, + reverse) + + elif type == "partner-missing": + return cmdutil.iter_partner_missing(self.tree, reverse) + + elif type == "ancestry": + revision = cmdutil.determine_revision_tree(self.tree, spec) + iter = cmdutil._iter_ancestry(self.tree, revision) + if reverse: + return iter + else: + return cmdutil.iter_reverse(iter) + + elif type == "dependencies" or type == "non-dependencies": + nondeps = (type == "non-dependencies") + revision = cmdutil.determine_revision_tree(self.tree, spec) + anc_iter = cmdutil._iter_ancestry(self.tree, revision) + iter_depends = cmdutil.iter_depends(anc_iter, nondeps) + if reverse: + return iter_depends + else: + return cmdutil.iter_reverse(iter_depends) + elif type == "micro": + return cmdutil.iter_micro(self.tree) + + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai revisions [revision]") + select = cmdutil.OptionGroup(parser, "Selection options", + "Control which revisions are listed. These options" + " are mutually exclusive. If more than one is" + " specified, the last is used.") + select.add_option("", "--archive", action="store_const", + const="archive", dest="type", default="archive", + help="List all revisions in the archive") + select.add_option("", "--cacherevs", action="store_const", + const="cacherevs", dest="type", + help="List all revisions stored in the archive as " + "complete copies") + select.add_option("", "--logs", action="store_const", + const="logs", dest="type", + help="List revisions that have a patchlog in the " + "tree") + select.add_option("", "--missing", action="store_const", + const="missing", dest="type", + help="List revisions from the specified version that" + " have no patchlog in the tree") + select.add_option("", "--skip-present", action="store_const", + const="skip-present", dest="type", + help="List revisions from the specified version that" + " have no patchlogs at all in the tree") + select.add_option("", "--present", action="store_const", + const="present", dest="type", + help="List revisions from the specified version that" + " have no patchlog in the tree, but can't be merged") + select.add_option("", "--missing-from", action="store_const", + const="missing-from", dest="type", + help="List revisions from the specified revision " + "that have no patchlog for the tree version") + select.add_option("", "--partner-missing", action="store_const", + const="partner-missing", dest="type", + help="List revisions in partner versions that are" + " missing") + select.add_option("", "--new-merges", action="store_const", + const="new-merges", dest="type", + help="List revisions that have had patchlogs added" + " to the tree since the last commit") + select.add_option("", "--direct-merges", action="store_const", + const="direct-merges", dest="type", + help="List revisions that have been directly added" + " to tree since the last commit ") + select.add_option("", "--library", action="store_const", + const="library", dest="type", + help="List revisions in the revision library") + select.add_option("", "--ancestry", action="store_const", + const="ancestry", dest="type", + help="List revisions that are ancestors of the " + "current tree version") + + select.add_option("", "--dependencies", action="store_const", + const="dependencies", dest="type", + help="List revisions that the given revision " + "depends on") + + select.add_option("", "--non-dependencies", action="store_const", + const="non-dependencies", dest="type", + help="List revisions that the given revision " + "does not depend on") + + select.add_option("--micro", action="store_const", + const="micro", dest="type", + help="List partner revisions aimed for this " + "micro-branch") + + select.add_option("", "--modified", dest="modified", + help="List tree ancestor revisions that modified a " + "given file", metavar="FILE[:LINE]") + + parser.add_option("", "--skip", dest="skip", + help="Skip revisions. Positive numbers skip from " + "beginning, negative skip from end.", + metavar="NUMBER") + + parser.add_option_group(select) + + format = cmdutil.OptionGroup(parser, "Revision format options", + "These control the appearance of listed revisions") + format.add_option("", "--location", action="store_const", + const=paths.determine_path, dest="display", + help="Show location instead of name", default=str) + format.add_option("--import", action="store_const", + const=paths.determine_import_path, dest="display", + help="Show location of import file") + format.add_option("--log", action="store_const", + const=paths.determine_log_path, dest="display", + help="Show location of log file") + format.add_option("--patch", action="store_const", + dest="display", const=paths.determine_patch_path, + help="Show location of patchfile") + format.add_option("--continuation", action="store_const", + const=paths.determine_continuation_path, + dest="display", + help="Show location of continuation file") + format.add_option("--cacherev", action="store_const", + const=paths.determine_cacherev_path, dest="display", + help="Show location of cacherev file") + parser.add_option_group(format) + display = cmdutil.OptionGroup(parser, "Display format options", + "These control the display of data") + display.add_option("-r", "--reverse", action="store_true", + dest="reverse", help="Sort from newest to oldest") + display.add_option("-s", "--summary", action="store_true", + dest="summary", help="Show patchlog summary") + display.add_option("-D", "--date", action="store_true", + dest="date", help="Show patchlog date") + display.add_option("-c", "--creator", action="store_true", + dest="creator", help="Show the id that committed the" + " revision") + display.add_option("-m", "--merges", action="store_true", + dest="merges", help="Show the revisions that were" + " merged") + parser.add_option_group(display) + return parser + def help(self, parser=None): + """Attempt to explain the revisions command + + :param parser: If supplied, used to determine options + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """List revisions. + """ + help_tree_spec() + + +class Get(BaseCommand): + """ + Retrieve a revision from the archive + """ + def __init__(self): + self.description="Retrieve a revision from the archive" + self.parser=self.get_parser() + + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + + def do_command(self, cmdargs): + """ + Master function that perfoms the "get" command. + """ + (options, args) = self.parser.parse_args(cmdargs) + if len(args) < 1: + return self.help() + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + tree = None + + arch_loc = None + try: + revision, arch_loc = paths.full_path_decode(args[0]) + except Exception, e: + revision = cmdutil.determine_revision_arch(tree, args[0], + check_existence=False, allow_package=True) + if len(args) > 1: + directory = args[1] + else: + directory = str(revision.nonarch) + if os.path.exists(directory): + raise DirectoryExists(directory) + cmdutil.ensure_archive_registered(revision.archive, arch_loc) + try: + cmdutil.ensure_revision_exists(revision) + except cmdutil.NoSuchRevision, e: + raise CommandFailedWrapper(e) + + link = cmdutil.prompt ("get link") + for line in cmdutil.iter_get(revision, directory, link, + options.no_pristine, + options.no_greedy_add): + cmdutil.colorize(line) + + def get_parser(self): + """ + Returns the options parser to use for the "get" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai get revision [dir]") + parser.add_option("--no-pristine", action="store_true", + dest="no_pristine", + help="Do not make pristine copy for reference") + parser.add_option("--no-greedy-add", action="store_true", + dest="no_greedy_add", + help="Never add to greedy libraries") + + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Expands aliases and constructs a project tree for a revision. If the optional +"dir" argument is provided, the project tree will be stored in this directory. + """ + help_tree_spec() + return + +class PromptCmd(cmd.Cmd): + def __init__(self): + cmd.Cmd.__init__(self) + self.prompt = "Fai> " + try: + self.tree = arch.tree_root() + except: + self.tree = None + self.set_title() + self.set_prompt() + self.fake_aba = abacmds.AbaCmds() + self.identchars += '-' + self.history_file = os.path.expanduser("~/.fai-history") + readline.set_completer_delims(string.whitespace) + if os.access(self.history_file, os.R_OK) and \ + os.path.isfile(self.history_file): + readline.read_history_file(self.history_file) + + def write_history(self): + readline.write_history_file(self.history_file) + + def do_quit(self, args): + self.write_history() + sys.exit(0) + + def do_exit(self, args): + self.do_quit(args) + + def do_EOF(self, args): + print + self.do_quit(args) + + def postcmd(self, line, bar): + self.set_title() + self.set_prompt() + + def set_prompt(self): + if self.tree is not None: + try: + version = " "+self.tree.tree_version.nonarch + except: + version = "" + else: + version = "" + self.prompt = "Fai%s> " % version + + def set_title(self, command=None): + try: + version = self.tree.tree_version.nonarch + except: + version = "[no version]" + if command is None: + command = "" + sys.stdout.write(terminal.term_title("Fai %s %s" % (command, version))) + + def do_cd(self, line): + if line == "": + line = "~" + try: + os.chdir(os.path.expanduser(line)) + except Exception, e: + print e + try: + self.tree = arch.tree_root() + except: + self.tree = None + + def do_help(self, line): + Help()(line) + + def default(self, line): + args = line.split() + if find_command(args[0]): + try: + find_command(args[0]).do_command(args[1:]) + except cmdutil.BadCommandOption, e: + print e + except cmdutil.GetHelp, e: + find_command(args[0]).help() + except CommandFailed, e: + print e + except arch.errors.ArchiveNotRegistered, e: + print e + except KeyboardInterrupt, e: + print "Interrupted" + except arch.util.ExecProblem, e: + print e.proc.error.rstrip('\n') + except cmdutil.CantDetermineVersion, e: + print e + except cmdutil.CantDetermineRevision, e: + print e + except Exception, e: + print "Unhandled error:\n%s" % cmdutil.exception_str(e) + + elif suggestions.has_key(args[0]): + print suggestions[args[0]] + + elif self.fake_aba.is_command(args[0]): + tree = None + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + pass + cmd = self.fake_aba.is_command(args[0]) + try: + cmd.run(cmdutil.expand_prefix_alias(args[1:], tree)) + except KeyboardInterrupt, e: + print "Interrupted" + + elif options.tla_fallthrough and args[0] != "rm" and \ + cmdutil.is_tla_command(args[0]): + try: + tree = None + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + pass + args = cmdutil.expand_prefix_alias(args, tree) + arch.util.exec_safe('tla', args, stderr=sys.stderr, + expected=(0, 1)) + except arch.util.ExecProblem, e: + pass + except KeyboardInterrupt, e: + print "Interrupted" + else: + try: + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + tree = None + args=line.split() + os.system(" ".join(cmdutil.expand_prefix_alias(args, tree))) + except KeyboardInterrupt, e: + print "Interrupted" + + def completenames(self, text, line, begidx, endidx): + completions = [] + iter = iter_command_names(self.fake_aba) + try: + if len(line) > 0: + arg = line.split()[-1] + else: + arg = "" + iter = iter_munged_completions(iter, arg, text) + except Exception, e: + print e + return list(iter) + + def completedefault(self, text, line, begidx, endidx): + """Perform completion for native commands. + + :param text: The text to complete + :type text: str + :param line: The entire line to complete + :type line: str + :param begidx: The start of the text in the line + :type begidx: int + :param endidx: The end of the text in the line + :type endidx: int + """ + try: + (cmd, args, foo) = self.parseline(line) + command_obj=find_command(cmd) + if command_obj is not None: + return command_obj.complete(args.split(), text) + elif not self.fake_aba.is_command(cmd) and \ + cmdutil.is_tla_command(cmd): + iter = cmdutil.iter_supported_switches(cmd) + if len(args) > 0: + arg = args.split()[-1] + else: + arg = "" + if arg.startswith("-"): + return list(iter_munged_completions(iter, arg, text)) + else: + return list(iter_munged_completions( + iter_file_completions(arg), arg, text)) + + + elif cmd == "cd": + if len(args) > 0: + arg = args.split()[-1] + else: + arg = "" + iter = iter_dir_completions(arg) + iter = iter_munged_completions(iter, arg, text) + return list(iter) + elif len(args)>0: + arg = args.split()[-1] + return list(iter_munged_completions(iter_file_completions(arg), + arg, text)) + else: + return self.completenames(text, line, begidx, endidx) + except Exception, e: + print e + + +def iter_command_names(fake_aba): + for entry in cmdutil.iter_combine([commands.iterkeys(), + fake_aba.get_commands(), + cmdutil.iter_tla_commands(False)]): + if not suggestions.has_key(str(entry)): + yield entry + + +def iter_file_completions(arg, only_dirs = False): + """Generate an iterator that iterates through filename completions. + + :param arg: The filename fragment to match + :type arg: str + :param only_dirs: If true, match only directories + :type only_dirs: bool + """ + cwd = os.getcwd() + if cwd != "/": + extras = [".", ".."] + else: + extras = [] + (dir, file) = os.path.split(arg) + if dir != "": + listingdir = os.path.expanduser(dir) + else: + listingdir = cwd + for file in cmdutil.iter_combine([os.listdir(listingdir), extras]): + if dir != "": + userfile = dir+'/'+file + else: + userfile = file + if userfile.startswith(arg): + if os.path.isdir(listingdir+'/'+file): + userfile+='/' + yield userfile + elif not only_dirs: + yield userfile + +def iter_munged_completions(iter, arg, text): + for completion in iter: + completion = str(completion) + if completion.startswith(arg): + yield completion[len(arg)-len(text):] + +def iter_source_file_completions(tree, arg): + treepath = cmdutil.tree_cwd(tree) + if len(treepath) > 0: + dirs = [treepath] + else: + dirs = None + for file in tree.iter_inventory(dirs, source=True, both=True): + file = file_completion_match(file, treepath, arg) + if file is not None: + yield file + + +def iter_untagged(tree, dirs): + for file in arch_core.iter_inventory_filter(tree, dirs, tagged=False, + categories=arch_core.non_root, + control_files=True): + yield file.name + + +def iter_untagged_completions(tree, arg): + """Generate an iterator for all visible untagged files that match arg. + + :param tree: The tree to look for untagged files in + :type tree: `arch.WorkingTree` + :param arg: The argument to match + :type arg: str + :return: An iterator of all matching untagged files + :rtype: iterator of str + """ + treepath = cmdutil.tree_cwd(tree) + if len(treepath) > 0: + dirs = [treepath] + else: + dirs = None + + for file in iter_untagged(tree, dirs): + file = file_completion_match(file, treepath, arg) + if file is not None: + yield file + + +def file_completion_match(file, treepath, arg): + """Determines whether a file within an arch tree matches the argument. + + :param file: The rooted filename + :type file: str + :param treepath: The path to the cwd within the tree + :type treepath: str + :param arg: The prefix to match + :return: The completion name, or None if not a match + :rtype: str + """ + if not file.startswith(treepath): + return None + if treepath != "": + file = file[len(treepath)+1:] + + if not file.startswith(arg): + return None + if os.path.isdir(file): + file += '/' + return file + +def iter_modified_file_completions(tree, arg): + """Returns a list of modified files that match the specified prefix. + + :param tree: The current tree + :type tree: `arch.WorkingTree` + :param arg: The prefix to match + :type arg: str + """ + treepath = cmdutil.tree_cwd(tree) + tmpdir = cmdutil.tmpdir() + changeset = tmpdir+"/changeset" + completions = [] + revision = cmdutil.determine_revision_tree(tree) + for line in arch.iter_delta(revision, tree, changeset): + if isinstance(line, arch.FileModification): + file = file_completion_match(line.name[1:], treepath, arg) + if file is not None: + completions.append(file) + shutil.rmtree(tmpdir) + return completions + +def iter_dir_completions(arg): + """Generate an iterator that iterates through directory name completions. + + :param arg: The directory name fragment to match + :type arg: str + """ + return iter_file_completions(arg, True) + +class Shell(BaseCommand): + def __init__(self): + self.description = "Runs Fai as a shell" + + def do_command(self, cmdargs): + if len(cmdargs)!=0: + raise cmdutil.GetHelp + prompt = PromptCmd() + try: + prompt.cmdloop() + finally: + prompt.write_history() + +class AddID(BaseCommand): + """ + Adds an inventory id for the given file + """ + def __init__(self): + self.description="Add an inventory id for a given file" + + def get_completer(self, arg, index): + tree = arch.tree_root() + return iter_untagged_completions(tree, arg) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + + tree = arch.tree_root() + + if (len(args) == 0) == (options.untagged == False): + raise cmdutil.GetHelp + + #if options.id and len(args) != 1: + # print "If --id is specified, only one file can be named." + # return + + method = tree.tagging_method + + if options.id_type == "tagline": + if method != "tagline": + if not cmdutil.prompt("Tagline in other tree"): + if method == "explicit": + options.id_type == explicit + else: + print "add-id not supported for \"%s\" tagging method"\ + % method + return + + elif options.id_type == "explicit": + if method != "tagline" and method != explicit: + if not prompt("Explicit in other tree"): + print "add-id not supported for \"%s\" tagging method" % \ + method + return + + if options.id_type == "auto": + if method != "tagline" and method != "explicit": + print "add-id not supported for \"%s\" tagging method" % method + return + else: + options.id_type = method + if options.untagged: + args = None + self.add_ids(tree, options.id_type, args) + + def add_ids(self, tree, id_type, files=()): + """Add inventory ids to files. + + :param tree: the tree the files are in + :type tree: `arch.WorkingTree` + :param id_type: the type of id to add: "explicit" or "tagline" + :type id_type: str + :param files: The list of files to add. If None do all untagged. + :type files: tuple of str + """ + + untagged = (files is None) + if untagged: + files = list(iter_untagged(tree, None)) + previous_files = [] + while len(files) > 0: + previous_files.extend(files) + if id_type == "explicit": + cmdutil.add_id(files) + elif id_type == "tagline": + for file in files: + try: + cmdutil.add_tagline_or_explicit_id(file) + except cmdutil.AlreadyTagged: + print "\"%s\" already has a tagline." % file + except cmdutil.NoCommentSyntax: + pass + #do inventory after tagging until no untagged files are encountered + if untagged: + files = [] + for file in iter_untagged(tree, None): + if not file in previous_files: + files.append(file) + + else: + break + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai add-id file1 [file2] [file3]...") +# ddaa suggests removing this to promote GUIDs. Let's see who squalks. +# parser.add_option("-i", "--id", dest="id", +# help="Specify id for a single file", default=None) + parser.add_option("--tltl", action="store_true", + dest="lord_style", help="Use Tom Lord's style of id.") + parser.add_option("--explicit", action="store_const", + const="explicit", dest="id_type", + help="Use an explicit id", default="auto") + parser.add_option("--tagline", action="store_const", + const="tagline", dest="id_type", + help="Use a tagline id") + parser.add_option("--untagged", action="store_true", + dest="untagged", default=False, + help="tag all untagged files") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Adds an inventory to the specified file(s) and directories. If --untagged is +specified, adds inventory to all untagged files and directories. + """ + return + + +class Merge(BaseCommand): + """ + Merges changes from other versions into the current tree + """ + def __init__(self): + self.description="Merges changes from other versions" + try: + self.tree = arch.tree_root() + except: + self.tree = None + + + def get_completer(self, arg, index): + if self.tree is None: + raise arch.errors.TreeRootError + completions = list(ancillary.iter_partners(self.tree, + self.tree.tree_version)) + if len(completions) == 0: + completions = list(self.tree.iter_log_versions()) + + aliases = [] + try: + for completion in completions: + alias = ancillary.compact_alias(str(completion), self.tree) + if alias: + aliases.extend(alias) + + for completion in completions: + if completion.archive == self.tree.tree_version.archive: + aliases.append(completion.nonarch) + + except Exception, e: + print e + + completions.extend(aliases) + return completions + + def do_command(self, cmdargs): + """ + Master function that perfoms the "merge" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + if options.diff3: + action="star-merge" + else: + action = options.action + + if self.tree is None: + raise arch.errors.TreeRootError(os.getcwd()) + if cmdutil.has_changed(self.tree.tree_version): + raise UncommittedChanges(self.tree) + + if len(args) > 0: + revisions = [] + for arg in args: + revisions.append(cmdutil.determine_revision_arch(self.tree, + arg)) + source = "from commandline" + else: + revisions = ancillary.iter_partner_revisions(self.tree, + self.tree.tree_version) + source = "from partner version" + revisions = misc.rewind_iterator(revisions) + try: + revisions.next() + revisions.rewind() + except StopIteration, e: + revision = cmdutil.tag_cur(self.tree) + if revision is None: + raise CantDetermineRevision("", "No version specified, no " + "partner-versions, and no tag" + " source") + revisions = [revision] + source = "from tag source" + for revision in revisions: + cmdutil.ensure_archive_registered(revision.archive) + cmdutil.colorize(arch.Chatter("* Merging %s [%s]" % + (revision, source))) + if action=="native-merge" or action=="update": + if self.native_merge(revision, action) == 0: + continue + elif action=="star-merge": + try: + self.star_merge(revision, options.diff3) + except errors.MergeProblem, e: + break + if cmdutil.has_changed(self.tree.tree_version): + break + + def star_merge(self, revision, diff3): + """Perform a star-merge on the current tree. + + :param revision: The revision to use for the merge + :type revision: `arch.Revision` + :param diff3: If true, do a diff3 merge + :type diff3: bool + """ + try: + for line in self.tree.iter_star_merge(revision, diff3=diff3): + cmdutil.colorize(line) + except arch.util.ExecProblem, e: + if e.proc.status is not None and e.proc.status == 1: + if e.proc.error: + print e.proc.error + raise MergeProblem + else: + raise + + def native_merge(self, other_revision, action): + """Perform a native-merge on the current tree. + + :param other_revision: The revision to use for the merge + :type other_revision: `arch.Revision` + :return: 0 if the merge was skipped, 1 if it was applied + """ + other_tree = cmdutil.find_or_make_local_revision(other_revision) + try: + if action == "native-merge": + ancestor = cmdutil.merge_ancestor2(self.tree, other_tree, + other_revision) + elif action == "update": + ancestor = cmdutil.tree_latest(self.tree, + other_revision.version) + except CantDetermineRevision, e: + raise CommandFailedWrapper(e) + cmdutil.colorize(arch.Chatter("* Found common ancestor %s" % ancestor)) + if (ancestor == other_revision): + cmdutil.colorize(arch.Chatter("* Skipping redundant merge" + % ancestor)) + return 0 + delta = cmdutil.apply_delta(ancestor, other_tree, self.tree) + for line in cmdutil.iter_apply_delta_filter(delta): + cmdutil.colorize(line) + return 1 + + + + def get_parser(self): + """ + Returns the options parser to use for the "merge" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai merge [VERSION]") + parser.add_option("-s", "--star-merge", action="store_const", + dest="action", help="Use star-merge", + const="star-merge", default="native-merge") + parser.add_option("--update", action="store_const", + dest="action", help="Use update picker", + const="update") + parser.add_option("--diff3", action="store_true", + dest="diff3", + help="Use diff3 for merge (implies star-merge)") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Performs a merge operation using the specified version. + """ + return + +class ELog(BaseCommand): + """ + Produces a raw patchlog and invokes the user's editor + """ + def __init__(self): + self.description="Edit a patchlog to commit" + try: + self.tree = arch.tree_root() + except: + self.tree = None + + + def do_command(self, cmdargs): + """ + Master function that perfoms the "elog" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + if self.tree is None: + raise arch.errors.TreeRootError + + edit_log(self.tree) + + def get_parser(self): + """ + Returns the options parser to use for the "merge" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai elog") + return parser + + + def help(self, parser=None): + """ + Invokes $EDITOR to produce a log for committing. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Invokes $EDITOR to produce a log for committing. + """ + return + +def edit_log(tree): + """Makes and edits the log for a tree. Does all kinds of fancy things + like log templates and merge summaries and log-for-merge + + :param tree: The tree to edit the log for + :type tree: `arch.WorkingTree` + """ + #ensure we have an editor before preparing the log + cmdutil.find_editor() + log = tree.log_message(create=False) + log_is_new = False + if log is None or cmdutil.prompt("Overwrite log"): + if log is not None: + os.remove(log.name) + log = tree.log_message(create=True) + log_is_new = True + tmplog = log.name + template = tree+"/{arch}/=log-template" + if not os.path.exists(template): + template = os.path.expanduser("~/.arch-params/=log-template") + if not os.path.exists(template): + template = None + if template: + shutil.copyfile(template, tmplog) + + new_merges = list(cmdutil.iter_new_merges(tree, + tree.tree_version)) + log["Summary"] = merge_summary(new_merges, tree.tree_version) + if len(new_merges) > 0: + if cmdutil.prompt("Log for merge"): + mergestuff = cmdutil.log_for_merge(tree) + log.description += mergestuff + log.save() + try: + cmdutil.invoke_editor(log.name) + except: + if log_is_new: + os.remove(log.name) + raise + +def merge_summary(new_merges, tree_version): + if len(new_merges) == 0: + return "" + if len(new_merges) == 1: + summary = new_merges[0].summary + else: + summary = "Merge" + + credits = [] + for merge in new_merges: + if arch.my_id() != merge.creator: + name = re.sub("<.*>", "", merge.creator).rstrip(" "); + if not name in credits: + credits.append(name) + else: + version = merge.revision.version + if version.archive == tree_version.archive: + if not version.nonarch in credits: + credits.append(version.nonarch) + elif not str(version) in credits: + credits.append(str(version)) + + return ("%s (%s)") % (summary, ", ".join(credits)) + +class MirrorArchive(BaseCommand): + """ + Updates a mirror from an archive + """ + def __init__(self): + self.description="Update a mirror from an archive" + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + if len(args) > 1: + raise GetHelp + try: + tree = arch.tree_root() + except: + tree = None + + if len(args) == 0: + if tree is not None: + name = tree.tree_version() + else: + name = cmdutil.expand_alias(args[0], tree) + name = arch.NameParser(name) + + to_arch = name.get_archive() + from_arch = cmdutil.get_mirror_source(arch.Archive(to_arch)) + limit = name.get_nonarch() + + iter = arch_core.mirror_archive(from_arch,to_arch, limit) + for line in arch.chatter_classifier(iter): + cmdutil.colorize(line) + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai mirror-archive ARCHIVE") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Updates a mirror from an archive. If a branch, package, or version is +supplied, only changes under it are mirrored. + """ + return + +def help_tree_spec(): + print """Specifying revisions (default: tree) +Revisions may be specified by alias, revision, version or patchlevel. +Revisions or versions may be fully qualified. Unqualified revisions, versions, +or patchlevels use the archive of the current project tree. Versions will +use the latest patchlevel in the tree. Patchlevels will use the current tree- +version. + +Use "alias" to list available (user and automatic) aliases.""" + +def help_aliases(tree): + print """Auto-generated aliases + acur : The latest revision in the archive of the tree-version. You can specfy + a different version like so: acur:foo--bar--0 (aliases can be used) + tcur : (tree current) The latest revision in the tree of the tree-version. + You can specify a different version like so: tcur:foo--bar--0 (aliases + can be used). +tprev : (tree previous) The previous revision in the tree of the tree-version. + To specify an older revision, use a number, e.g. "tprev:4" + tanc : (tree ancestor) The ancestor revision of the tree + To specify an older revision, use a number, e.g. "tanc:4" +tdate : (tree date) The latest revision from a given date (e.g. "tdate:July 6") + tmod : (tree modified) The latest revision to modify a given file + (e.g. "tmod:engine.cpp" or "tmod:engine.cpp:16") + ttag : (tree tag) The revision that was tagged into the current tree revision, + according to the tree. +tagcur: (tag current) The latest revision of the version that the current tree + was tagged from. +mergeanc : The common ancestor of the current tree and the specified revision. + Defaults to the first partner-version's latest revision or to tagcur. + """ + print "User aliases" + for parts in ancillary.iter_all_alias(tree): + print parts[0].rjust(10)+" : "+parts[1] + + +class Inventory(BaseCommand): + """List the status of files in the tree""" + def __init__(self): + self.description=self.__doc__ + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + tree = arch.tree_root() + categories = [] + + if (options.source): + categories.append(arch_core.SourceFile) + if (options.precious): + categories.append(arch_core.PreciousFile) + if (options.backup): + categories.append(arch_core.BackupFile) + if (options.junk): + categories.append(arch_core.JunkFile) + + if len(categories) == 1: + show_leading = False + else: + show_leading = True + + if len(categories) == 0: + categories = None + + if options.untagged: + categories = arch_core.non_root + show_leading = False + tagged = False + else: + tagged = None + + for file in arch_core.iter_inventory_filter(tree, None, + control_files=options.control_files, + categories = categories, tagged=tagged): + print arch_core.file_line(file, + category = show_leading, + untagged = show_leading, + id = options.ids) + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai inventory [options]") + parser.add_option("--ids", action="store_true", dest="ids", + help="Show file ids") + parser.add_option("--control", action="store_true", + dest="control_files", help="include control files") + parser.add_option("--source", action="store_true", dest="source", + help="List source files") + parser.add_option("--backup", action="store_true", dest="backup", + help="List backup files") + parser.add_option("--precious", action="store_true", dest="precious", + help="List precious files") + parser.add_option("--junk", action="store_true", dest="junk", + help="List junk files") + parser.add_option("--unrecognized", action="store_true", + dest="unrecognized", help="List unrecognized files") + parser.add_option("--untagged", action="store_true", + dest="untagged", help="List only untagged files") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Lists the status of files in the archive: +S source +P precious +B backup +J junk +U unrecognized +T tree root +? untagged-source +Leading letter are not displayed if only one kind of file is shown + """ + return + + +class Alias(BaseCommand): + """List or adjust aliases""" + def __init__(self): + self.description=self.__doc__ + + def get_completer(self, arg, index): + if index > 2: + return () + try: + self.tree = arch.tree_root() + except: + self.tree = None + + if index == 0: + return [part[0]+" " for part in ancillary.iter_all_alias(self.tree)] + elif index == 1: + return cmdutil.iter_revision_completions(arg, self.tree) + + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: + self.tree = arch.tree_root() + except: + self.tree = None + + + try: + options.action(args, options) + except cmdutil.ForbiddenAliasSyntax, e: + raise CommandFailedWrapper(e) + + def arg_dispatch(self, args, options): + """Add, modify, or list aliases, depending on number of arguments + + :param args: The list of commandline arguments + :type args: list of str + :param options: The commandline options + """ + if len(args) == 0: + help_aliases(self.tree) + return + elif len(args) == 1: + self.print_alias(args[0]) + elif (len(args)) == 2: + self.add(args[0], args[1], options) + else: + raise cmdutil.GetHelp + + def print_alias(self, alias): + answer = None + for pair in ancillary.iter_all_alias(self.tree): + if pair[0] == alias: + answer = pair[1] + if answer is not None: + print answer + else: + print "The alias %s is not assigned." % alias + + def add(self, alias, expansion, options): + """Add or modify aliases + + :param alias: The alias name to create/modify + :type alias: str + :param expansion: The expansion to assign to the alias name + :type expansion: str + :param options: The commandline options + """ + newlist = "" + written = False + new_line = "%s=%s\n" % (alias, cmdutil.expand_alias(expansion, + self.tree)) + ancillary.check_alias(new_line.rstrip("\n"), [alias, expansion]) + + for pair in self.get_iterator(options): + if pair[0] != alias: + newlist+="%s=%s\n" % (pair[0], pair[1]) + elif not written: + newlist+=new_line + written = True + if not written: + newlist+=new_line + self.write_aliases(newlist, options) + + def delete(self, args, options): + """Delete the specified alias + + :param args: The list of arguments + :type args: list of str + :param options: The commandline options + """ + deleted = False + if len(args) != 1: + raise cmdutil.GetHelp + newlist = "" + for pair in self.get_iterator(options): + if pair[0] != args[0]: + newlist+="%s=%s\n" % (pair[0], pair[1]) + else: + deleted = True + if not deleted: + raise errors.NoSuchAlias(args[0]) + self.write_aliases(newlist, options) + + def get_alias_file(self, options): + """Return the name of the alias file to use + + :param options: The commandline options + """ + if options.tree: + if self.tree is None: + self.tree == arch.tree_root() + return str(self.tree)+"/{arch}/+aliases" + else: + return "~/.aba/aliases" + + def get_iterator(self, options): + """Return the alias iterator to use + + :param options: The commandline options + """ + return ancillary.iter_alias(self.get_alias_file(options)) + + def write_aliases(self, newlist, options): + """Safely rewrite the alias file + :param newlist: The new list of aliases + :type newlist: str + :param options: The commandline options + """ + filename = os.path.expanduser(self.get_alias_file(options)) + file = cmdutil.NewFileVersion(filename) + file.write(newlist) + file.commit() + + + def get_parser(self): + """ + Returns the options parser to use for the "alias" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai alias [ALIAS] [NAME]") + parser.add_option("-d", "--delete", action="store_const", dest="action", + const=self.delete, default=self.arg_dispatch, + help="Delete an alias") + parser.add_option("--tree", action="store_true", dest="tree", + help="Create a per-tree alias", default=False) + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Lists current aliases or modifies the list of aliases. + +If no arguments are supplied, aliases will be listed. If two arguments are +supplied, the specified alias will be created or modified. If -d or --delete +is supplied, the specified alias will be deleted. + +You can create aliases that refer to any fully-qualified part of the +Arch namespace, e.g. +archive, +archive/category, +archive/category--branch, +archive/category--branch--version (my favourite) +archive/category--branch--version--patchlevel + +Aliases can be used automatically by native commands. To use them +with external or tla commands, prefix them with ^ (you can do this +with native commands, too). +""" + + +class RequestMerge(BaseCommand): + """Submit a merge request to Bug Goo""" + def __init__(self): + self.description=self.__doc__ + + def do_command(self, cmdargs): + """Submit a merge request + + :param cmdargs: The commandline arguments + :type cmdargs: list of str + """ + cmdutil.find_editor() + parser = self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: + self.tree=arch.tree_root() + except: + self.tree=None + base, revisions = self.revision_specs(args) + message = self.make_headers(base, revisions) + message += self.make_summary(revisions) + path = self.edit_message(message) + message = self.tidy_message(path) + if cmdutil.prompt("Send merge"): + self.send_message(message) + print "Merge request sent" + + def make_headers(self, base, revisions): + """Produce email and Bug Goo header strings + + :param base: The base revision to apply merges to + :type base: `arch.Revision` + :param revisions: The revisions to replay into the base + :type revisions: list of `arch.Patchlog` + :return: The headers + :rtype: str + """ + headers = "To: gnu-arch-users@gnu.org\n" + headers += "From: %s\n" % options.fromaddr + if len(revisions) == 1: + headers += "Subject: [MERGE REQUEST] %s\n" % revisions[0].summary + else: + headers += "Subject: [MERGE REQUEST]\n" + headers += "\n" + headers += "Base-Revision: %s\n" % base + for revision in revisions: + headers += "Revision: %s\n" % revision.revision + headers += "Bug: \n\n" + return headers + + def make_summary(self, logs): + """Generate a summary of merges + + :param logs: the patchlogs that were directly added by the merges + :type logs: list of `arch.Patchlog` + :return: the summary + :rtype: str + """ + summary = "" + for log in logs: + summary+=str(log.revision)+"\n" + summary+=log.summary+"\n" + if log.description.strip(): + summary+=log.description.strip('\n')+"\n\n" + return summary + + def revision_specs(self, args): + """Determine the base and merge revisions from tree and arguments. + + :param args: The parsed arguments + :type args: list of str + :return: The base revision and merge revisions + :rtype: `arch.Revision`, list of `arch.Patchlog` + """ + if len(args) > 0: + target_revision = cmdutil.determine_revision_arch(self.tree, + args[0]) + else: + target_revision = cmdutil.tree_latest(self.tree) + if len(args) > 1: + merges = [ arch.Patchlog(cmdutil.determine_revision_arch( + self.tree, f)) for f in args[1:] ] + else: + if self.tree is None: + raise CantDetermineRevision("", "Not in a project tree") + merge_iter = cmdutil.iter_new_merges(self.tree, + target_revision.version, + False) + merges = [f for f in cmdutil.direct_merges(merge_iter)] + return (target_revision, merges) + + def edit_message(self, message): + """Edit an email message in the user's standard editor + + :param message: The message to edit + :type message: str + :return: the path of the edited message + :rtype: str + """ + if self.tree is None: + path = os.get_cwd() + else: + path = self.tree + path += "/,merge-request" + file = open(path, 'w') + file.write(message) + file.flush() + cmdutil.invoke_editor(path) + return path + + def tidy_message(self, path): + """Validate and clean up message. + + :param path: The path to the message to clean up + :type path: str + :return: The parsed message + :rtype: `email.Message` + """ + mail = email.message_from_file(open(path)) + if mail["Subject"].strip() == "[MERGE REQUEST]": + raise BlandSubject + + request = email.message_from_string(mail.get_payload()) + if request.has_key("Bug"): + if request["Bug"].strip()=="": + del request["Bug"] + mail.set_payload(request.as_string()) + return mail + + def send_message(self, message): + """Send a message, using its headers to address it. + + :param message: The message to send + :type message: `email.Message`""" + server = smtplib.SMTP() + server.sendmail(message['From'], message['To'], message.as_string()) + server.quit() + + def help(self, parser=None): + """Print a usage message + + :param parser: The options parser to use + :type parser: `cmdutil.CmdOptionParser` + """ + if parser is None: + parser = self.get_parser() + parser.print_help() + print """ +Sends a merge request formatted for Bug Goo. Intended use: get the tree +you'd like to merge into. Apply the merges you want. Invoke request-merge. +The merge request will open in your $EDITOR. + +When no TARGET is specified, it uses the current tree revision. When +no MERGE is specified, it uses the direct merges (as in "revisions +--direct-merges"). But you can specify just the TARGET, or all the MERGE +revisions. +""" + + def get_parser(self): + """Produce a commandline parser for this command. + + :rtype: `cmdutil.CmdOptionParser` + """ + parser=cmdutil.CmdOptionParser("request-merge [TARGET] [MERGE1...]") + return parser + +commands = { +'changes' : Changes, +'help' : Help, +'update': Update, +'apply-changes':ApplyChanges, +'cat-log': CatLog, +'commit': Commit, +'revision': Revision, +'revisions': Revisions, +'get': Get, +'revert': Revert, +'shell': Shell, +'add-id': AddID, +'merge': Merge, +'elog': ELog, +'mirror-archive': MirrorArchive, +'ninventory': Inventory, +'alias' : Alias, +'request-merge': RequestMerge, +} +suggestions = { +'apply-delta' : "Try \"apply-changes\".", +'delta' : "To compare two revisions, use \"changes\".", +'diff-rev' : "To compare two revisions, use \"changes\".", +'undo' : "To undo local changes, use \"revert\".", +'undelete' : "To undo only deletions, use \"revert --deletions\"", +'missing-from' : "Try \"revisions --missing-from\".", +'missing' : "Try \"revisions --missing\".", +'missing-merge' : "Try \"revisions --partner-missing\".", +'new-merges' : "Try \"revisions --new-merges\".", +'cachedrevs' : "Try \"revisions --cacherevs\". (no 'd')", +'logs' : "Try \"revisions --logs\"", +'tree-source' : "Use the \"^ttag\" alias (\"revision ^ttag\")", +'latest-revision' : "Use the \"^acur\" alias (\"revision ^acur\")", +'change-version' : "Try \"update REVISION\"", +'tree-revision' : "Use the \"^tcur\" alias (\"revision ^tcur\")", +'rev-depends' : "Use revisions --dependencies", +'auto-get' : "Plain get will do archive lookups", +'tagline' : "Use add-id. It uses taglines in tagline trees", +'emlog' : "Use elog. It automatically adds log-for-merge text, if any", +'library-revisions' : "Use revisions --library", +'file-revert' : "Use revert FILE" +} +# arch-tag: 19d5739d-3708-486c-93ba-deecc3027fc7 *** modified file 'bzrlib/branch.py' --- bzrlib/branch.py +++ bzrlib/branch.py @@ -31,6 +31,8 @@ from revision import Revision from errors import bailout, BzrError from textui import show_status +import patches +from bzrlib import progress BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? @@ -802,3 +804,36 @@ s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) + + +def iter_anno_data(branch, file_id): + later_revision = branch.revno() + q = range(branch.revno()) + q.reverse() + later_text_id = branch.basis_tree().inventory[file_id].text_id + i = 0 + for revno in q: + i += 1 + cur_tree = branch.revision_tree(branch.lookup_revision(revno)) + if file_id not in cur_tree.inventory: + text_id = None + else: + text_id = cur_tree.inventory[file_id].text_id + if text_id != later_text_id: + patch = get_patch(branch, revno, later_revision, file_id) + yield revno, patch.iter_inserted(), patch + later_revision = revno + later_text_id = text_id + yield progress.Progress("revisions", i) + +def get_patch(branch, old_revno, new_revno, file_id): + old_tree = branch.revision_tree(branch.lookup_revision(old_revno)) + new_tree = branch.revision_tree(branch.lookup_revision(new_revno)) + if file_id in old_tree.inventory: + old_file = old_tree.get_file(file_id).readlines() + else: + old_file = [] + ud = difflib.unified_diff(old_file, new_tree.get_file(file_id).readlines()) + return patches.parse_patch(ud) + + *** modified file 'bzrlib/commands.py' --- bzrlib/commands.py +++ bzrlib/commands.py @@ -27,6 +27,9 @@ from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date from bzrlib import merge +from bzrlib.branch import iter_anno_data +from bzrlib import patches +from bzrlib import progress def _squish_command_name(cmd): @@ -882,7 +885,15 @@ print '%3d FAILED!' % mf else: print - + result = bzrlib.patches.test() + resultFailed = len(result.errors) + len(result.failures) + print '%-40s %3d tests' % ('bzrlib.patches', result.testsRun), + if resultFailed: + print '%3d FAILED!' % resultFailed + else: + print + tests += result.testsRun + failures += resultFailed print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures @@ -897,6 +908,27 @@ """Show version of bzr""" def run(self): show_version() + +class cmd_annotate(Command): + """Show which revision added each line in a file""" + + takes_args = ['filename'] + def run(self, filename): + branch = (Branch(filename)) + file_id = branch.working_tree().path2id(filename) + lines = branch.basis_tree().get_file(file_id) + total = branch.revno() + anno_d_iter = iter_anno_data(branch, file_id) + for result in patches.iter_annotate_file(lines, anno_d_iter): + if isinstance(result, progress.Progress): + result.total = total + progress.progress_bar(result) + else: + progress.clear_progress_bar() + anno_lines = result + for line in anno_lines: + sys.stdout.write("%4s:%s" % (str(line.log), line.text)) + def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ commit refs/heads/master mark :645 committer Martin Pool 1118384975 +1000 data 36 - split out proposed progress module from :644 M 644 inline patches/progress.diff data 3328 *** added file 'bzrlib/progress.py' --- /dev/null +++ bzrlib/progress.py @@ -0,0 +1,91 @@ +# Copyright (C) 2005 Aaron Bentley +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import sys + +class Progress(object): + def __init__(self, units, current, total=None): + self.units = units + self.current = current + self.total = total + self.percent = None + if self.total is not None: + self.percent = 100.0 * current / total + + def __str__(self): + if self.total is not None: + return "%i of %i %s %.1f%%" % (self.current, self.total, self.units, + self.percent) + else: + return "%i %s" (self.current, self.units) + + +def progress_bar(progress): + fmt = " %i of %i %s (%.1f%%)" + f = fmt % (progress.total, progress.total, progress.units, 100.0) + max = len(f) + cols = 77 - max + markers = int (float(cols) * progress.current / progress.total) + txt = fmt % (progress.current, progress.total, progress.units, + progress.percent) + sys.stderr.write("\r[%s%s]%s" % ('='*markers, ' '*(cols-markers), txt)) + +def clear_progress_bar(): + sys.stderr.write('\r%s\r' % (' '*79)) + +def spinner_str(progress, show_text=False): + """ + Produces the string for a textual "spinner" progress indicator + :param progress: an object represinting current progress + :param show_text: If true, show progress text as well + :return: The spinner string + + >>> spinner_str(Progress("baloons", 0)) + '|' + >>> spinner_str(Progress("baloons", 5)) + '/' + >>> spinner_str(Progress("baloons", 6), show_text=True) + '- 6 baloons' + """ + positions = ('|', '/', '-', '\\') + text = positions[progress.current % 4] + if show_text: + text+=" %i %s" % (progress.current, progress.units) + return text + +def spinner(progress, show_text=False, output=sys.stderr): + """ + Update a spinner progress indicator on an output + :param progress: The progress to display + :param show_text: If true, show text as well as spinner + :param output: The output to write to + + >>> spinner(Progress("baloons", 6), show_text=True, output=sys.stdout) + \r- 6 baloons + """ + output.write('\r%s' % spinner_str(progress, show_text)) + +def run_tests(): + import doctest + result = doctest.testmod() + if result[1] > 0: + if result[0] == 0: + print "All tests passed" + else: + print "No tests to run" +if __name__ == "__main__": + run_tests() M 644 inline patches/annotate3.patch data 268972 *** added file 'bzrlib/patches.py' --- /dev/null +++ bzrlib/patches.py @@ -0,0 +1,497 @@ +# Copyright (C) 2004, 2005 Aaron Bentley +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +import sys +import progress +class PatchSyntax(Exception): + def __init__(self, msg): + Exception.__init__(self, msg) + + +class MalformedPatchHeader(PatchSyntax): + def __init__(self, desc, line): + self.desc = desc + self.line = line + msg = "Malformed patch header. %s\n%s" % (self.desc, self.line) + PatchSyntax.__init__(self, msg) + +class MalformedHunkHeader(PatchSyntax): + def __init__(self, desc, line): + self.desc = desc + self.line = line + msg = "Malformed hunk header. %s\n%s" % (self.desc, self.line) + PatchSyntax.__init__(self, msg) + +class MalformedLine(PatchSyntax): + def __init__(self, desc, line): + self.desc = desc + self.line = line + msg = "Malformed line. %s\n%s" % (self.desc, self.line) + PatchSyntax.__init__(self, msg) + +def get_patch_names(iter_lines): + try: + line = iter_lines.next() + if not line.startswith("--- "): + raise MalformedPatchHeader("No orig name", line) + else: + orig_name = line[4:].rstrip("\n") + except StopIteration: + raise MalformedPatchHeader("No orig line", "") + try: + line = iter_lines.next() + if not line.startswith("+++ "): + raise PatchSyntax("No mod name") + else: + mod_name = line[4:].rstrip("\n") + except StopIteration: + raise MalformedPatchHeader("No mod line", "") + return (orig_name, mod_name) + +def parse_range(textrange): + """Parse a patch range, handling the "1" special-case + + :param textrange: The text to parse + :type textrange: str + :return: the position and range, as a tuple + :rtype: (int, int) + """ + tmp = textrange.split(',') + if len(tmp) == 1: + pos = tmp[0] + range = "1" + else: + (pos, range) = tmp + pos = int(pos) + range = int(range) + return (pos, range) + + +def hunk_from_header(line): + if not line.startswith("@@") or not line.endswith("@@\n") \ + or not len(line) > 4: + raise MalformedHunkHeader("Does not start and end with @@.", line) + try: + (orig, mod) = line[3:-4].split(" ") + except Exception, e: + raise MalformedHunkHeader(str(e), line) + if not orig.startswith('-') or not mod.startswith('+'): + raise MalformedHunkHeader("Positions don't start with + or -.", line) + try: + (orig_pos, orig_range) = parse_range(orig[1:]) + (mod_pos, mod_range) = parse_range(mod[1:]) + except Exception, e: + raise MalformedHunkHeader(str(e), line) + if mod_range < 0 or orig_range < 0: + raise MalformedHunkHeader("Hunk range is negative", line) + return Hunk(orig_pos, orig_range, mod_pos, mod_range) + + +class HunkLine: + def __init__(self, contents): + self.contents = contents + + def get_str(self, leadchar): + if self.contents == "\n" and leadchar == " " and False: + return "\n" + return leadchar + self.contents + +class ContextLine(HunkLine): + def __init__(self, contents): + HunkLine.__init__(self, contents) + + def __str__(self): + return self.get_str(" ") + + +class InsertLine(HunkLine): + def __init__(self, contents): + HunkLine.__init__(self, contents) + + def __str__(self): + return self.get_str("+") + + +class RemoveLine(HunkLine): + def __init__(self, contents): + HunkLine.__init__(self, contents) + + def __str__(self): + return self.get_str("-") + +__pychecker__="no-returnvalues" +def parse_line(line): + if line.startswith("\n"): + return ContextLine(line) + elif line.startswith(" "): + return ContextLine(line[1:]) + elif line.startswith("+"): + return InsertLine(line[1:]) + elif line.startswith("-"): + return RemoveLine(line[1:]) + else: + raise MalformedLine("Unknown line type", line) +__pychecker__="" + + +class Hunk: + def __init__(self, orig_pos, orig_range, mod_pos, mod_range): + self.orig_pos = orig_pos + self.orig_range = orig_range + self.mod_pos = mod_pos + self.mod_range = mod_range + self.lines = [] + + def get_header(self): + return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos, + self.orig_range), + self.range_str(self.mod_pos, + self.mod_range)) + + def range_str(self, pos, range): + """Return a file range, special-casing for 1-line files. + + :param pos: The position in the file + :type pos: int + :range: The range in the file + :type range: int + :return: a string in the format 1,4 except when range == pos == 1 + """ + if range == 1: + return "%i" % pos + else: + return "%i,%i" % (pos, range) + + def __str__(self): + lines = [self.get_header()] + for line in self.lines: + lines.append(str(line)) + return "".join(lines) + + def shift_to_mod(self, pos): + if pos < self.orig_pos-1: + return 0 + elif pos > self.orig_pos+self.orig_range: + return self.mod_range - self.orig_range + else: + return self.shift_to_mod_lines(pos) + + def shift_to_mod_lines(self, pos): + assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range) + position = self.orig_pos-1 + shift = 0 + for line in self.lines: + if isinstance(line, InsertLine): + shift += 1 + elif isinstance(line, RemoveLine): + if position == pos: + return None + shift -= 1 + position += 1 + elif isinstance(line, ContextLine): + position += 1 + if position > pos: + break + return shift + +def iter_hunks(iter_lines): + hunk = None + for line in iter_lines: + if line.startswith("@@"): + if hunk is not None: + yield hunk + hunk = hunk_from_header(line) + else: + hunk.lines.append(parse_line(line)) + + if hunk is not None: + yield hunk + +class Patch: + def __init__(self, oldname, newname): + self.oldname = oldname + self.newname = newname + self.hunks = [] + + def __str__(self): + ret = "--- %s\n+++ %s\n" % (self.oldname, self.newname) + ret += "".join([str(h) for h in self.hunks]) + return ret + + def stats_str(self): + """Return a string of patch statistics""" + removes = 0 + inserts = 0 + for hunk in self.hunks: + for line in hunk.lines: + if isinstance(line, InsertLine): + inserts+=1; + elif isinstance(line, RemoveLine): + removes+=1; + return "%i inserts, %i removes in %i hunks" % \ + (inserts, removes, len(self.hunks)) + + def pos_in_mod(self, position): + newpos = position + for hunk in self.hunks: + shift = hunk.shift_to_mod(position) + if shift is None: + return None + newpos += shift + return newpos + + def iter_inserted(self): + """Iteraties through inserted lines + + :return: Pair of line number, line + :rtype: iterator of (int, InsertLine) + """ + for hunk in self.hunks: + pos = hunk.mod_pos - 1; + for line in hunk.lines: + if isinstance(line, InsertLine): + yield (pos, line) + pos += 1 + if isinstance(line, ContextLine): + pos += 1 + +def parse_patch(iter_lines): + (orig_name, mod_name) = get_patch_names(iter_lines) + patch = Patch(orig_name, mod_name) + for hunk in iter_hunks(iter_lines): + patch.hunks.append(hunk) + return patch + + +class AnnotateLine: + """A line associated with the log that produced it""" + def __init__(self, text, log=None): + self.text = text + self.log = log + +class CantGetRevisionData(Exception): + def __init__(self, revision): + Exception.__init__(self, "Can't get data for revision %s" % revision) + +def annotate_file2(file_lines, anno_iter): + for result in iter_annotate_file(file_lines, anno_iter): + pass + return result + + +def iter_annotate_file(file_lines, anno_iter): + lines = [AnnotateLine(f) for f in file_lines] + patches = [] + try: + for result in anno_iter: + if isinstance(result, progress.Progress): + yield result + continue + log, iter_inserted, patch = result + for (num, line) in iter_inserted: + old_num = num + + for cur_patch in patches: + num = cur_patch.pos_in_mod(num) + if num == None: + break + + if num >= len(lines): + continue + if num is not None and lines[num].log is None: + lines[num].log = log + patches=[patch]+patches + except CantGetRevisionData: + pass + yield lines + + +def difference_index(atext, btext): + """Find the indext of the first character that differs betweeen two texts + + :param atext: The first text + :type atext: str + :param btext: The second text + :type str: str + :return: The index, or None if there are no differences within the range + :rtype: int or NoneType + """ + length = len(atext) + if len(btext) < length: + length = len(btext) + for i in range(length): + if atext[i] != btext[i]: + return i; + return None + + +def test(): + import unittest + class PatchesTester(unittest.TestCase): + def testValidPatchHeader(self): + """Parse a valid patch header""" + lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n') + (orig, mod) = get_patch_names(lines.__iter__()) + assert(orig == "orig/commands.py") + assert(mod == "mod/dommands.py") + + def testInvalidPatchHeader(self): + """Parse an invalid patch header""" + lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n') + self.assertRaises(MalformedPatchHeader, get_patch_names, + lines.__iter__()) + + def testValidHunkHeader(self): + """Parse a valid hunk header""" + header = "@@ -34,11 +50,6 @@\n" + hunk = hunk_from_header(header); + assert (hunk.orig_pos == 34) + assert (hunk.orig_range == 11) + assert (hunk.mod_pos == 50) + assert (hunk.mod_range == 6) + assert (str(hunk) == header) + + def testValidHunkHeader2(self): + """Parse a tricky, valid hunk header""" + header = "@@ -1 +0,0 @@\n" + hunk = hunk_from_header(header); + assert (hunk.orig_pos == 1) + assert (hunk.orig_range == 1) + assert (hunk.mod_pos == 0) + assert (hunk.mod_range == 0) + assert (str(hunk) == header) + + def makeMalformed(self, header): + self.assertRaises(MalformedHunkHeader, hunk_from_header, header) + + def testInvalidHeader(self): + """Parse an invalid hunk header""" + self.makeMalformed(" -34,11 +50,6 \n") + self.makeMalformed("@@ +50,6 -34,11 @@\n") + self.makeMalformed("@@ -34,11 +50,6 @@") + self.makeMalformed("@@ -34.5,11 +50,6 @@\n") + self.makeMalformed("@@-34,11 +50,6@@\n") + self.makeMalformed("@@ 34,11 50,6 @@\n") + self.makeMalformed("@@ -34,11 @@\n") + self.makeMalformed("@@ -34,11 +50,6.5 @@\n") + self.makeMalformed("@@ -34,11 +50,-6 @@\n") + + def lineThing(self,text, type): + line = parse_line(text) + assert(isinstance(line, type)) + assert(str(line)==text) + + def makeMalformedLine(self, text): + self.assertRaises(MalformedLine, parse_line, text) + + def testValidLine(self): + """Parse a valid hunk line""" + self.lineThing(" hello\n", ContextLine) + self.lineThing("+hello\n", InsertLine) + self.lineThing("-hello\n", RemoveLine) + + def testMalformedLine(self): + """Parse invalid valid hunk lines""" + self.makeMalformedLine("hello\n") + + def compare_parsed(self, patchtext): + lines = patchtext.splitlines(True) + patch = parse_patch(lines.__iter__()) + pstr = str(patch) + i = difference_index(patchtext, pstr) + if i is not None: + print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i]) + assert (patchtext == str(patch)) + + def testAll(self): + """Test parsing a whole patch""" + patchtext = """--- orig/commands.py ++++ mod/commands.py +@@ -1337,7 +1337,8 @@ + + def set_title(self, command=None): + try: +- version = self.tree.tree_version.nonarch ++ version = pylon.alias_or_version(self.tree.tree_version, self.tree, ++ full=False) + except: + version = "[no version]" + if command is None: +@@ -1983,7 +1984,11 @@ + version) + if len(new_merges) > 0: + if cmdutil.prompt("Log for merge"): +- mergestuff = cmdutil.log_for_merge(tree, comp_version) ++ if cmdutil.prompt("changelog for merge"): ++ mergestuff = "Patches applied:\\n" ++ mergestuff += pylon.changelog_for_merge(new_merges) ++ else: ++ mergestuff = cmdutil.log_for_merge(tree, comp_version) + log.description += mergestuff + log.save() + try: +""" + self.compare_parsed(patchtext) + + def testInit(self): + """Handle patches missing half the position, range tuple""" + patchtext = \ +"""--- orig/__init__.py ++++ mod/__init__.py +@@ -1 +1,2 @@ + __docformat__ = "restructuredtext en" ++__doc__ = An alternate Arch commandline interface""" + self.compare_parsed(patchtext) + + + + def testLineLookup(self): + """Make sure we can accurately look up mod line from orig""" + patch = parse_patch(open("testdata/diff")) + orig = list(open("testdata/orig")) + mod = list(open("testdata/mod")) + removals = [] + for i in range(len(orig)): + mod_pos = patch.pos_in_mod(i) + if mod_pos is None: + removals.append(orig[i]) + continue + assert(mod[mod_pos]==orig[i]) + rem_iter = removals.__iter__() + for hunk in patch.hunks: + for line in hunk.lines: + if isinstance(line, RemoveLine): + next = rem_iter.next() + if line.contents != next: + sys.stdout.write(" orig:%spatch:%s" % (next, + line.contents)) + assert(line.contents == next) + self.assertRaises(StopIteration, rem_iter.next) + + def testFirstLineRenumber(self): + """Make sure we handle lines at the beginning of the hunk""" + patch = parse_patch(open("testdata/insert_top.patch")) + assert (patch.pos_in_mod(0)==1) + + + patchesTestSuite = unittest.makeSuite(PatchesTester,'test') + runner = unittest.TextTestRunner(verbosity=0) + return runner.run(patchesTestSuite) + + +if __name__ == "__main__": + test() +# arch-tag: d1541a25-eac5-4de9-a476-08a7cecd5683 *** added directory 'testdata' *** added file 'testdata/diff' --- /dev/null +++ testdata/diff @@ -0,0 +1,1154 @@ +--- orig/commands.py ++++ mod/commands.py +@@ -19,25 +19,31 @@ + import arch + import arch.util + import arch.arch ++ ++import pylon.errors ++from pylon.errors import * ++from pylon import errors ++from pylon import util ++from pylon import arch_core ++from pylon import arch_compound ++from pylon import ancillary ++from pylon import misc ++from pylon import paths ++ + import abacmds + import cmdutil + import shutil + import os + import options +-import paths + import time + import cmd + import readline + import re + import string +-import arch_core +-from errors import * +-import errors + import terminal +-import ancillary +-import misc + import email + import smtplib ++import textwrap + + __docformat__ = "restructuredtext" + __doc__ = "Implementation of user (sub) commands" +@@ -257,7 +263,7 @@ + + tree=arch.tree_root() + if len(args) == 0: +- a_spec = cmdutil.comp_revision(tree) ++ a_spec = ancillary.comp_revision(tree) + else: + a_spec = cmdutil.determine_revision_tree(tree, args[0]) + cmdutil.ensure_archive_registered(a_spec.archive) +@@ -284,7 +290,7 @@ + changeset=options.changeset + tmpdir = None + else: +- tmpdir=cmdutil.tmpdir() ++ tmpdir=util.tmpdir() + changeset=tmpdir+"/changeset" + try: + delta=arch.iter_delta(a_spec, b_spec, changeset) +@@ -304,14 +310,14 @@ + if status > 1: + return + if (options.perform_diff): +- chan = cmdutil.ChangesetMunger(changeset) ++ chan = arch_compound.ChangesetMunger(changeset) + chan.read_indices() +- if isinstance(b_spec, arch.Revision): +- b_dir = b_spec.library_find() +- else: +- b_dir = b_spec +- a_dir = a_spec.library_find() + if options.diffopts is not None: ++ if isinstance(b_spec, arch.Revision): ++ b_dir = b_spec.library_find() ++ else: ++ b_dir = b_spec ++ a_dir = a_spec.library_find() + diffopts = options.diffopts.split() + cmdutil.show_custom_diffs(chan, diffopts, a_dir, b_dir) + else: +@@ -517,7 +523,7 @@ + except arch.errors.TreeRootError, e: + print e + return +- from_revision=cmdutil.tree_latest(tree) ++ from_revision = arch_compound.tree_latest(tree) + if from_revision==to_revision: + print "Tree is already up to date with:\n"+str(to_revision)+"." + return +@@ -592,6 +598,9 @@ + + if len(args) == 0: + args = None ++ if options.version is None: ++ return options, tree.tree_version, args ++ + revision=cmdutil.determine_revision_arch(tree, options.version) + return options, revision.get_version(), args + +@@ -601,11 +610,16 @@ + """ + tree=arch.tree_root() + options, version, files = self.parse_commandline(cmdargs, tree) ++ ancestor = None + if options.__dict__.has_key("base") and options.base: + base = cmdutil.determine_revision_tree(tree, options.base) ++ ancestor = base + else: +- base = cmdutil.submit_revision(tree) +- ++ base = ancillary.submit_revision(tree) ++ ancestor = base ++ if ancestor is None: ++ ancestor = arch_compound.tree_latest(tree, version) ++ + writeversion=version + archive=version.archive + source=cmdutil.get_mirror_source(archive) +@@ -625,18 +639,26 @@ + try: + last_revision=tree.iter_logs(version, True).next().revision + except StopIteration, e: +- if cmdutil.prompt("Import from commit"): +- return do_import(version) +- else: +- raise NoVersionLogs(version) +- if last_revision!=version.iter_revisions(True).next(): ++ last_revision = None ++ if ancestor is None: ++ if cmdutil.prompt("Import from commit"): ++ return do_import(version) ++ else: ++ raise NoVersionLogs(version) ++ try: ++ arch_last_revision = version.iter_revisions(True).next() ++ except StopIteration, e: ++ arch_last_revision = None ++ ++ if last_revision != arch_last_revision: ++ print "Tree is not up to date with %s" % str(version) + if not cmdutil.prompt("Out of date"): + raise OutOfDate + else: + allow_old=True + + try: +- if not cmdutil.has_changed(version): ++ if not cmdutil.has_changed(ancestor): + if not cmdutil.prompt("Empty commit"): + raise EmptyCommit + except arch.util.ExecProblem, e: +@@ -645,15 +667,15 @@ + raise MissingID(e) + else: + raise +- log = tree.log_message(create=False) ++ log = tree.log_message(create=False, version=version) + if log is None: + try: + if cmdutil.prompt("Create log"): +- edit_log(tree) ++ edit_log(tree, version) + + except cmdutil.NoEditorSpecified, e: + raise CommandFailed(e) +- log = tree.log_message(create=False) ++ log = tree.log_message(create=False, version=version) + if log is None: + raise NoLogMessage + if log["Summary"] is None or len(log["Summary"].strip()) == 0: +@@ -837,23 +859,24 @@ + if spec is not None: + revision = cmdutil.determine_revision_tree(tree, spec) + else: +- revision = cmdutil.comp_revision(tree) ++ revision = ancillary.comp_revision(tree) + except cmdutil.CantDetermineRevision, e: + raise CommandFailedWrapper(e) + munger = None + + if options.file_contents or options.file_perms or options.deletions\ + or options.additions or options.renames or options.hunk_prompt: +- munger = cmdutil.MungeOpts() +- munger.hunk_prompt = options.hunk_prompt ++ munger = arch_compound.MungeOpts() ++ munger.set_hunk_prompt(cmdutil.colorize, cmdutil.user_hunk_confirm, ++ options.hunk_prompt) + + if len(args) > 0 or options.logs or options.pattern_files or \ + options.control: + if munger is None: +- munger = cmdutil.MungeOpts(True) ++ munger = cmdutil.arch_compound.MungeOpts(True) + munger.all_types(True) + if len(args) > 0: +- t_cwd = cmdutil.tree_cwd(tree) ++ t_cwd = arch_compound.tree_cwd(tree) + for name in args: + if len(t_cwd) > 0: + t_cwd += "/" +@@ -878,7 +901,7 @@ + if options.pattern_files: + munger.add_keep_pattern(options.pattern_files) + +- for line in cmdutil.revert(tree, revision, munger, ++ for line in arch_compound.revert(tree, revision, munger, + not options.no_output): + cmdutil.colorize(line) + +@@ -1042,18 +1065,13 @@ + help_tree_spec() + return + +-def require_version_exists(version, spec): +- if not version.exists(): +- raise cmdutil.CantDetermineVersion(spec, +- "The version %s does not exist." \ +- % version) +- + class Revisions(BaseCommand): + """ + Print a revision name based on a revision specifier + """ + def __init__(self): + self.description="Lists revisions" ++ self.cl_revisions = [] + + def do_command(self, cmdargs): + """ +@@ -1066,224 +1084,68 @@ + self.tree = arch.tree_root() + except arch.errors.TreeRootError: + self.tree = None ++ if options.type == "default": ++ options.type = "archive" + try: +- iter = self.get_iterator(options.type, args, options.reverse, +- options.modified) ++ iter = cmdutil.revision_iterator(self.tree, options.type, args, ++ options.reverse, options.modified, ++ options.shallow) + except cmdutil.CantDetermineRevision, e: + raise CommandFailedWrapper(e) +- ++ except cmdutil.CantDetermineVersion, e: ++ raise CommandFailedWrapper(e) + if options.skip is not None: + iter = cmdutil.iter_skip(iter, int(options.skip)) + +- for revision in iter: +- log = None +- if isinstance(revision, arch.Patchlog): +- log = revision +- revision=revision.revision +- print options.display(revision) +- if log is None and (options.summary or options.creator or +- options.date or options.merges): +- log = revision.patchlog +- if options.creator: +- print " %s" % log.creator +- if options.date: +- print " %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date) +- if options.summary: +- print " %s" % log.summary +- if options.merges: +- showed_title = False +- for revision in log.merged_patches: +- if not showed_title: +- print " Merged:" +- showed_title = True +- print " %s" % revision +- +- def get_iterator(self, type, args, reverse, modified): +- if len(args) > 0: +- spec = args[0] +- else: +- spec = None +- if modified is not None: +- iter = cmdutil.modified_iter(modified, self.tree) +- if reverse: +- return iter +- else: +- return cmdutil.iter_reverse(iter) +- elif type == "archive": +- if spec is None: +- if self.tree is None: +- raise cmdutil.CantDetermineRevision("", +- "Not in a project tree") +- version = cmdutil.determine_version_tree(spec, self.tree) +- else: +- version = cmdutil.determine_version_arch(spec, self.tree) +- cmdutil.ensure_archive_registered(version.archive) +- require_version_exists(version, spec) +- return version.iter_revisions(reverse) +- elif type == "cacherevs": +- if spec is None: +- if self.tree is None: +- raise cmdutil.CantDetermineRevision("", +- "Not in a project tree") +- version = cmdutil.determine_version_tree(spec, self.tree) +- else: +- version = cmdutil.determine_version_arch(spec, self.tree) +- cmdutil.ensure_archive_registered(version.archive) +- require_version_exists(version, spec) +- return cmdutil.iter_cacherevs(version, reverse) +- elif type == "library": +- if spec is None: +- if self.tree is None: +- raise cmdutil.CantDetermineRevision("", +- "Not in a project tree") +- version = cmdutil.determine_version_tree(spec, self.tree) +- else: +- version = cmdutil.determine_version_arch(spec, self.tree) +- return version.iter_library_revisions(reverse) +- elif type == "logs": +- if self.tree is None: +- raise cmdutil.CantDetermineRevision("", "Not in a project tree") +- return self.tree.iter_logs(cmdutil.determine_version_tree(spec, \ +- self.tree), reverse) +- elif type == "missing" or type == "skip-present": +- if self.tree is None: +- raise cmdutil.CantDetermineRevision("", "Not in a project tree") +- skip = (type == "skip-present") +- version = cmdutil.determine_version_tree(spec, self.tree) +- cmdutil.ensure_archive_registered(version.archive) +- require_version_exists(version, spec) +- return cmdutil.iter_missing(self.tree, version, reverse, +- skip_present=skip) +- +- elif type == "present": +- if self.tree is None: +- raise cmdutil.CantDetermineRevision("", "Not in a project tree") +- version = cmdutil.determine_version_tree(spec, self.tree) +- cmdutil.ensure_archive_registered(version.archive) +- require_version_exists(version, spec) +- return cmdutil.iter_present(self.tree, version, reverse) +- +- elif type == "new-merges" or type == "direct-merges": +- if self.tree is None: +- raise cmdutil.CantDetermineRevision("", "Not in a project tree") +- version = cmdutil.determine_version_tree(spec, self.tree) +- cmdutil.ensure_archive_registered(version.archive) +- require_version_exists(version, spec) +- iter = cmdutil.iter_new_merges(self.tree, version, reverse) +- if type == "new-merges": +- return iter +- elif type == "direct-merges": +- return cmdutil.direct_merges(iter) +- +- elif type == "missing-from": +- if self.tree is None: +- raise cmdutil.CantDetermineRevision("", "Not in a project tree") +- revision = cmdutil.determine_revision_tree(self.tree, spec) +- libtree = cmdutil.find_or_make_local_revision(revision) +- return cmdutil.iter_missing(libtree, self.tree.tree_version, +- reverse) +- +- elif type == "partner-missing": +- return cmdutil.iter_partner_missing(self.tree, reverse) +- +- elif type == "ancestry": +- revision = cmdutil.determine_revision_tree(self.tree, spec) +- iter = cmdutil._iter_ancestry(self.tree, revision) +- if reverse: +- return iter +- else: +- return cmdutil.iter_reverse(iter) +- +- elif type == "dependencies" or type == "non-dependencies": +- nondeps = (type == "non-dependencies") +- revision = cmdutil.determine_revision_tree(self.tree, spec) +- anc_iter = cmdutil._iter_ancestry(self.tree, revision) +- iter_depends = cmdutil.iter_depends(anc_iter, nondeps) +- if reverse: +- return iter_depends +- else: +- return cmdutil.iter_reverse(iter_depends) +- elif type == "micro": +- return cmdutil.iter_micro(self.tree) +- +- ++ try: ++ for revision in iter: ++ log = None ++ if isinstance(revision, arch.Patchlog): ++ log = revision ++ revision=revision.revision ++ out = options.display(revision) ++ if out is not None: ++ print out ++ if log is None and (options.summary or options.creator or ++ options.date or options.merges): ++ log = revision.patchlog ++ if options.creator: ++ print " %s" % log.creator ++ if options.date: ++ print " %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date) ++ if options.summary: ++ print " %s" % log.summary ++ if options.merges: ++ showed_title = False ++ for revision in log.merged_patches: ++ if not showed_title: ++ print " Merged:" ++ showed_title = True ++ print " %s" % revision ++ if len(self.cl_revisions) > 0: ++ print pylon.changelog_for_merge(self.cl_revisions) ++ except pylon.errors.TreeRootNone: ++ raise CommandFailedWrapper( ++ Exception("This option can only be used in a project tree.")) ++ ++ def changelog_append(self, revision): ++ if isinstance(revision, arch.Revision): ++ revision=arch.Patchlog(revision) ++ self.cl_revisions.append(revision) ++ + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ +- parser=cmdutil.CmdOptionParser("fai revisions [revision]") ++ parser=cmdutil.CmdOptionParser("fai revisions [version/revision]") + select = cmdutil.OptionGroup(parser, "Selection options", + "Control which revisions are listed. These options" + " are mutually exclusive. If more than one is" + " specified, the last is used.") +- select.add_option("", "--archive", action="store_const", +- const="archive", dest="type", default="archive", +- help="List all revisions in the archive") +- select.add_option("", "--cacherevs", action="store_const", +- const="cacherevs", dest="type", +- help="List all revisions stored in the archive as " +- "complete copies") +- select.add_option("", "--logs", action="store_const", +- const="logs", dest="type", +- help="List revisions that have a patchlog in the " +- "tree") +- select.add_option("", "--missing", action="store_const", +- const="missing", dest="type", +- help="List revisions from the specified version that" +- " have no patchlog in the tree") +- select.add_option("", "--skip-present", action="store_const", +- const="skip-present", dest="type", +- help="List revisions from the specified version that" +- " have no patchlogs at all in the tree") +- select.add_option("", "--present", action="store_const", +- const="present", dest="type", +- help="List revisions from the specified version that" +- " have no patchlog in the tree, but can't be merged") +- select.add_option("", "--missing-from", action="store_const", +- const="missing-from", dest="type", +- help="List revisions from the specified revision " +- "that have no patchlog for the tree version") +- select.add_option("", "--partner-missing", action="store_const", +- const="partner-missing", dest="type", +- help="List revisions in partner versions that are" +- " missing") +- select.add_option("", "--new-merges", action="store_const", +- const="new-merges", dest="type", +- help="List revisions that have had patchlogs added" +- " to the tree since the last commit") +- select.add_option("", "--direct-merges", action="store_const", +- const="direct-merges", dest="type", +- help="List revisions that have been directly added" +- " to tree since the last commit ") +- select.add_option("", "--library", action="store_const", +- const="library", dest="type", +- help="List revisions in the revision library") +- select.add_option("", "--ancestry", action="store_const", +- const="ancestry", dest="type", +- help="List revisions that are ancestors of the " +- "current tree version") +- +- select.add_option("", "--dependencies", action="store_const", +- const="dependencies", dest="type", +- help="List revisions that the given revision " +- "depends on") +- +- select.add_option("", "--non-dependencies", action="store_const", +- const="non-dependencies", dest="type", +- help="List revisions that the given revision " +- "does not depend on") +- +- select.add_option("--micro", action="store_const", +- const="micro", dest="type", +- help="List partner revisions aimed for this " +- "micro-branch") +- +- select.add_option("", "--modified", dest="modified", +- help="List tree ancestor revisions that modified a " +- "given file", metavar="FILE[:LINE]") + ++ cmdutil.add_revision_iter_options(select) + parser.add_option("", "--skip", dest="skip", + help="Skip revisions. Positive numbers skip from " + "beginning, negative skip from end.", +@@ -1312,6 +1174,9 @@ + format.add_option("--cacherev", action="store_const", + const=paths.determine_cacherev_path, dest="display", + help="Show location of cacherev file") ++ format.add_option("--changelog", action="store_const", ++ const=self.changelog_append, dest="display", ++ help="Show location of cacherev file") + parser.add_option_group(format) + display = cmdutil.OptionGroup(parser, "Display format options", + "These control the display of data") +@@ -1448,6 +1313,7 @@ + if os.access(self.history_file, os.R_OK) and \ + os.path.isfile(self.history_file): + readline.read_history_file(self.history_file) ++ self.cwd = os.getcwd() + + def write_history(self): + readline.write_history_file(self.history_file) +@@ -1470,16 +1336,21 @@ + def set_prompt(self): + if self.tree is not None: + try: +- version = " "+self.tree.tree_version.nonarch ++ prompt = pylon.alias_or_version(self.tree.tree_version, ++ self.tree, ++ full=False) ++ if prompt is not None: ++ prompt = " " + prompt + except: +- version = "" ++ prompt = "" + else: +- version = "" +- self.prompt = "Fai%s> " % version ++ prompt = "" ++ self.prompt = "Fai%s> " % prompt + + def set_title(self, command=None): + try: +- version = self.tree.tree_version.nonarch ++ version = pylon.alias_or_version(self.tree.tree_version, self.tree, ++ full=False) + except: + version = "[no version]" + if command is None: +@@ -1489,8 +1360,15 @@ + def do_cd(self, line): + if line == "": + line = "~" ++ line = os.path.expanduser(line) ++ if os.path.isabs(line): ++ newcwd = line ++ else: ++ newcwd = self.cwd+'/'+line ++ newcwd = os.path.normpath(newcwd) + try: +- os.chdir(os.path.expanduser(line)) ++ os.chdir(newcwd) ++ self.cwd = newcwd + except Exception, e: + print e + try: +@@ -1523,7 +1401,7 @@ + except cmdutil.CantDetermineRevision, e: + print e + except Exception, e: +- print "Unhandled error:\n%s" % cmdutil.exception_str(e) ++ print "Unhandled error:\n%s" % errors.exception_str(e) + + elif suggestions.has_key(args[0]): + print suggestions[args[0]] +@@ -1574,7 +1452,7 @@ + arg = line.split()[-1] + else: + arg = "" +- iter = iter_munged_completions(iter, arg, text) ++ iter = cmdutil.iter_munged_completions(iter, arg, text) + except Exception, e: + print e + return list(iter) +@@ -1604,10 +1482,11 @@ + else: + arg = "" + if arg.startswith("-"): +- return list(iter_munged_completions(iter, arg, text)) ++ return list(cmdutil.iter_munged_completions(iter, arg, ++ text)) + else: +- return list(iter_munged_completions( +- iter_file_completions(arg), arg, text)) ++ return list(cmdutil.iter_munged_completions( ++ cmdutil.iter_file_completions(arg), arg, text)) + + + elif cmd == "cd": +@@ -1615,13 +1494,13 @@ + arg = args.split()[-1] + else: + arg = "" +- iter = iter_dir_completions(arg) +- iter = iter_munged_completions(iter, arg, text) ++ iter = cmdutil.iter_dir_completions(arg) ++ iter = cmdutil.iter_munged_completions(iter, arg, text) + return list(iter) + elif len(args)>0: + arg = args.split()[-1] +- return list(iter_munged_completions(iter_file_completions(arg), +- arg, text)) ++ iter = cmdutil.iter_file_completions(arg) ++ return list(cmdutil.iter_munged_completions(iter, arg, text)) + else: + return self.completenames(text, line, begidx, endidx) + except Exception, e: +@@ -1636,44 +1515,8 @@ + yield entry + + +-def iter_file_completions(arg, only_dirs = False): +- """Generate an iterator that iterates through filename completions. +- +- :param arg: The filename fragment to match +- :type arg: str +- :param only_dirs: If true, match only directories +- :type only_dirs: bool +- """ +- cwd = os.getcwd() +- if cwd != "/": +- extras = [".", ".."] +- else: +- extras = [] +- (dir, file) = os.path.split(arg) +- if dir != "": +- listingdir = os.path.expanduser(dir) +- else: +- listingdir = cwd +- for file in cmdutil.iter_combine([os.listdir(listingdir), extras]): +- if dir != "": +- userfile = dir+'/'+file +- else: +- userfile = file +- if userfile.startswith(arg): +- if os.path.isdir(listingdir+'/'+file): +- userfile+='/' +- yield userfile +- elif not only_dirs: +- yield userfile +- +-def iter_munged_completions(iter, arg, text): +- for completion in iter: +- completion = str(completion) +- if completion.startswith(arg): +- yield completion[len(arg)-len(text):] +- + def iter_source_file_completions(tree, arg): +- treepath = cmdutil.tree_cwd(tree) ++ treepath = arch_compound.tree_cwd(tree) + if len(treepath) > 0: + dirs = [treepath] + else: +@@ -1701,7 +1544,7 @@ + :return: An iterator of all matching untagged files + :rtype: iterator of str + """ +- treepath = cmdutil.tree_cwd(tree) ++ treepath = arch_compound.tree_cwd(tree) + if len(treepath) > 0: + dirs = [treepath] + else: +@@ -1743,8 +1586,8 @@ + :param arg: The prefix to match + :type arg: str + """ +- treepath = cmdutil.tree_cwd(tree) +- tmpdir = cmdutil.tmpdir() ++ treepath = arch_compound.tree_cwd(tree) ++ tmpdir = util.tmpdir() + changeset = tmpdir+"/changeset" + completions = [] + revision = cmdutil.determine_revision_tree(tree) +@@ -1756,14 +1599,6 @@ + shutil.rmtree(tmpdir) + return completions + +-def iter_dir_completions(arg): +- """Generate an iterator that iterates through directory name completions. +- +- :param arg: The directory name fragment to match +- :type arg: str +- """ +- return iter_file_completions(arg, True) +- + class Shell(BaseCommand): + def __init__(self): + self.description = "Runs Fai as a shell" +@@ -1795,7 +1630,11 @@ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + +- tree = arch.tree_root() ++ try: ++ tree = arch.tree_root() ++ except arch.errors.TreeRootError, e: ++ raise pylon.errors.CommandFailedWrapper(e) ++ + + if (len(args) == 0) == (options.untagged == False): + raise cmdutil.GetHelp +@@ -1809,13 +1648,22 @@ + if options.id_type == "tagline": + if method != "tagline": + if not cmdutil.prompt("Tagline in other tree"): +- if method == "explicit": +- options.id_type == explicit ++ if method == "explicit" or method == "implicit": ++ options.id_type == method + else: + print "add-id not supported for \"%s\" tagging method"\ + % method + return + ++ elif options.id_type == "implicit": ++ if method != "implicit": ++ if not cmdutil.prompt("Implicit in other tree"): ++ if method == "explicit" or method == "tagline": ++ options.id_type == method ++ else: ++ print "add-id not supported for \"%s\" tagging method"\ ++ % method ++ return + elif options.id_type == "explicit": + if method != "tagline" and method != explicit: + if not prompt("Explicit in other tree"): +@@ -1824,7 +1672,8 @@ + return + + if options.id_type == "auto": +- if method != "tagline" and method != "explicit": ++ if method != "tagline" and method != "explicit" \ ++ and method !="implicit": + print "add-id not supported for \"%s\" tagging method" % method + return + else: +@@ -1852,10 +1701,12 @@ + previous_files.extend(files) + if id_type == "explicit": + cmdutil.add_id(files) +- elif id_type == "tagline": ++ elif id_type == "tagline" or id_type == "implicit": + for file in files: + try: +- cmdutil.add_tagline_or_explicit_id(file) ++ implicit = (id_type == "implicit") ++ cmdutil.add_tagline_or_explicit_id(file, False, ++ implicit) + except cmdutil.AlreadyTagged: + print "\"%s\" already has a tagline." % file + except cmdutil.NoCommentSyntax: +@@ -1888,6 +1739,9 @@ + parser.add_option("--tagline", action="store_const", + const="tagline", dest="id_type", + help="Use a tagline id") ++ parser.add_option("--implicit", action="store_const", ++ const="implicit", dest="id_type", ++ help="Use an implicit id (deprecated)") + parser.add_option("--untagged", action="store_true", + dest="untagged", default=False, + help="tag all untagged files") +@@ -1926,27 +1780,7 @@ + def get_completer(self, arg, index): + if self.tree is None: + raise arch.errors.TreeRootError +- completions = list(ancillary.iter_partners(self.tree, +- self.tree.tree_version)) +- if len(completions) == 0: +- completions = list(self.tree.iter_log_versions()) +- +- aliases = [] +- try: +- for completion in completions: +- alias = ancillary.compact_alias(str(completion), self.tree) +- if alias: +- aliases.extend(alias) +- +- for completion in completions: +- if completion.archive == self.tree.tree_version.archive: +- aliases.append(completion.nonarch) +- +- except Exception, e: +- print e +- +- completions.extend(aliases) +- return completions ++ return cmdutil.merge_completions(self.tree, arg, index) + + def do_command(self, cmdargs): + """ +@@ -1961,7 +1795,7 @@ + + if self.tree is None: + raise arch.errors.TreeRootError(os.getcwd()) +- if cmdutil.has_changed(self.tree.tree_version): ++ if cmdutil.has_changed(ancillary.comp_revision(self.tree)): + raise UncommittedChanges(self.tree) + + if len(args) > 0: +@@ -2027,14 +1861,14 @@ + :type other_revision: `arch.Revision` + :return: 0 if the merge was skipped, 1 if it was applied + """ +- other_tree = cmdutil.find_or_make_local_revision(other_revision) ++ other_tree = arch_compound.find_or_make_local_revision(other_revision) + try: + if action == "native-merge": +- ancestor = cmdutil.merge_ancestor2(self.tree, other_tree, +- other_revision) ++ ancestor = arch_compound.merge_ancestor2(self.tree, other_tree, ++ other_revision) + elif action == "update": +- ancestor = cmdutil.tree_latest(self.tree, +- other_revision.version) ++ ancestor = arch_compound.tree_latest(self.tree, ++ other_revision.version) + except CantDetermineRevision, e: + raise CommandFailedWrapper(e) + cmdutil.colorize(arch.Chatter("* Found common ancestor %s" % ancestor)) +@@ -2104,7 +1938,10 @@ + if self.tree is None: + raise arch.errors.TreeRootError + +- edit_log(self.tree) ++ try: ++ edit_log(self.tree, self.tree.tree_version) ++ except pylon.errors.NoEditorSpecified, e: ++ raise pylon.errors.CommandFailedWrapper(e) + + def get_parser(self): + """ +@@ -2132,7 +1969,7 @@ + """ + return + +-def edit_log(tree): ++def edit_log(tree, version): + """Makes and edits the log for a tree. Does all kinds of fancy things + like log templates and merge summaries and log-for-merge + +@@ -2141,28 +1978,29 @@ + """ + #ensure we have an editor before preparing the log + cmdutil.find_editor() +- log = tree.log_message(create=False) ++ log = tree.log_message(create=False, version=version) + log_is_new = False + if log is None or cmdutil.prompt("Overwrite log"): + if log is not None: + os.remove(log.name) +- log = tree.log_message(create=True) ++ log = tree.log_message(create=True, version=version) + log_is_new = True + tmplog = log.name +- template = tree+"/{arch}/=log-template" +- if not os.path.exists(template): +- template = os.path.expanduser("~/.arch-params/=log-template") +- if not os.path.exists(template): +- template = None ++ template = pylon.log_template_path(tree) + if template: + shutil.copyfile(template, tmplog) +- +- new_merges = list(cmdutil.iter_new_merges(tree, +- tree.tree_version)) +- log["Summary"] = merge_summary(new_merges, tree.tree_version) ++ comp_version = ancillary.comp_revision(tree).version ++ new_merges = cmdutil.iter_new_merges(tree, comp_version) ++ new_merges = cmdutil.direct_merges(new_merges) ++ log["Summary"] = pylon.merge_summary(new_merges, ++ version) + if len(new_merges) > 0: + if cmdutil.prompt("Log for merge"): +- mergestuff = cmdutil.log_for_merge(tree) ++ if cmdutil.prompt("changelog for merge"): ++ mergestuff = "Patches applied:\n" ++ mergestuff += pylon.changelog_for_merge(new_merges) ++ else: ++ mergestuff = cmdutil.log_for_merge(tree, comp_version) + log.description += mergestuff + log.save() + try: +@@ -2172,29 +2010,6 @@ + os.remove(log.name) + raise + +-def merge_summary(new_merges, tree_version): +- if len(new_merges) == 0: +- return "" +- if len(new_merges) == 1: +- summary = new_merges[0].summary +- else: +- summary = "Merge" +- +- credits = [] +- for merge in new_merges: +- if arch.my_id() != merge.creator: +- name = re.sub("<.*>", "", merge.creator).rstrip(" "); +- if not name in credits: +- credits.append(name) +- else: +- version = merge.revision.version +- if version.archive == tree_version.archive: +- if not version.nonarch in credits: +- credits.append(version.nonarch) +- elif not str(version) in credits: +- credits.append(str(version)) +- +- return ("%s (%s)") % (summary, ", ".join(credits)) + + class MirrorArchive(BaseCommand): + """ +@@ -2268,31 +2083,73 @@ + + Use "alias" to list available (user and automatic) aliases.""" + ++auto_alias = [ ++"acur", ++"The latest revision in the archive of the tree-version. You can specify \ ++a different version like so: acur:foo--bar--0 (aliases can be used)", ++"tcur", ++"""(tree current) The latest revision in the tree of the tree-version. \ ++You can specify a different version like so: tcur:foo--bar--0 (aliases can be \ ++used).""", ++"tprev" , ++"""(tree previous) The previous revision in the tree of the tree-version. To \ ++specify an older revision, use a number, e.g. "tprev:4" """, ++"tanc" , ++"""(tree ancestor) The ancestor revision of the tree To specify an older \ ++revision, use a number, e.g. "tanc:4".""", ++"tdate" , ++"""(tree date) The latest revision from a given date, e.g. "tdate:July 6".""", ++"tmod" , ++""" (tree modified) The latest revision to modify a given file, e.g. \ ++"tmod:engine.cpp" or "tmod:engine.cpp:16".""", ++"ttag" , ++"""(tree tag) The revision that was tagged into the current tree revision, \ ++according to the tree""", ++"tagcur", ++"""(tag current) The latest revision of the version that the current tree \ ++was tagged from.""", ++"mergeanc" , ++"""The common ancestor of the current tree and the specified revision. \ ++Defaults to the first partner-version's latest revision or to tagcur.""", ++] ++ ++ ++def is_auto_alias(name): ++ """Determine whether a name is an auto alias name ++ ++ :param name: the name to check ++ :type name: str ++ :return: True if the name is an auto alias, false if not ++ :rtype: bool ++ """ ++ return name in [f for (f, v) in pylon.util.iter_pairs(auto_alias)] ++ ++ ++def display_def(iter, wrap = 80): ++ """Display a list of definitions ++ ++ :param iter: iter of name, definition pairs ++ :type iter: iter of (str, str) ++ :param wrap: The width for text wrapping ++ :type wrap: int ++ """ ++ vals = list(iter) ++ maxlen = 0 ++ for (key, value) in vals: ++ if len(key) > maxlen: ++ maxlen = len(key) ++ for (key, value) in vals: ++ tw=textwrap.TextWrapper(width=wrap, ++ initial_indent=key.rjust(maxlen)+" : ", ++ subsequent_indent="".rjust(maxlen+3)) ++ print tw.fill(value) ++ ++ + def help_aliases(tree): +- print """Auto-generated aliases +- acur : The latest revision in the archive of the tree-version. You can specfy +- a different version like so: acur:foo--bar--0 (aliases can be used) +- tcur : (tree current) The latest revision in the tree of the tree-version. +- You can specify a different version like so: tcur:foo--bar--0 (aliases +- can be used). +-tprev : (tree previous) The previous revision in the tree of the tree-version. +- To specify an older revision, use a number, e.g. "tprev:4" +- tanc : (tree ancestor) The ancestor revision of the tree +- To specify an older revision, use a number, e.g. "tanc:4" +-tdate : (tree date) The latest revision from a given date (e.g. "tdate:July 6") +- tmod : (tree modified) The latest revision to modify a given file +- (e.g. "tmod:engine.cpp" or "tmod:engine.cpp:16") +- ttag : (tree tag) The revision that was tagged into the current tree revision, +- according to the tree. +-tagcur: (tag current) The latest revision of the version that the current tree +- was tagged from. +-mergeanc : The common ancestor of the current tree and the specified revision. +- Defaults to the first partner-version's latest revision or to tagcur. +- """ ++ print """Auto-generated aliases""" ++ display_def(pylon.util.iter_pairs(auto_alias)) + print "User aliases" +- for parts in ancillary.iter_all_alias(tree): +- print parts[0].rjust(10)+" : "+parts[1] +- ++ display_def(ancillary.iter_all_alias(tree)) + + class Inventory(BaseCommand): + """List the status of files in the tree""" +@@ -2428,6 +2285,11 @@ + except cmdutil.ForbiddenAliasSyntax, e: + raise CommandFailedWrapper(e) + ++ def no_prefix(self, alias): ++ if alias.startswith("^"): ++ alias = alias[1:] ++ return alias ++ + def arg_dispatch(self, args, options): + """Add, modify, or list aliases, depending on number of arguments + +@@ -2438,15 +2300,20 @@ + if len(args) == 0: + help_aliases(self.tree) + return +- elif len(args) == 1: +- self.print_alias(args[0]) +- elif (len(args)) == 2: +- self.add(args[0], args[1], options) + else: +- raise cmdutil.GetHelp ++ alias = self.no_prefix(args[0]) ++ if len(args) == 1: ++ self.print_alias(alias) ++ elif (len(args)) == 2: ++ self.add(alias, args[1], options) ++ else: ++ raise cmdutil.GetHelp + + def print_alias(self, alias): + answer = None ++ if is_auto_alias(alias): ++ raise pylon.errors.IsAutoAlias(alias, "\"%s\" is an auto alias." ++ " Use \"revision\" to expand auto aliases." % alias) + for pair in ancillary.iter_all_alias(self.tree): + if pair[0] == alias: + answer = pair[1] +@@ -2464,6 +2331,8 @@ + :type expansion: str + :param options: The commandline options + """ ++ if is_auto_alias(alias): ++ raise IsAutoAlias(alias) + newlist = "" + written = False + new_line = "%s=%s\n" % (alias, cmdutil.expand_alias(expansion, +@@ -2490,14 +2359,17 @@ + deleted = False + if len(args) != 1: + raise cmdutil.GetHelp ++ alias = self.no_prefix(args[0]) ++ if is_auto_alias(alias): ++ raise IsAutoAlias(alias) + newlist = "" + for pair in self.get_iterator(options): +- if pair[0] != args[0]: ++ if pair[0] != alias: + newlist+="%s=%s\n" % (pair[0], pair[1]) + else: + deleted = True + if not deleted: +- raise errors.NoSuchAlias(args[0]) ++ raise errors.NoSuchAlias(alias) + self.write_aliases(newlist, options) + + def get_alias_file(self, options): +@@ -2526,7 +2398,7 @@ + :param options: The commandline options + """ + filename = os.path.expanduser(self.get_alias_file(options)) +- file = cmdutil.NewFileVersion(filename) ++ file = util.NewFileVersion(filename) + file.write(newlist) + file.commit() + +@@ -2588,10 +2460,13 @@ + :param cmdargs: The commandline arguments + :type cmdargs: list of str + """ +- cmdutil.find_editor() + parser = self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: ++ cmdutil.find_editor() ++ except pylon.errors.NoEditorSpecified, e: ++ raise pylon.errors.CommandFailedWrapper(e) ++ try: + self.tree=arch.tree_root() + except: + self.tree=None +@@ -2655,7 +2530,7 @@ + target_revision = cmdutil.determine_revision_arch(self.tree, + args[0]) + else: +- target_revision = cmdutil.tree_latest(self.tree) ++ target_revision = arch_compound.tree_latest(self.tree) + if len(args) > 1: + merges = [ arch.Patchlog(cmdutil.determine_revision_arch( + self.tree, f)) for f in args[1:] ] +@@ -2711,7 +2586,7 @@ + + :param message: The message to send + :type message: `email.Message`""" +- server = smtplib.SMTP() ++ server = smtplib.SMTP("localhost") + server.sendmail(message['From'], message['To'], message.as_string()) + server.quit() + +@@ -2763,6 +2638,22 @@ + 'alias' : Alias, + 'request-merge': RequestMerge, + } ++ ++def my_import(mod_name): ++ module = __import__(mod_name) ++ components = mod_name.split('.') ++ for comp in components[1:]: ++ module = getattr(module, comp) ++ return module ++ ++def plugin(mod_name): ++ module = my_import(mod_name) ++ module.add_command(commands) ++ ++for file in os.listdir(sys.path[0]+"/command"): ++ if len(file) > 3 and file[-3:] == ".py" and file != "__init__.py": ++ plugin("command."+file[:-3]) ++ + suggestions = { + 'apply-delta' : "Try \"apply-changes\".", + 'delta' : "To compare two revisions, use \"changes\".", +@@ -2784,6 +2675,7 @@ + 'tagline' : "Use add-id. It uses taglines in tagline trees", + 'emlog' : "Use elog. It automatically adds log-for-merge text, if any", + 'library-revisions' : "Use revisions --library", +-'file-revert' : "Use revert FILE" ++'file-revert' : "Use revert FILE", ++'join-branch' : "Use replay --logs-only" + } + # arch-tag: 19d5739d-3708-486c-93ba-deecc3027fc7 *** added file 'testdata/insert_top.patch' --- /dev/null +++ testdata/insert_top.patch @@ -0,0 +1,7 @@ +--- orig/pylon/patches.py ++++ mod/pylon/patches.py +@@ -1,3 +1,4 @@ ++#test + import util + import sys + class PatchSyntax(Exception): *** added file 'testdata/mod' --- /dev/null +++ testdata/mod @@ -0,0 +1,2681 @@ +# Copyright (C) 2004 Aaron Bentley +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import sys +import arch +import arch.util +import arch.arch + +import pylon.errors +from pylon.errors import * +from pylon import errors +from pylon import util +from pylon import arch_core +from pylon import arch_compound +from pylon import ancillary +from pylon import misc +from pylon import paths + +import abacmds +import cmdutil +import shutil +import os +import options +import time +import cmd +import readline +import re +import string +import terminal +import email +import smtplib +import textwrap + +__docformat__ = "restructuredtext" +__doc__ = "Implementation of user (sub) commands" +commands = {} + +def find_command(cmd): + """ + Return an instance of a command type. Return None if the type isn't + registered. + + :param cmd: the name of the command to look for + :type cmd: the type of the command + """ + if commands.has_key(cmd): + return commands[cmd]() + else: + return None + +class BaseCommand: + def __call__(self, cmdline): + try: + self.do_command(cmdline.split()) + except cmdutil.GetHelp, e: + self.help() + except Exception, e: + print e + + def get_completer(index): + return None + + def complete(self, args, text): + """ + Returns a list of possible completions for the given text. + + :param args: The complete list of arguments + :type args: List of str + :param text: text to complete (may be shorter than args[-1]) + :type text: str + :rtype: list of str + """ + matches = [] + candidates = None + + if len(args) > 0: + realtext = args[-1] + else: + realtext = "" + + try: + parser=self.get_parser() + if realtext.startswith('-'): + candidates = parser.iter_options() + else: + (options, parsed_args) = parser.parse_args(args) + + if len (parsed_args) > 0: + candidates = self.get_completer(parsed_args[-1], len(parsed_args) -1) + else: + candidates = self.get_completer("", 0) + except: + pass + if candidates is None: + return + for candidate in candidates: + candidate = str(candidate) + if candidate.startswith(realtext): + matches.append(candidate[len(realtext)- len(text):]) + return matches + + +class Help(BaseCommand): + """ + Lists commands, prints help messages. + """ + def __init__(self): + self.description="Prints help mesages" + self.parser = None + + def do_command(self, cmdargs): + """ + Prints a help message. + """ + options, args = self.get_parser().parse_args(cmdargs) + if len(args) > 1: + raise cmdutil.GetHelp + + if options.native or options.suggestions or options.external: + native = options.native + suggestions = options.suggestions + external = options.external + else: + native = True + suggestions = False + external = True + + if len(args) == 0: + self.list_commands(native, suggestions, external) + return + elif len(args) == 1: + command_help(args[0]) + return + + def help(self): + self.get_parser().print_help() + print """ +If no command is specified, commands are listed. If a command is +specified, help for that command is listed. + """ + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + if self.parser is not None: + return self.parser + parser=cmdutil.CmdOptionParser("fai help [command]") + parser.add_option("-n", "--native", action="store_true", + dest="native", help="Show native commands") + parser.add_option("-e", "--external", action="store_true", + dest="external", help="Show external commands") + parser.add_option("-s", "--suggest", action="store_true", + dest="suggestions", help="Show suggestions") + self.parser = parser + return parser + + def list_commands(self, native=True, suggest=False, external=True): + """ + Lists supported commands. + + :param native: list native, python-based commands + :type native: bool + :param external: list external aba-style commands + :type external: bool + """ + if native: + print "Native Fai commands" + keys=commands.keys() + keys.sort() + for k in keys: + space="" + for i in range(28-len(k)): + space+=" " + print space+k+" : "+commands[k]().description + print + if suggest: + print "Unavailable commands and suggested alternatives" + key_list = suggestions.keys() + key_list.sort() + for key in key_list: + print "%28s : %s" % (key, suggestions[key]) + print + if external: + fake_aba = abacmds.AbaCmds() + if (fake_aba.abadir == ""): + return + print "External commands" + fake_aba.list_commands() + print + if not suggest: + print "Use help --suggest to list alternatives to tla and aba"\ + " commands." + if options.tla_fallthrough and (native or external): + print "Fai also supports tla commands." + +def command_help(cmd): + """ + Prints help for a command. + + :param cmd: The name of the command to print help for + :type cmd: str + """ + fake_aba = abacmds.AbaCmds() + cmdobj = find_command(cmd) + if cmdobj != None: + cmdobj.help() + elif suggestions.has_key(cmd): + print "Not available\n" + suggestions[cmd] + else: + abacmd = fake_aba.is_command(cmd) + if abacmd: + abacmd.help() + else: + print "No help is available for \""+cmd+"\". Maybe try \"tla "+cmd+" -H\"?" + + + +class Changes(BaseCommand): + """ + the "changes" command: lists differences between trees/revisions: + """ + + def __init__(self): + self.description="Lists what files have changed in the project tree" + + def get_completer(self, arg, index): + if index > 1: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def parse_commandline(self, cmdline): + """ + Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. + + :param cmdline: A list of arguments to parse + :rtype: (options, Revision, Revision/WorkingTree) + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdline) + if len(args) > 2: + raise cmdutil.GetHelp + + tree=arch.tree_root() + if len(args) == 0: + a_spec = ancillary.comp_revision(tree) + else: + a_spec = cmdutil.determine_revision_tree(tree, args[0]) + cmdutil.ensure_archive_registered(a_spec.archive) + if len(args) == 2: + b_spec = cmdutil.determine_revision_tree(tree, args[1]) + cmdutil.ensure_archive_registered(b_spec.archive) + else: + b_spec=tree + return options, a_spec, b_spec + + def do_command(self, cmdargs): + """ + Master function that perfoms the "changes" command. + """ + try: + options, a_spec, b_spec = self.parse_commandline(cmdargs); + except cmdutil.CantDetermineRevision, e: + print e + return + except arch.errors.TreeRootError, e: + print e + return + if options.changeset: + changeset=options.changeset + tmpdir = None + else: + tmpdir=util.tmpdir() + changeset=tmpdir+"/changeset" + try: + delta=arch.iter_delta(a_spec, b_spec, changeset) + try: + for line in delta: + if cmdutil.chattermatch(line, "changeset:"): + pass + else: + cmdutil.colorize(line, options.suppress_chatter) + except arch.util.ExecProblem, e: + if e.proc.error and e.proc.error.startswith( + "missing explicit id for file"): + raise MissingID(e) + else: + raise + status=delta.status + if status > 1: + return + if (options.perform_diff): + chan = arch_compound.ChangesetMunger(changeset) + chan.read_indices() + if options.diffopts is not None: + if isinstance(b_spec, arch.Revision): + b_dir = b_spec.library_find() + else: + b_dir = b_spec + a_dir = a_spec.library_find() + diffopts = options.diffopts.split() + cmdutil.show_custom_diffs(chan, diffopts, a_dir, b_dir) + else: + cmdutil.show_diffs(delta.changeset) + finally: + if tmpdir and (os.access(tmpdir, os.X_OK)): + shutil.rmtree(tmpdir) + + def get_parser(self): + """ + Returns the options parser to use for the "changes" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai changes [options] [revision]" + " [revision]") + parser.add_option("-d", "--diff", action="store_true", + dest="perform_diff", default=False, + help="Show diffs in summary") + parser.add_option("-c", "--changeset", dest="changeset", + help="Store a changeset in the given directory", + metavar="DIRECTORY") + parser.add_option("-s", "--silent", action="store_true", + dest="suppress_chatter", default=False, + help="Suppress chatter messages") + parser.add_option("--diffopts", dest="diffopts", + help="Use the specified diff options", + metavar="OPTIONS") + + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser is None: + parser=self.get_parser() + parser.print_help() + print """ +Performs source-tree comparisons + +If no revision is specified, the current project tree is compared to the +last-committed revision. If one revision is specified, the current project +tree is compared to that revision. If two revisions are specified, they are +compared to each other. + """ + help_tree_spec() + return + + +class ApplyChanges(BaseCommand): + """ + Apply differences between two revisions to a tree + """ + + def __init__(self): + self.description="Applies changes to a project tree" + + def get_completer(self, arg, index): + if index > 1: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def parse_commandline(self, cmdline, tree): + """ + Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. + + :param cmdline: A list of arguments to parse + :rtype: (options, Revision, Revision/WorkingTree) + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdline) + if len(args) != 2: + raise cmdutil.GetHelp + + a_spec = cmdutil.determine_revision_tree(tree, args[0]) + cmdutil.ensure_archive_registered(a_spec.archive) + b_spec = cmdutil.determine_revision_tree(tree, args[1]) + cmdutil.ensure_archive_registered(b_spec.archive) + return options, a_spec, b_spec + + def do_command(self, cmdargs): + """ + Master function that performs "apply-changes". + """ + try: + tree = arch.tree_root() + options, a_spec, b_spec = self.parse_commandline(cmdargs, tree); + except cmdutil.CantDetermineRevision, e: + print e + return + except arch.errors.TreeRootError, e: + print e + return + delta=cmdutil.apply_delta(a_spec, b_spec, tree) + for line in cmdutil.iter_apply_delta_filter(delta): + cmdutil.colorize(line, options.suppress_chatter) + + def get_parser(self): + """ + Returns the options parser to use for the "apply-changes" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai apply-changes [options] revision" + " revision") + parser.add_option("-d", "--diff", action="store_true", + dest="perform_diff", default=False, + help="Show diffs in summary") + parser.add_option("-c", "--changeset", dest="changeset", + help="Store a changeset in the given directory", + metavar="DIRECTORY") + parser.add_option("-s", "--silent", action="store_true", + dest="suppress_chatter", default=False, + help="Suppress chatter messages") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser is None: + parser=self.get_parser() + parser.print_help() + print """ +Applies changes to a project tree + +Compares two revisions and applies the difference between them to the current +tree. + """ + help_tree_spec() + return + +class Update(BaseCommand): + """ + Updates a project tree to a given revision, preserving un-committed hanges. + """ + + def __init__(self): + self.description="Apply the latest changes to the current directory" + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def parse_commandline(self, cmdline, tree): + """ + Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. + + :param cmdline: A list of arguments to parse + :rtype: (options, Revision, Revision/WorkingTree) + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdline) + if len(args) > 2: + raise cmdutil.GetHelp + + spec=None + if len(args)>0: + spec=args[0] + revision=cmdutil.determine_revision_arch(tree, spec) + cmdutil.ensure_archive_registered(revision.archive) + + mirror_source = cmdutil.get_mirror_source(revision.archive) + if mirror_source != None: + if cmdutil.prompt("Mirror update"): + cmd=cmdutil.mirror_archive(mirror_source, + revision.archive, arch.NameParser(revision).get_package_version()) + for line in arch.chatter_classifier(cmd): + cmdutil.colorize(line, options.suppress_chatter) + + revision=cmdutil.determine_revision_arch(tree, spec) + + return options, revision + + def do_command(self, cmdargs): + """ + Master function that perfoms the "update" command. + """ + tree=arch.tree_root() + try: + options, to_revision = self.parse_commandline(cmdargs, tree); + except cmdutil.CantDetermineRevision, e: + print e + return + except arch.errors.TreeRootError, e: + print e + return + from_revision = arch_compound.tree_latest(tree) + if from_revision==to_revision: + print "Tree is already up to date with:\n"+str(to_revision)+"." + return + cmdutil.ensure_archive_registered(from_revision.archive) + cmd=cmdutil.apply_delta(from_revision, to_revision, tree, + options.patch_forward) + for line in cmdutil.iter_apply_delta_filter(cmd): + cmdutil.colorize(line) + if to_revision.version != tree.tree_version: + if cmdutil.prompt("Update version"): + tree.tree_version = to_revision.version + + def get_parser(self): + """ + Returns the options parser to use for the "update" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai update [options]" + " [revision/version]") + parser.add_option("-f", "--forward", action="store_true", + dest="patch_forward", default=False, + help="pass the --forward option to 'patch'") + parser.add_option("-s", "--silent", action="store_true", + dest="suppress_chatter", default=False, + help="Suppress chatter messages") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser is None: + parser=self.get_parser() + parser.print_help() + print """ +Updates a working tree to the current archive revision + +If a revision or version is specified, that is used instead + """ + help_tree_spec() + return + + +class Commit(BaseCommand): + """ + Create a revision based on the changes in the current tree. + """ + + def __init__(self): + self.description="Write local changes to the archive" + + def get_completer(self, arg, index): + if arg is None: + arg = "" + return iter_modified_file_completions(arch.tree_root(), arg) +# return iter_source_file_completions(arch.tree_root(), arg) + + def parse_commandline(self, cmdline, tree): + """ + Parse commandline arguments. Raise cmtutil.GetHelp if help is needed. + + :param cmdline: A list of arguments to parse + :rtype: (options, Revision, Revision/WorkingTree) + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdline) + + if len(args) == 0: + args = None + if options.version is None: + return options, tree.tree_version, args + + revision=cmdutil.determine_revision_arch(tree, options.version) + return options, revision.get_version(), args + + def do_command(self, cmdargs): + """ + Master function that perfoms the "commit" command. + """ + tree=arch.tree_root() + options, version, files = self.parse_commandline(cmdargs, tree) + ancestor = None + if options.__dict__.has_key("base") and options.base: + base = cmdutil.determine_revision_tree(tree, options.base) + ancestor = base + else: + base = ancillary.submit_revision(tree) + ancestor = base + if ancestor is None: + ancestor = arch_compound.tree_latest(tree, version) + + writeversion=version + archive=version.archive + source=cmdutil.get_mirror_source(archive) + allow_old=False + writethrough="implicit" + + if source!=None: + if writethrough=="explicit" and \ + cmdutil.prompt("Writethrough"): + writeversion=arch.Version(str(source)+"/"+str(version.get_nonarch())) + elif writethrough=="none": + raise CommitToMirror(archive) + + elif archive.is_mirror: + raise CommitToMirror(archive) + + try: + last_revision=tree.iter_logs(version, True).next().revision + except StopIteration, e: + last_revision = None + if ancestor is None: + if cmdutil.prompt("Import from commit"): + return do_import(version) + else: + raise NoVersionLogs(version) + try: + arch_last_revision = version.iter_revisions(True).next() + except StopIteration, e: + arch_last_revision = None + + if last_revision != arch_last_revision: + print "Tree is not up to date with %s" % str(version) + if not cmdutil.prompt("Out of date"): + raise OutOfDate + else: + allow_old=True + + try: + if not cmdutil.has_changed(ancestor): + if not cmdutil.prompt("Empty commit"): + raise EmptyCommit + except arch.util.ExecProblem, e: + if e.proc.error and e.proc.error.startswith( + "missing explicit id for file"): + raise MissingID(e) + else: + raise + log = tree.log_message(create=False, version=version) + if log is None: + try: + if cmdutil.prompt("Create log"): + edit_log(tree, version) + + except cmdutil.NoEditorSpecified, e: + raise CommandFailed(e) + log = tree.log_message(create=False, version=version) + if log is None: + raise NoLogMessage + if log["Summary"] is None or len(log["Summary"].strip()) == 0: + if not cmdutil.prompt("Omit log summary"): + raise errors.NoLogSummary + try: + for line in tree.iter_commit(version, seal=options.seal_version, + base=base, out_of_date_ok=allow_old, file_list=files): + cmdutil.colorize(line, options.suppress_chatter) + + except arch.util.ExecProblem, e: + if e.proc.error and e.proc.error.startswith( + "These files violate naming conventions:"): + raise LintFailure(e.proc.error) + else: + raise + + def get_parser(self): + """ + Returns the options parser to use for the "commit" command. + + :rtype: cmdutil.CmdOptionParser + """ + + parser=cmdutil.CmdOptionParser("fai commit [options] [file1]" + " [file2...]") + parser.add_option("--seal", action="store_true", + dest="seal_version", default=False, + help="seal this version") + parser.add_option("-v", "--version", dest="version", + help="Use the specified version", + metavar="VERSION") + parser.add_option("-s", "--silent", action="store_true", + dest="suppress_chatter", default=False, + help="Suppress chatter messages") + if cmdutil.supports_switch("commit", "--base"): + parser.add_option("--base", dest="base", help="", + metavar="REVISION") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser is None: + parser=self.get_parser() + parser.print_help() + print """ +Updates a working tree to the current archive revision + +If a version is specified, that is used instead + """ +# help_tree_spec() + return + + + +class CatLog(BaseCommand): + """ + Print the log of a given file (from current tree) + """ + def __init__(self): + self.description="Prints the patch log for a revision" + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "cat-log" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: + tree = arch.tree_root() + except arch.errors.TreeRootError, e: + tree = None + spec=None + if len(args) > 0: + spec=args[0] + if len(args) > 1: + raise cmdutil.GetHelp() + try: + if tree: + revision = cmdutil.determine_revision_tree(tree, spec) + else: + revision = cmdutil.determine_revision_arch(tree, spec) + except cmdutil.CantDetermineRevision, e: + raise CommandFailedWrapper(e) + log = None + + use_tree = (options.source == "tree" or \ + (options.source == "any" and tree)) + use_arch = (options.source == "archive" or options.source == "any") + + log = None + if use_tree: + for log in tree.iter_logs(revision.get_version()): + if log.revision == revision: + break + else: + log = None + if log is None and use_arch: + cmdutil.ensure_revision_exists(revision) + log = arch.Patchlog(revision) + if log is not None: + for item in log.items(): + print "%s: %s" % item + print log.description + + def get_parser(self): + """ + Returns the options parser to use for the "cat-log" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai cat-log [revision]") + parser.add_option("--archive", action="store_const", dest="source", + const="archive", default="any", + help="Always get the log from the archive") + parser.add_option("--tree", action="store_const", dest="source", + const="tree", help="Always get the log from the tree") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Prints the log for the specified revision + """ + help_tree_spec() + return + +class Revert(BaseCommand): + """ Reverts a tree (or aspects of it) to a revision + """ + def __init__(self): + self.description="Reverts a tree (or aspects of it) to a revision " + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return iter_modified_file_completions(tree, arg) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revert" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: + tree = arch.tree_root() + except arch.errors.TreeRootError, e: + raise CommandFailed(e) + spec=None + if options.revision is not None: + spec=options.revision + try: + if spec is not None: + revision = cmdutil.determine_revision_tree(tree, spec) + else: + revision = ancillary.comp_revision(tree) + except cmdutil.CantDetermineRevision, e: + raise CommandFailedWrapper(e) + munger = None + + if options.file_contents or options.file_perms or options.deletions\ + or options.additions or options.renames or options.hunk_prompt: + munger = arch_compound.MungeOpts() + munger.set_hunk_prompt(cmdutil.colorize, cmdutil.user_hunk_confirm, + options.hunk_prompt) + + if len(args) > 0 or options.logs or options.pattern_files or \ + options.control: + if munger is None: + munger = cmdutil.arch_compound.MungeOpts(True) + munger.all_types(True) + if len(args) > 0: + t_cwd = arch_compound.tree_cwd(tree) + for name in args: + if len(t_cwd) > 0: + t_cwd += "/" + name = "./" + t_cwd + name + munger.add_keep_file(name); + + if options.file_perms: + munger.file_perms = True + if options.file_contents: + munger.file_contents = True + if options.deletions: + munger.deletions = True + if options.additions: + munger.additions = True + if options.renames: + munger.renames = True + if options.logs: + munger.add_keep_pattern('^\./\{arch\}/[^=].*') + if options.control: + munger.add_keep_pattern("/\.arch-ids|^\./\{arch\}|"\ + "/\.arch-inventory$") + if options.pattern_files: + munger.add_keep_pattern(options.pattern_files) + + for line in arch_compound.revert(tree, revision, munger, + not options.no_output): + cmdutil.colorize(line) + + + def get_parser(self): + """ + Returns the options parser to use for the "cat-log" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai revert [options] [FILE...]") + parser.add_option("", "--contents", action="store_true", + dest="file_contents", + help="Revert file content changes") + parser.add_option("", "--permissions", action="store_true", + dest="file_perms", + help="Revert file permissions changes") + parser.add_option("", "--deletions", action="store_true", + dest="deletions", + help="Restore deleted files") + parser.add_option("", "--additions", action="store_true", + dest="additions", + help="Remove added files") + parser.add_option("", "--renames", action="store_true", + dest="renames", + help="Revert file names") + parser.add_option("--hunks", action="store_true", + dest="hunk_prompt", default=False, + help="Prompt which hunks to revert") + parser.add_option("--pattern-files", dest="pattern_files", + help="Revert files that match this pattern", + metavar="REGEX") + parser.add_option("--logs", action="store_true", + dest="logs", default=False, + help="Revert only logs") + parser.add_option("--control-files", action="store_true", + dest="control", default=False, + help="Revert logs and other control files") + parser.add_option("-n", "--no-output", action="store_true", + dest="no_output", + help="Don't keep an undo changeset") + parser.add_option("--revision", dest="revision", + help="Revert to the specified revision", + metavar="REVISION") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Reverts changes in the current working tree. If no flags are specified, all +types of changes are reverted. Otherwise, only selected types of changes are +reverted. + +If a revision is specified on the commandline, differences between the current +tree and that revision are reverted. If a version is specified, the current +tree is used to determine the revision. + +If files are specified, only those files listed will have any changes applied. +To specify a renamed file, you can use either the old or new name. (or both!) + +Unless "-n" is specified, reversions can be undone with "redo". + """ + return + +class Revision(BaseCommand): + """ + Print a revision name based on a revision specifier + """ + def __init__(self): + self.description="Prints the name of a revision" + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + tree = None + + spec=None + if len(args) > 0: + spec=args[0] + if len(args) > 1: + raise cmdutil.GetHelp + try: + if tree: + revision = cmdutil.determine_revision_tree(tree, spec) + else: + revision = cmdutil.determine_revision_arch(tree, spec) + except cmdutil.CantDetermineRevision, e: + print str(e) + return + print options.display(revision) + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai revision [revision]") + parser.add_option("", "--location", action="store_const", + const=paths.determine_path, dest="display", + help="Show location instead of name", default=str) + parser.add_option("--import", action="store_const", + const=paths.determine_import_path, dest="display", + help="Show location of import file") + parser.add_option("--log", action="store_const", + const=paths.determine_log_path, dest="display", + help="Show location of log file") + parser.add_option("--patch", action="store_const", + dest="display", const=paths.determine_patch_path, + help="Show location of patchfile") + parser.add_option("--continuation", action="store_const", + const=paths.determine_continuation_path, + dest="display", + help="Show location of continuation file") + parser.add_option("--cacherev", action="store_const", + const=paths.determine_cacherev_path, dest="display", + help="Show location of cacherev file") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Expands aliases and prints the name of the specified revision. Instead of +the name, several options can be used to print locations. If more than one is +specified, the last one is used. + """ + help_tree_spec() + return + +class Revisions(BaseCommand): + """ + Print a revision name based on a revision specifier + """ + def __init__(self): + self.description="Lists revisions" + self.cl_revisions = [] + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + (options, args) = self.get_parser().parse_args(cmdargs) + if len(args) > 1: + raise cmdutil.GetHelp + try: + self.tree = arch.tree_root() + except arch.errors.TreeRootError: + self.tree = None + if options.type == "default": + options.type = "archive" + try: + iter = cmdutil.revision_iterator(self.tree, options.type, args, + options.reverse, options.modified, + options.shallow) + except cmdutil.CantDetermineRevision, e: + raise CommandFailedWrapper(e) + except cmdutil.CantDetermineVersion, e: + raise CommandFailedWrapper(e) + if options.skip is not None: + iter = cmdutil.iter_skip(iter, int(options.skip)) + + try: + for revision in iter: + log = None + if isinstance(revision, arch.Patchlog): + log = revision + revision=revision.revision + out = options.display(revision) + if out is not None: + print out + if log is None and (options.summary or options.creator or + options.date or options.merges): + log = revision.patchlog + if options.creator: + print " %s" % log.creator + if options.date: + print " %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date) + if options.summary: + print " %s" % log.summary + if options.merges: + showed_title = False + for revision in log.merged_patches: + if not showed_title: + print " Merged:" + showed_title = True + print " %s" % revision + if len(self.cl_revisions) > 0: + print pylon.changelog_for_merge(self.cl_revisions) + except pylon.errors.TreeRootNone: + raise CommandFailedWrapper( + Exception("This option can only be used in a project tree.")) + + def changelog_append(self, revision): + if isinstance(revision, arch.Revision): + revision=arch.Patchlog(revision) + self.cl_revisions.append(revision) + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai revisions [version/revision]") + select = cmdutil.OptionGroup(parser, "Selection options", + "Control which revisions are listed. These options" + " are mutually exclusive. If more than one is" + " specified, the last is used.") + + cmdutil.add_revision_iter_options(select) + parser.add_option("", "--skip", dest="skip", + help="Skip revisions. Positive numbers skip from " + "beginning, negative skip from end.", + metavar="NUMBER") + + parser.add_option_group(select) + + format = cmdutil.OptionGroup(parser, "Revision format options", + "These control the appearance of listed revisions") + format.add_option("", "--location", action="store_const", + const=paths.determine_path, dest="display", + help="Show location instead of name", default=str) + format.add_option("--import", action="store_const", + const=paths.determine_import_path, dest="display", + help="Show location of import file") + format.add_option("--log", action="store_const", + const=paths.determine_log_path, dest="display", + help="Show location of log file") + format.add_option("--patch", action="store_const", + dest="display", const=paths.determine_patch_path, + help="Show location of patchfile") + format.add_option("--continuation", action="store_const", + const=paths.determine_continuation_path, + dest="display", + help="Show location of continuation file") + format.add_option("--cacherev", action="store_const", + const=paths.determine_cacherev_path, dest="display", + help="Show location of cacherev file") + format.add_option("--changelog", action="store_const", + const=self.changelog_append, dest="display", + help="Show location of cacherev file") + parser.add_option_group(format) + display = cmdutil.OptionGroup(parser, "Display format options", + "These control the display of data") + display.add_option("-r", "--reverse", action="store_true", + dest="reverse", help="Sort from newest to oldest") + display.add_option("-s", "--summary", action="store_true", + dest="summary", help="Show patchlog summary") + display.add_option("-D", "--date", action="store_true", + dest="date", help="Show patchlog date") + display.add_option("-c", "--creator", action="store_true", + dest="creator", help="Show the id that committed the" + " revision") + display.add_option("-m", "--merges", action="store_true", + dest="merges", help="Show the revisions that were" + " merged") + parser.add_option_group(display) + return parser + def help(self, parser=None): + """Attempt to explain the revisions command + + :param parser: If supplied, used to determine options + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """List revisions. + """ + help_tree_spec() + + +class Get(BaseCommand): + """ + Retrieve a revision from the archive + """ + def __init__(self): + self.description="Retrieve a revision from the archive" + self.parser=self.get_parser() + + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + + def do_command(self, cmdargs): + """ + Master function that perfoms the "get" command. + """ + (options, args) = self.parser.parse_args(cmdargs) + if len(args) < 1: + return self.help() + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + tree = None + + arch_loc = None + try: + revision, arch_loc = paths.full_path_decode(args[0]) + except Exception, e: + revision = cmdutil.determine_revision_arch(tree, args[0], + check_existence=False, allow_package=True) + if len(args) > 1: + directory = args[1] + else: + directory = str(revision.nonarch) + if os.path.exists(directory): + raise DirectoryExists(directory) + cmdutil.ensure_archive_registered(revision.archive, arch_loc) + try: + cmdutil.ensure_revision_exists(revision) + except cmdutil.NoSuchRevision, e: + raise CommandFailedWrapper(e) + + link = cmdutil.prompt ("get link") + for line in cmdutil.iter_get(revision, directory, link, + options.no_pristine, + options.no_greedy_add): + cmdutil.colorize(line) + + def get_parser(self): + """ + Returns the options parser to use for the "get" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai get revision [dir]") + parser.add_option("--no-pristine", action="store_true", + dest="no_pristine", + help="Do not make pristine copy for reference") + parser.add_option("--no-greedy-add", action="store_true", + dest="no_greedy_add", + help="Never add to greedy libraries") + + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Expands aliases and constructs a project tree for a revision. If the optional +"dir" argument is provided, the project tree will be stored in this directory. + """ + help_tree_spec() + return + +class PromptCmd(cmd.Cmd): + def __init__(self): + cmd.Cmd.__init__(self) + self.prompt = "Fai> " + try: + self.tree = arch.tree_root() + except: + self.tree = None + self.set_title() + self.set_prompt() + self.fake_aba = abacmds.AbaCmds() + self.identchars += '-' + self.history_file = os.path.expanduser("~/.fai-history") + readline.set_completer_delims(string.whitespace) + if os.access(self.history_file, os.R_OK) and \ + os.path.isfile(self.history_file): + readline.read_history_file(self.history_file) + self.cwd = os.getcwd() + + def write_history(self): + readline.write_history_file(self.history_file) + + def do_quit(self, args): + self.write_history() + sys.exit(0) + + def do_exit(self, args): + self.do_quit(args) + + def do_EOF(self, args): + print + self.do_quit(args) + + def postcmd(self, line, bar): + self.set_title() + self.set_prompt() + + def set_prompt(self): + if self.tree is not None: + try: + prompt = pylon.alias_or_version(self.tree.tree_version, + self.tree, + full=False) + if prompt is not None: + prompt = " " + prompt + except: + prompt = "" + else: + prompt = "" + self.prompt = "Fai%s> " % prompt + + def set_title(self, command=None): + try: + version = pylon.alias_or_version(self.tree.tree_version, self.tree, + full=False) + except: + version = "[no version]" + if command is None: + command = "" + sys.stdout.write(terminal.term_title("Fai %s %s" % (command, version))) + + def do_cd(self, line): + if line == "": + line = "~" + line = os.path.expanduser(line) + if os.path.isabs(line): + newcwd = line + else: + newcwd = self.cwd+'/'+line + newcwd = os.path.normpath(newcwd) + try: + os.chdir(newcwd) + self.cwd = newcwd + except Exception, e: + print e + try: + self.tree = arch.tree_root() + except: + self.tree = None + + def do_help(self, line): + Help()(line) + + def default(self, line): + args = line.split() + if find_command(args[0]): + try: + find_command(args[0]).do_command(args[1:]) + except cmdutil.BadCommandOption, e: + print e + except cmdutil.GetHelp, e: + find_command(args[0]).help() + except CommandFailed, e: + print e + except arch.errors.ArchiveNotRegistered, e: + print e + except KeyboardInterrupt, e: + print "Interrupted" + except arch.util.ExecProblem, e: + print e.proc.error.rstrip('\n') + except cmdutil.CantDetermineVersion, e: + print e + except cmdutil.CantDetermineRevision, e: + print e + except Exception, e: + print "Unhandled error:\n%s" % errors.exception_str(e) + + elif suggestions.has_key(args[0]): + print suggestions[args[0]] + + elif self.fake_aba.is_command(args[0]): + tree = None + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + pass + cmd = self.fake_aba.is_command(args[0]) + try: + cmd.run(cmdutil.expand_prefix_alias(args[1:], tree)) + except KeyboardInterrupt, e: + print "Interrupted" + + elif options.tla_fallthrough and args[0] != "rm" and \ + cmdutil.is_tla_command(args[0]): + try: + tree = None + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + pass + args = cmdutil.expand_prefix_alias(args, tree) + arch.util.exec_safe('tla', args, stderr=sys.stderr, + expected=(0, 1)) + except arch.util.ExecProblem, e: + pass + except KeyboardInterrupt, e: + print "Interrupted" + else: + try: + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + tree = None + args=line.split() + os.system(" ".join(cmdutil.expand_prefix_alias(args, tree))) + except KeyboardInterrupt, e: + print "Interrupted" + + def completenames(self, text, line, begidx, endidx): + completions = [] + iter = iter_command_names(self.fake_aba) + try: + if len(line) > 0: + arg = line.split()[-1] + else: + arg = "" + iter = cmdutil.iter_munged_completions(iter, arg, text) + except Exception, e: + print e + return list(iter) + + def completedefault(self, text, line, begidx, endidx): + """Perform completion for native commands. + + :param text: The text to complete + :type text: str + :param line: The entire line to complete + :type line: str + :param begidx: The start of the text in the line + :type begidx: int + :param endidx: The end of the text in the line + :type endidx: int + """ + try: + (cmd, args, foo) = self.parseline(line) + command_obj=find_command(cmd) + if command_obj is not None: + return command_obj.complete(args.split(), text) + elif not self.fake_aba.is_command(cmd) and \ + cmdutil.is_tla_command(cmd): + iter = cmdutil.iter_supported_switches(cmd) + if len(args) > 0: + arg = args.split()[-1] + else: + arg = "" + if arg.startswith("-"): + return list(cmdutil.iter_munged_completions(iter, arg, + text)) + else: + return list(cmdutil.iter_munged_completions( + cmdutil.iter_file_completions(arg), arg, text)) + + + elif cmd == "cd": + if len(args) > 0: + arg = args.split()[-1] + else: + arg = "" + iter = cmdutil.iter_dir_completions(arg) + iter = cmdutil.iter_munged_completions(iter, arg, text) + return list(iter) + elif len(args)>0: + arg = args.split()[-1] + iter = cmdutil.iter_file_completions(arg) + return list(cmdutil.iter_munged_completions(iter, arg, text)) + else: + return self.completenames(text, line, begidx, endidx) + except Exception, e: + print e + + +def iter_command_names(fake_aba): + for entry in cmdutil.iter_combine([commands.iterkeys(), + fake_aba.get_commands(), + cmdutil.iter_tla_commands(False)]): + if not suggestions.has_key(str(entry)): + yield entry + + +def iter_source_file_completions(tree, arg): + treepath = arch_compound.tree_cwd(tree) + if len(treepath) > 0: + dirs = [treepath] + else: + dirs = None + for file in tree.iter_inventory(dirs, source=True, both=True): + file = file_completion_match(file, treepath, arg) + if file is not None: + yield file + + +def iter_untagged(tree, dirs): + for file in arch_core.iter_inventory_filter(tree, dirs, tagged=False, + categories=arch_core.non_root, + control_files=True): + yield file.name + + +def iter_untagged_completions(tree, arg): + """Generate an iterator for all visible untagged files that match arg. + + :param tree: The tree to look for untagged files in + :type tree: `arch.WorkingTree` + :param arg: The argument to match + :type arg: str + :return: An iterator of all matching untagged files + :rtype: iterator of str + """ + treepath = arch_compound.tree_cwd(tree) + if len(treepath) > 0: + dirs = [treepath] + else: + dirs = None + + for file in iter_untagged(tree, dirs): + file = file_completion_match(file, treepath, arg) + if file is not None: + yield file + + +def file_completion_match(file, treepath, arg): + """Determines whether a file within an arch tree matches the argument. + + :param file: The rooted filename + :type file: str + :param treepath: The path to the cwd within the tree + :type treepath: str + :param arg: The prefix to match + :return: The completion name, or None if not a match + :rtype: str + """ + if not file.startswith(treepath): + return None + if treepath != "": + file = file[len(treepath)+1:] + + if not file.startswith(arg): + return None + if os.path.isdir(file): + file += '/' + return file + +def iter_modified_file_completions(tree, arg): + """Returns a list of modified files that match the specified prefix. + + :param tree: The current tree + :type tree: `arch.WorkingTree` + :param arg: The prefix to match + :type arg: str + """ + treepath = arch_compound.tree_cwd(tree) + tmpdir = util.tmpdir() + changeset = tmpdir+"/changeset" + completions = [] + revision = cmdutil.determine_revision_tree(tree) + for line in arch.iter_delta(revision, tree, changeset): + if isinstance(line, arch.FileModification): + file = file_completion_match(line.name[1:], treepath, arg) + if file is not None: + completions.append(file) + shutil.rmtree(tmpdir) + return completions + +class Shell(BaseCommand): + def __init__(self): + self.description = "Runs Fai as a shell" + + def do_command(self, cmdargs): + if len(cmdargs)!=0: + raise cmdutil.GetHelp + prompt = PromptCmd() + try: + prompt.cmdloop() + finally: + prompt.write_history() + +class AddID(BaseCommand): + """ + Adds an inventory id for the given file + """ + def __init__(self): + self.description="Add an inventory id for a given file" + + def get_completer(self, arg, index): + tree = arch.tree_root() + return iter_untagged_completions(tree, arg) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + + try: + tree = arch.tree_root() + except arch.errors.TreeRootError, e: + raise pylon.errors.CommandFailedWrapper(e) + + + if (len(args) == 0) == (options.untagged == False): + raise cmdutil.GetHelp + + #if options.id and len(args) != 1: + # print "If --id is specified, only one file can be named." + # return + + method = tree.tagging_method + + if options.id_type == "tagline": + if method != "tagline": + if not cmdutil.prompt("Tagline in other tree"): + if method == "explicit" or method == "implicit": + options.id_type == method + else: + print "add-id not supported for \"%s\" tagging method"\ + % method + return + + elif options.id_type == "implicit": + if method != "implicit": + if not cmdutil.prompt("Implicit in other tree"): + if method == "explicit" or method == "tagline": + options.id_type == method + else: + print "add-id not supported for \"%s\" tagging method"\ + % method + return + elif options.id_type == "explicit": + if method != "tagline" and method != explicit: + if not prompt("Explicit in other tree"): + print "add-id not supported for \"%s\" tagging method" % \ + method + return + + if options.id_type == "auto": + if method != "tagline" and method != "explicit" \ + and method !="implicit": + print "add-id not supported for \"%s\" tagging method" % method + return + else: + options.id_type = method + if options.untagged: + args = None + self.add_ids(tree, options.id_type, args) + + def add_ids(self, tree, id_type, files=()): + """Add inventory ids to files. + + :param tree: the tree the files are in + :type tree: `arch.WorkingTree` + :param id_type: the type of id to add: "explicit" or "tagline" + :type id_type: str + :param files: The list of files to add. If None do all untagged. + :type files: tuple of str + """ + + untagged = (files is None) + if untagged: + files = list(iter_untagged(tree, None)) + previous_files = [] + while len(files) > 0: + previous_files.extend(files) + if id_type == "explicit": + cmdutil.add_id(files) + elif id_type == "tagline" or id_type == "implicit": + for file in files: + try: + implicit = (id_type == "implicit") + cmdutil.add_tagline_or_explicit_id(file, False, + implicit) + except cmdutil.AlreadyTagged: + print "\"%s\" already has a tagline." % file + except cmdutil.NoCommentSyntax: + pass + #do inventory after tagging until no untagged files are encountered + if untagged: + files = [] + for file in iter_untagged(tree, None): + if not file in previous_files: + files.append(file) + + else: + break + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai add-id file1 [file2] [file3]...") +# ddaa suggests removing this to promote GUIDs. Let's see who squalks. +# parser.add_option("-i", "--id", dest="id", +# help="Specify id for a single file", default=None) + parser.add_option("--tltl", action="store_true", + dest="lord_style", help="Use Tom Lord's style of id.") + parser.add_option("--explicit", action="store_const", + const="explicit", dest="id_type", + help="Use an explicit id", default="auto") + parser.add_option("--tagline", action="store_const", + const="tagline", dest="id_type", + help="Use a tagline id") + parser.add_option("--implicit", action="store_const", + const="implicit", dest="id_type", + help="Use an implicit id (deprecated)") + parser.add_option("--untagged", action="store_true", + dest="untagged", default=False, + help="tag all untagged files") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Adds an inventory to the specified file(s) and directories. If --untagged is +specified, adds inventory to all untagged files and directories. + """ + return + + +class Merge(BaseCommand): + """ + Merges changes from other versions into the current tree + """ + def __init__(self): + self.description="Merges changes from other versions" + try: + self.tree = arch.tree_root() + except: + self.tree = None + + + def get_completer(self, arg, index): + if self.tree is None: + raise arch.errors.TreeRootError + return cmdutil.merge_completions(self.tree, arg, index) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "merge" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + if options.diff3: + action="star-merge" + else: + action = options.action + + if self.tree is None: + raise arch.errors.TreeRootError(os.getcwd()) + if cmdutil.has_changed(ancillary.comp_revision(self.tree)): + raise UncommittedChanges(self.tree) + + if len(args) > 0: + revisions = [] + for arg in args: + revisions.append(cmdutil.determine_revision_arch(self.tree, + arg)) + source = "from commandline" + else: + revisions = ancillary.iter_partner_revisions(self.tree, + self.tree.tree_version) + source = "from partner version" + revisions = misc.rewind_iterator(revisions) + try: + revisions.next() + revisions.rewind() + except StopIteration, e: + revision = cmdutil.tag_cur(self.tree) + if revision is None: + raise CantDetermineRevision("", "No version specified, no " + "partner-versions, and no tag" + " source") + revisions = [revision] + source = "from tag source" + for revision in revisions: + cmdutil.ensure_archive_registered(revision.archive) + cmdutil.colorize(arch.Chatter("* Merging %s [%s]" % + (revision, source))) + if action=="native-merge" or action=="update": + if self.native_merge(revision, action) == 0: + continue + elif action=="star-merge": + try: + self.star_merge(revision, options.diff3) + except errors.MergeProblem, e: + break + if cmdutil.has_changed(self.tree.tree_version): + break + + def star_merge(self, revision, diff3): + """Perform a star-merge on the current tree. + + :param revision: The revision to use for the merge + :type revision: `arch.Revision` + :param diff3: If true, do a diff3 merge + :type diff3: bool + """ + try: + for line in self.tree.iter_star_merge(revision, diff3=diff3): + cmdutil.colorize(line) + except arch.util.ExecProblem, e: + if e.proc.status is not None and e.proc.status == 1: + if e.proc.error: + print e.proc.error + raise MergeProblem + else: + raise + + def native_merge(self, other_revision, action): + """Perform a native-merge on the current tree. + + :param other_revision: The revision to use for the merge + :type other_revision: `arch.Revision` + :return: 0 if the merge was skipped, 1 if it was applied + """ + other_tree = arch_compound.find_or_make_local_revision(other_revision) + try: + if action == "native-merge": + ancestor = arch_compound.merge_ancestor2(self.tree, other_tree, + other_revision) + elif action == "update": + ancestor = arch_compound.tree_latest(self.tree, + other_revision.version) + except CantDetermineRevision, e: + raise CommandFailedWrapper(e) + cmdutil.colorize(arch.Chatter("* Found common ancestor %s" % ancestor)) + if (ancestor == other_revision): + cmdutil.colorize(arch.Chatter("* Skipping redundant merge" + % ancestor)) + return 0 + delta = cmdutil.apply_delta(ancestor, other_tree, self.tree) + for line in cmdutil.iter_apply_delta_filter(delta): + cmdutil.colorize(line) + return 1 + + + + def get_parser(self): + """ + Returns the options parser to use for the "merge" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai merge [VERSION]") + parser.add_option("-s", "--star-merge", action="store_const", + dest="action", help="Use star-merge", + const="star-merge", default="native-merge") + parser.add_option("--update", action="store_const", + dest="action", help="Use update picker", + const="update") + parser.add_option("--diff3", action="store_true", + dest="diff3", + help="Use diff3 for merge (implies star-merge)") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Performs a merge operation using the specified version. + """ + return + +class ELog(BaseCommand): + """ + Produces a raw patchlog and invokes the user's editor + """ + def __init__(self): + self.description="Edit a patchlog to commit" + try: + self.tree = arch.tree_root() + except: + self.tree = None + + + def do_command(self, cmdargs): + """ + Master function that perfoms the "elog" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + if self.tree is None: + raise arch.errors.TreeRootError + + try: + edit_log(self.tree, self.tree.tree_version) + except pylon.errors.NoEditorSpecified, e: + raise pylon.errors.CommandFailedWrapper(e) + + def get_parser(self): + """ + Returns the options parser to use for the "merge" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai elog") + return parser + + + def help(self, parser=None): + """ + Invokes $EDITOR to produce a log for committing. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Invokes $EDITOR to produce a log for committing. + """ + return + +def edit_log(tree, version): + """Makes and edits the log for a tree. Does all kinds of fancy things + like log templates and merge summaries and log-for-merge + + :param tree: The tree to edit the log for + :type tree: `arch.WorkingTree` + """ + #ensure we have an editor before preparing the log + cmdutil.find_editor() + log = tree.log_message(create=False, version=version) + log_is_new = False + if log is None or cmdutil.prompt("Overwrite log"): + if log is not None: + os.remove(log.name) + log = tree.log_message(create=True, version=version) + log_is_new = True + tmplog = log.name + template = pylon.log_template_path(tree) + if template: + shutil.copyfile(template, tmplog) + comp_version = ancillary.comp_revision(tree).version + new_merges = cmdutil.iter_new_merges(tree, comp_version) + new_merges = cmdutil.direct_merges(new_merges) + log["Summary"] = pylon.merge_summary(new_merges, + version) + if len(new_merges) > 0: + if cmdutil.prompt("Log for merge"): + if cmdutil.prompt("changelog for merge"): + mergestuff = "Patches applied:\n" + mergestuff += pylon.changelog_for_merge(new_merges) + else: + mergestuff = cmdutil.log_for_merge(tree, comp_version) + log.description += mergestuff + log.save() + try: + cmdutil.invoke_editor(log.name) + except: + if log_is_new: + os.remove(log.name) + raise + + +class MirrorArchive(BaseCommand): + """ + Updates a mirror from an archive + """ + def __init__(self): + self.description="Update a mirror from an archive" + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + if len(args) > 1: + raise GetHelp + try: + tree = arch.tree_root() + except: + tree = None + + if len(args) == 0: + if tree is not None: + name = tree.tree_version() + else: + name = cmdutil.expand_alias(args[0], tree) + name = arch.NameParser(name) + + to_arch = name.get_archive() + from_arch = cmdutil.get_mirror_source(arch.Archive(to_arch)) + limit = name.get_nonarch() + + iter = arch_core.mirror_archive(from_arch,to_arch, limit) + for line in arch.chatter_classifier(iter): + cmdutil.colorize(line) + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai mirror-archive ARCHIVE") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Updates a mirror from an archive. If a branch, package, or version is +supplied, only changes under it are mirrored. + """ + return + +def help_tree_spec(): + print """Specifying revisions (default: tree) +Revisions may be specified by alias, revision, version or patchlevel. +Revisions or versions may be fully qualified. Unqualified revisions, versions, +or patchlevels use the archive of the current project tree. Versions will +use the latest patchlevel in the tree. Patchlevels will use the current tree- +version. + +Use "alias" to list available (user and automatic) aliases.""" + +auto_alias = [ +"acur", +"The latest revision in the archive of the tree-version. You can specify \ +a different version like so: acur:foo--bar--0 (aliases can be used)", +"tcur", +"""(tree current) The latest revision in the tree of the tree-version. \ +You can specify a different version like so: tcur:foo--bar--0 (aliases can be \ +used).""", +"tprev" , +"""(tree previous) The previous revision in the tree of the tree-version. To \ +specify an older revision, use a number, e.g. "tprev:4" """, +"tanc" , +"""(tree ancestor) The ancestor revision of the tree To specify an older \ +revision, use a number, e.g. "tanc:4".""", +"tdate" , +"""(tree date) The latest revision from a given date, e.g. "tdate:July 6".""", +"tmod" , +""" (tree modified) The latest revision to modify a given file, e.g. \ +"tmod:engine.cpp" or "tmod:engine.cpp:16".""", +"ttag" , +"""(tree tag) The revision that was tagged into the current tree revision, \ +according to the tree""", +"tagcur", +"""(tag current) The latest revision of the version that the current tree \ +was tagged from.""", +"mergeanc" , +"""The common ancestor of the current tree and the specified revision. \ +Defaults to the first partner-version's latest revision or to tagcur.""", +] + + +def is_auto_alias(name): + """Determine whether a name is an auto alias name + + :param name: the name to check + :type name: str + :return: True if the name is an auto alias, false if not + :rtype: bool + """ + return name in [f for (f, v) in pylon.util.iter_pairs(auto_alias)] + + +def display_def(iter, wrap = 80): + """Display a list of definitions + + :param iter: iter of name, definition pairs + :type iter: iter of (str, str) + :param wrap: The width for text wrapping + :type wrap: int + """ + vals = list(iter) + maxlen = 0 + for (key, value) in vals: + if len(key) > maxlen: + maxlen = len(key) + for (key, value) in vals: + tw=textwrap.TextWrapper(width=wrap, + initial_indent=key.rjust(maxlen)+" : ", + subsequent_indent="".rjust(maxlen+3)) + print tw.fill(value) + + +def help_aliases(tree): + print """Auto-generated aliases""" + display_def(pylon.util.iter_pairs(auto_alias)) + print "User aliases" + display_def(ancillary.iter_all_alias(tree)) + +class Inventory(BaseCommand): + """List the status of files in the tree""" + def __init__(self): + self.description=self.__doc__ + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + tree = arch.tree_root() + categories = [] + + if (options.source): + categories.append(arch_core.SourceFile) + if (options.precious): + categories.append(arch_core.PreciousFile) + if (options.backup): + categories.append(arch_core.BackupFile) + if (options.junk): + categories.append(arch_core.JunkFile) + + if len(categories) == 1: + show_leading = False + else: + show_leading = True + + if len(categories) == 0: + categories = None + + if options.untagged: + categories = arch_core.non_root + show_leading = False + tagged = False + else: + tagged = None + + for file in arch_core.iter_inventory_filter(tree, None, + control_files=options.control_files, + categories = categories, tagged=tagged): + print arch_core.file_line(file, + category = show_leading, + untagged = show_leading, + id = options.ids) + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai inventory [options]") + parser.add_option("--ids", action="store_true", dest="ids", + help="Show file ids") + parser.add_option("--control", action="store_true", + dest="control_files", help="include control files") + parser.add_option("--source", action="store_true", dest="source", + help="List source files") + parser.add_option("--backup", action="store_true", dest="backup", + help="List backup files") + parser.add_option("--precious", action="store_true", dest="precious", + help="List precious files") + parser.add_option("--junk", action="store_true", dest="junk", + help="List junk files") + parser.add_option("--unrecognized", action="store_true", + dest="unrecognized", help="List unrecognized files") + parser.add_option("--untagged", action="store_true", + dest="untagged", help="List only untagged files") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Lists the status of files in the archive: +S source +P precious +B backup +J junk +U unrecognized +T tree root +? untagged-source +Leading letter are not displayed if only one kind of file is shown + """ + return + + +class Alias(BaseCommand): + """List or adjust aliases""" + def __init__(self): + self.description=self.__doc__ + + def get_completer(self, arg, index): + if index > 2: + return () + try: + self.tree = arch.tree_root() + except: + self.tree = None + + if index == 0: + return [part[0]+" " for part in ancillary.iter_all_alias(self.tree)] + elif index == 1: + return cmdutil.iter_revision_completions(arg, self.tree) + + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: + self.tree = arch.tree_root() + except: + self.tree = None + + + try: + options.action(args, options) + except cmdutil.ForbiddenAliasSyntax, e: + raise CommandFailedWrapper(e) + + def no_prefix(self, alias): + if alias.startswith("^"): + alias = alias[1:] + return alias + + def arg_dispatch(self, args, options): + """Add, modify, or list aliases, depending on number of arguments + + :param args: The list of commandline arguments + :type args: list of str + :param options: The commandline options + """ + if len(args) == 0: + help_aliases(self.tree) + return + else: + alias = self.no_prefix(args[0]) + if len(args) == 1: + self.print_alias(alias) + elif (len(args)) == 2: + self.add(alias, args[1], options) + else: + raise cmdutil.GetHelp + + def print_alias(self, alias): + answer = None + if is_auto_alias(alias): + raise pylon.errors.IsAutoAlias(alias, "\"%s\" is an auto alias." + " Use \"revision\" to expand auto aliases." % alias) + for pair in ancillary.iter_all_alias(self.tree): + if pair[0] == alias: + answer = pair[1] + if answer is not None: + print answer + else: + print "The alias %s is not assigned." % alias + + def add(self, alias, expansion, options): + """Add or modify aliases + + :param alias: The alias name to create/modify + :type alias: str + :param expansion: The expansion to assign to the alias name + :type expansion: str + :param options: The commandline options + """ + if is_auto_alias(alias): + raise IsAutoAlias(alias) + newlist = "" + written = False + new_line = "%s=%s\n" % (alias, cmdutil.expand_alias(expansion, + self.tree)) + ancillary.check_alias(new_line.rstrip("\n"), [alias, expansion]) + + for pair in self.get_iterator(options): + if pair[0] != alias: + newlist+="%s=%s\n" % (pair[0], pair[1]) + elif not written: + newlist+=new_line + written = True + if not written: + newlist+=new_line + self.write_aliases(newlist, options) + + def delete(self, args, options): + """Delete the specified alias + + :param args: The list of arguments + :type args: list of str + :param options: The commandline options + """ + deleted = False + if len(args) != 1: + raise cmdutil.GetHelp + alias = self.no_prefix(args[0]) + if is_auto_alias(alias): + raise IsAutoAlias(alias) + newlist = "" + for pair in self.get_iterator(options): + if pair[0] != alias: + newlist+="%s=%s\n" % (pair[0], pair[1]) + else: + deleted = True + if not deleted: + raise errors.NoSuchAlias(alias) + self.write_aliases(newlist, options) + + def get_alias_file(self, options): + """Return the name of the alias file to use + + :param options: The commandline options + """ + if options.tree: + if self.tree is None: + self.tree == arch.tree_root() + return str(self.tree)+"/{arch}/+aliases" + else: + return "~/.aba/aliases" + + def get_iterator(self, options): + """Return the alias iterator to use + + :param options: The commandline options + """ + return ancillary.iter_alias(self.get_alias_file(options)) + + def write_aliases(self, newlist, options): + """Safely rewrite the alias file + :param newlist: The new list of aliases + :type newlist: str + :param options: The commandline options + """ + filename = os.path.expanduser(self.get_alias_file(options)) + file = util.NewFileVersion(filename) + file.write(newlist) + file.commit() + + + def get_parser(self): + """ + Returns the options parser to use for the "alias" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai alias [ALIAS] [NAME]") + parser.add_option("-d", "--delete", action="store_const", dest="action", + const=self.delete, default=self.arg_dispatch, + help="Delete an alias") + parser.add_option("--tree", action="store_true", dest="tree", + help="Create a per-tree alias", default=False) + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Lists current aliases or modifies the list of aliases. + +If no arguments are supplied, aliases will be listed. If two arguments are +supplied, the specified alias will be created or modified. If -d or --delete +is supplied, the specified alias will be deleted. + +You can create aliases that refer to any fully-qualified part of the +Arch namespace, e.g. +archive, +archive/category, +archive/category--branch, +archive/category--branch--version (my favourite) +archive/category--branch--version--patchlevel + +Aliases can be used automatically by native commands. To use them +with external or tla commands, prefix them with ^ (you can do this +with native commands, too). +""" + + +class RequestMerge(BaseCommand): + """Submit a merge request to Bug Goo""" + def __init__(self): + self.description=self.__doc__ + + def do_command(self, cmdargs): + """Submit a merge request + + :param cmdargs: The commandline arguments + :type cmdargs: list of str + """ + parser = self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: + cmdutil.find_editor() + except pylon.errors.NoEditorSpecified, e: + raise pylon.errors.CommandFailedWrapper(e) + try: + self.tree=arch.tree_root() + except: + self.tree=None + base, revisions = self.revision_specs(args) + message = self.make_headers(base, revisions) + message += self.make_summary(revisions) + path = self.edit_message(message) + message = self.tidy_message(path) + if cmdutil.prompt("Send merge"): + self.send_message(message) + print "Merge request sent" + + def make_headers(self, base, revisions): + """Produce email and Bug Goo header strings + + :param base: The base revision to apply merges to + :type base: `arch.Revision` + :param revisions: The revisions to replay into the base + :type revisions: list of `arch.Patchlog` + :return: The headers + :rtype: str + """ + headers = "To: gnu-arch-users@gnu.org\n" + headers += "From: %s\n" % options.fromaddr + if len(revisions) == 1: + headers += "Subject: [MERGE REQUEST] %s\n" % revisions[0].summary + else: + headers += "Subject: [MERGE REQUEST]\n" + headers += "\n" + headers += "Base-Revision: %s\n" % base + for revision in revisions: + headers += "Revision: %s\n" % revision.revision + headers += "Bug: \n\n" + return headers + + def make_summary(self, logs): + """Generate a summary of merges + + :param logs: the patchlogs that were directly added by the merges + :type logs: list of `arch.Patchlog` + :return: the summary + :rtype: str + """ + summary = "" + for log in logs: + summary+=str(log.revision)+"\n" + summary+=log.summary+"\n" + if log.description.strip(): + summary+=log.description.strip('\n')+"\n\n" + return summary + + def revision_specs(self, args): + """Determine the base and merge revisions from tree and arguments. + + :param args: The parsed arguments + :type args: list of str + :return: The base revision and merge revisions + :rtype: `arch.Revision`, list of `arch.Patchlog` + """ + if len(args) > 0: + target_revision = cmdutil.determine_revision_arch(self.tree, + args[0]) + else: + target_revision = arch_compound.tree_latest(self.tree) + if len(args) > 1: + merges = [ arch.Patchlog(cmdutil.determine_revision_arch( + self.tree, f)) for f in args[1:] ] + else: + if self.tree is None: + raise CantDetermineRevision("", "Not in a project tree") + merge_iter = cmdutil.iter_new_merges(self.tree, + target_revision.version, + False) + merges = [f for f in cmdutil.direct_merges(merge_iter)] + return (target_revision, merges) + + def edit_message(self, message): + """Edit an email message in the user's standard editor + + :param message: The message to edit + :type message: str + :return: the path of the edited message + :rtype: str + """ + if self.tree is None: + path = os.get_cwd() + else: + path = self.tree + path += "/,merge-request" + file = open(path, 'w') + file.write(message) + file.flush() + cmdutil.invoke_editor(path) + return path + + def tidy_message(self, path): + """Validate and clean up message. + + :param path: The path to the message to clean up + :type path: str + :return: The parsed message + :rtype: `email.Message` + """ + mail = email.message_from_file(open(path)) + if mail["Subject"].strip() == "[MERGE REQUEST]": + raise BlandSubject + + request = email.message_from_string(mail.get_payload()) + if request.has_key("Bug"): + if request["Bug"].strip()=="": + del request["Bug"] + mail.set_payload(request.as_string()) + return mail + + def send_message(self, message): + """Send a message, using its headers to address it. + + :param message: The message to send + :type message: `email.Message`""" + server = smtplib.SMTP("localhost") + server.sendmail(message['From'], message['To'], message.as_string()) + server.quit() + + def help(self, parser=None): + """Print a usage message + + :param parser: The options parser to use + :type parser: `cmdutil.CmdOptionParser` + """ + if parser is None: + parser = self.get_parser() + parser.print_help() + print """ +Sends a merge request formatted for Bug Goo. Intended use: get the tree +you'd like to merge into. Apply the merges you want. Invoke request-merge. +The merge request will open in your $EDITOR. + +When no TARGET is specified, it uses the current tree revision. When +no MERGE is specified, it uses the direct merges (as in "revisions +--direct-merges"). But you can specify just the TARGET, or all the MERGE +revisions. +""" + + def get_parser(self): + """Produce a commandline parser for this command. + + :rtype: `cmdutil.CmdOptionParser` + """ + parser=cmdutil.CmdOptionParser("request-merge [TARGET] [MERGE1...]") + return parser + +commands = { +'changes' : Changes, +'help' : Help, +'update': Update, +'apply-changes':ApplyChanges, +'cat-log': CatLog, +'commit': Commit, +'revision': Revision, +'revisions': Revisions, +'get': Get, +'revert': Revert, +'shell': Shell, +'add-id': AddID, +'merge': Merge, +'elog': ELog, +'mirror-archive': MirrorArchive, +'ninventory': Inventory, +'alias' : Alias, +'request-merge': RequestMerge, +} + +def my_import(mod_name): + module = __import__(mod_name) + components = mod_name.split('.') + for comp in components[1:]: + module = getattr(module, comp) + return module + +def plugin(mod_name): + module = my_import(mod_name) + module.add_command(commands) + +for file in os.listdir(sys.path[0]+"/command"): + if len(file) > 3 and file[-3:] == ".py" and file != "__init__.py": + plugin("command."+file[:-3]) + +suggestions = { +'apply-delta' : "Try \"apply-changes\".", +'delta' : "To compare two revisions, use \"changes\".", +'diff-rev' : "To compare two revisions, use \"changes\".", +'undo' : "To undo local changes, use \"revert\".", +'undelete' : "To undo only deletions, use \"revert --deletions\"", +'missing-from' : "Try \"revisions --missing-from\".", +'missing' : "Try \"revisions --missing\".", +'missing-merge' : "Try \"revisions --partner-missing\".", +'new-merges' : "Try \"revisions --new-merges\".", +'cachedrevs' : "Try \"revisions --cacherevs\". (no 'd')", +'logs' : "Try \"revisions --logs\"", +'tree-source' : "Use the \"^ttag\" alias (\"revision ^ttag\")", +'latest-revision' : "Use the \"^acur\" alias (\"revision ^acur\")", +'change-version' : "Try \"update REVISION\"", +'tree-revision' : "Use the \"^tcur\" alias (\"revision ^tcur\")", +'rev-depends' : "Use revisions --dependencies", +'auto-get' : "Plain get will do archive lookups", +'tagline' : "Use add-id. It uses taglines in tagline trees", +'emlog' : "Use elog. It automatically adds log-for-merge text, if any", +'library-revisions' : "Use revisions --library", +'file-revert' : "Use revert FILE", +'join-branch' : "Use replay --logs-only" +} +# arch-tag: 19d5739d-3708-486c-93ba-deecc3027fc7 *** added file 'testdata/orig' --- /dev/null +++ testdata/orig @@ -0,0 +1,2789 @@ +# Copyright (C) 2004 Aaron Bentley +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import sys +import arch +import arch.util +import arch.arch +import abacmds +import cmdutil +import shutil +import os +import options +import paths +import time +import cmd +import readline +import re +import string +import arch_core +from errors import * +import errors +import terminal +import ancillary +import misc +import email +import smtplib + +__docformat__ = "restructuredtext" +__doc__ = "Implementation of user (sub) commands" +commands = {} + +def find_command(cmd): + """ + Return an instance of a command type. Return None if the type isn't + registered. + + :param cmd: the name of the command to look for + :type cmd: the type of the command + """ + if commands.has_key(cmd): + return commands[cmd]() + else: + return None + +class BaseCommand: + def __call__(self, cmdline): + try: + self.do_command(cmdline.split()) + except cmdutil.GetHelp, e: + self.help() + except Exception, e: + print e + + def get_completer(index): + return None + + def complete(self, args, text): + """ + Returns a list of possible completions for the given text. + + :param args: The complete list of arguments + :type args: List of str + :param text: text to complete (may be shorter than args[-1]) + :type text: str + :rtype: list of str + """ + matches = [] + candidates = None + + if len(args) > 0: + realtext = args[-1] + else: + realtext = "" + + try: + parser=self.get_parser() + if realtext.startswith('-'): + candidates = parser.iter_options() + else: + (options, parsed_args) = parser.parse_args(args) + + if len (parsed_args) > 0: + candidates = self.get_completer(parsed_args[-1], len(parsed_args) -1) + else: + candidates = self.get_completer("", 0) + except: + pass + if candidates is None: + return + for candidate in candidates: + candidate = str(candidate) + if candidate.startswith(realtext): + matches.append(candidate[len(realtext)- len(text):]) + return matches + + +class Help(BaseCommand): + """ + Lists commands, prints help messages. + """ + def __init__(self): + self.description="Prints help mesages" + self.parser = None + + def do_command(self, cmdargs): + """ + Prints a help message. + """ + options, args = self.get_parser().parse_args(cmdargs) + if len(args) > 1: + raise cmdutil.GetHelp + + if options.native or options.suggestions or options.external: + native = options.native + suggestions = options.suggestions + external = options.external + else: + native = True + suggestions = False + external = True + + if len(args) == 0: + self.list_commands(native, suggestions, external) + return + elif len(args) == 1: + command_help(args[0]) + return + + def help(self): + self.get_parser().print_help() + print """ +If no command is specified, commands are listed. If a command is +specified, help for that command is listed. + """ + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + if self.parser is not None: + return self.parser + parser=cmdutil.CmdOptionParser("fai help [command]") + parser.add_option("-n", "--native", action="store_true", + dest="native", help="Show native commands") + parser.add_option("-e", "--external", action="store_true", + dest="external", help="Show external commands") + parser.add_option("-s", "--suggest", action="store_true", + dest="suggestions", help="Show suggestions") + self.parser = parser + return parser + + def list_commands(self, native=True, suggest=False, external=True): + """ + Lists supported commands. + + :param native: list native, python-based commands + :type native: bool + :param external: list external aba-style commands + :type external: bool + """ + if native: + print "Native Fai commands" + keys=commands.keys() + keys.sort() + for k in keys: + space="" + for i in range(28-len(k)): + space+=" " + print space+k+" : "+commands[k]().description + print + if suggest: + print "Unavailable commands and suggested alternatives" + key_list = suggestions.keys() + key_list.sort() + for key in key_list: + print "%28s : %s" % (key, suggestions[key]) + print + if external: + fake_aba = abacmds.AbaCmds() + if (fake_aba.abadir == ""): + return + print "External commands" + fake_aba.list_commands() + print + if not suggest: + print "Use help --suggest to list alternatives to tla and aba"\ + " commands." + if options.tla_fallthrough and (native or external): + print "Fai also supports tla commands." + +def command_help(cmd): + """ + Prints help for a command. + + :param cmd: The name of the command to print help for + :type cmd: str + """ + fake_aba = abacmds.AbaCmds() + cmdobj = find_command(cmd) + if cmdobj != None: + cmdobj.help() + elif suggestions.has_key(cmd): + print "Not available\n" + suggestions[cmd] + else: + abacmd = fake_aba.is_command(cmd) + if abacmd: + abacmd.help() + else: + print "No help is available for \""+cmd+"\". Maybe try \"tla "+cmd+" -H\"?" + + + +class Changes(BaseCommand): + """ + the "changes" command: lists differences between trees/revisions: + """ + + def __init__(self): + self.description="Lists what files have changed in the project tree" + + def get_completer(self, arg, index): + if index > 1: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def parse_commandline(self, cmdline): + """ + Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. + + :param cmdline: A list of arguments to parse + :rtype: (options, Revision, Revision/WorkingTree) + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdline) + if len(args) > 2: + raise cmdutil.GetHelp + + tree=arch.tree_root() + if len(args) == 0: + a_spec = cmdutil.comp_revision(tree) + else: + a_spec = cmdutil.determine_revision_tree(tree, args[0]) + cmdutil.ensure_archive_registered(a_spec.archive) + if len(args) == 2: + b_spec = cmdutil.determine_revision_tree(tree, args[1]) + cmdutil.ensure_archive_registered(b_spec.archive) + else: + b_spec=tree + return options, a_spec, b_spec + + def do_command(self, cmdargs): + """ + Master function that perfoms the "changes" command. + """ + try: + options, a_spec, b_spec = self.parse_commandline(cmdargs); + except cmdutil.CantDetermineRevision, e: + print e + return + except arch.errors.TreeRootError, e: + print e + return + if options.changeset: + changeset=options.changeset + tmpdir = None + else: + tmpdir=cmdutil.tmpdir() + changeset=tmpdir+"/changeset" + try: + delta=arch.iter_delta(a_spec, b_spec, changeset) + try: + for line in delta: + if cmdutil.chattermatch(line, "changeset:"): + pass + else: + cmdutil.colorize(line, options.suppress_chatter) + except arch.util.ExecProblem, e: + if e.proc.error and e.proc.error.startswith( + "missing explicit id for file"): + raise MissingID(e) + else: + raise + status=delta.status + if status > 1: + return + if (options.perform_diff): + chan = cmdutil.ChangesetMunger(changeset) + chan.read_indices() + if isinstance(b_spec, arch.Revision): + b_dir = b_spec.library_find() + else: + b_dir = b_spec + a_dir = a_spec.library_find() + if options.diffopts is not None: + diffopts = options.diffopts.split() + cmdutil.show_custom_diffs(chan, diffopts, a_dir, b_dir) + else: + cmdutil.show_diffs(delta.changeset) + finally: + if tmpdir and (os.access(tmpdir, os.X_OK)): + shutil.rmtree(tmpdir) + + def get_parser(self): + """ + Returns the options parser to use for the "changes" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai changes [options] [revision]" + " [revision]") + parser.add_option("-d", "--diff", action="store_true", + dest="perform_diff", default=False, + help="Show diffs in summary") + parser.add_option("-c", "--changeset", dest="changeset", + help="Store a changeset in the given directory", + metavar="DIRECTORY") + parser.add_option("-s", "--silent", action="store_true", + dest="suppress_chatter", default=False, + help="Suppress chatter messages") + parser.add_option("--diffopts", dest="diffopts", + help="Use the specified diff options", + metavar="OPTIONS") + + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser is None: + parser=self.get_parser() + parser.print_help() + print """ +Performs source-tree comparisons + +If no revision is specified, the current project tree is compared to the +last-committed revision. If one revision is specified, the current project +tree is compared to that revision. If two revisions are specified, they are +compared to each other. + """ + help_tree_spec() + return + + +class ApplyChanges(BaseCommand): + """ + Apply differences between two revisions to a tree + """ + + def __init__(self): + self.description="Applies changes to a project tree" + + def get_completer(self, arg, index): + if index > 1: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def parse_commandline(self, cmdline, tree): + """ + Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. + + :param cmdline: A list of arguments to parse + :rtype: (options, Revision, Revision/WorkingTree) + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdline) + if len(args) != 2: + raise cmdutil.GetHelp + + a_spec = cmdutil.determine_revision_tree(tree, args[0]) + cmdutil.ensure_archive_registered(a_spec.archive) + b_spec = cmdutil.determine_revision_tree(tree, args[1]) + cmdutil.ensure_archive_registered(b_spec.archive) + return options, a_spec, b_spec + + def do_command(self, cmdargs): + """ + Master function that performs "apply-changes". + """ + try: + tree = arch.tree_root() + options, a_spec, b_spec = self.parse_commandline(cmdargs, tree); + except cmdutil.CantDetermineRevision, e: + print e + return + except arch.errors.TreeRootError, e: + print e + return + delta=cmdutil.apply_delta(a_spec, b_spec, tree) + for line in cmdutil.iter_apply_delta_filter(delta): + cmdutil.colorize(line, options.suppress_chatter) + + def get_parser(self): + """ + Returns the options parser to use for the "apply-changes" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai apply-changes [options] revision" + " revision") + parser.add_option("-d", "--diff", action="store_true", + dest="perform_diff", default=False, + help="Show diffs in summary") + parser.add_option("-c", "--changeset", dest="changeset", + help="Store a changeset in the given directory", + metavar="DIRECTORY") + parser.add_option("-s", "--silent", action="store_true", + dest="suppress_chatter", default=False, + help="Suppress chatter messages") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser is None: + parser=self.get_parser() + parser.print_help() + print """ +Applies changes to a project tree + +Compares two revisions and applies the difference between them to the current +tree. + """ + help_tree_spec() + return + +class Update(BaseCommand): + """ + Updates a project tree to a given revision, preserving un-committed hanges. + """ + + def __init__(self): + self.description="Apply the latest changes to the current directory" + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def parse_commandline(self, cmdline, tree): + """ + Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. + + :param cmdline: A list of arguments to parse + :rtype: (options, Revision, Revision/WorkingTree) + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdline) + if len(args) > 2: + raise cmdutil.GetHelp + + spec=None + if len(args)>0: + spec=args[0] + revision=cmdutil.determine_revision_arch(tree, spec) + cmdutil.ensure_archive_registered(revision.archive) + + mirror_source = cmdutil.get_mirror_source(revision.archive) + if mirror_source != None: + if cmdutil.prompt("Mirror update"): + cmd=cmdutil.mirror_archive(mirror_source, + revision.archive, arch.NameParser(revision).get_package_version()) + for line in arch.chatter_classifier(cmd): + cmdutil.colorize(line, options.suppress_chatter) + + revision=cmdutil.determine_revision_arch(tree, spec) + + return options, revision + + def do_command(self, cmdargs): + """ + Master function that perfoms the "update" command. + """ + tree=arch.tree_root() + try: + options, to_revision = self.parse_commandline(cmdargs, tree); + except cmdutil.CantDetermineRevision, e: + print e + return + except arch.errors.TreeRootError, e: + print e + return + from_revision=cmdutil.tree_latest(tree) + if from_revision==to_revision: + print "Tree is already up to date with:\n"+str(to_revision)+"." + return + cmdutil.ensure_archive_registered(from_revision.archive) + cmd=cmdutil.apply_delta(from_revision, to_revision, tree, + options.patch_forward) + for line in cmdutil.iter_apply_delta_filter(cmd): + cmdutil.colorize(line) + if to_revision.version != tree.tree_version: + if cmdutil.prompt("Update version"): + tree.tree_version = to_revision.version + + def get_parser(self): + """ + Returns the options parser to use for the "update" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai update [options]" + " [revision/version]") + parser.add_option("-f", "--forward", action="store_true", + dest="patch_forward", default=False, + help="pass the --forward option to 'patch'") + parser.add_option("-s", "--silent", action="store_true", + dest="suppress_chatter", default=False, + help="Suppress chatter messages") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser is None: + parser=self.get_parser() + parser.print_help() + print """ +Updates a working tree to the current archive revision + +If a revision or version is specified, that is used instead + """ + help_tree_spec() + return + + +class Commit(BaseCommand): + """ + Create a revision based on the changes in the current tree. + """ + + def __init__(self): + self.description="Write local changes to the archive" + + def get_completer(self, arg, index): + if arg is None: + arg = "" + return iter_modified_file_completions(arch.tree_root(), arg) +# return iter_source_file_completions(arch.tree_root(), arg) + + def parse_commandline(self, cmdline, tree): + """ + Parse commandline arguments. Raise cmtutil.GetHelp if help is needed. + + :param cmdline: A list of arguments to parse + :rtype: (options, Revision, Revision/WorkingTree) + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdline) + + if len(args) == 0: + args = None + revision=cmdutil.determine_revision_arch(tree, options.version) + return options, revision.get_version(), args + + def do_command(self, cmdargs): + """ + Master function that perfoms the "commit" command. + """ + tree=arch.tree_root() + options, version, files = self.parse_commandline(cmdargs, tree) + if options.__dict__.has_key("base") and options.base: + base = cmdutil.determine_revision_tree(tree, options.base) + else: + base = cmdutil.submit_revision(tree) + + writeversion=version + archive=version.archive + source=cmdutil.get_mirror_source(archive) + allow_old=False + writethrough="implicit" + + if source!=None: + if writethrough=="explicit" and \ + cmdutil.prompt("Writethrough"): + writeversion=arch.Version(str(source)+"/"+str(version.get_nonarch())) + elif writethrough=="none": + raise CommitToMirror(archive) + + elif archive.is_mirror: + raise CommitToMirror(archive) + + try: + last_revision=tree.iter_logs(version, True).next().revision + except StopIteration, e: + if cmdutil.prompt("Import from commit"): + return do_import(version) + else: + raise NoVersionLogs(version) + if last_revision!=version.iter_revisions(True).next(): + if not cmdutil.prompt("Out of date"): + raise OutOfDate + else: + allow_old=True + + try: + if not cmdutil.has_changed(version): + if not cmdutil.prompt("Empty commit"): + raise EmptyCommit + except arch.util.ExecProblem, e: + if e.proc.error and e.proc.error.startswith( + "missing explicit id for file"): + raise MissingID(e) + else: + raise + log = tree.log_message(create=False) + if log is None: + try: + if cmdutil.prompt("Create log"): + edit_log(tree) + + except cmdutil.NoEditorSpecified, e: + raise CommandFailed(e) + log = tree.log_message(create=False) + if log is None: + raise NoLogMessage + if log["Summary"] is None or len(log["Summary"].strip()) == 0: + if not cmdutil.prompt("Omit log summary"): + raise errors.NoLogSummary + try: + for line in tree.iter_commit(version, seal=options.seal_version, + base=base, out_of_date_ok=allow_old, file_list=files): + cmdutil.colorize(line, options.suppress_chatter) + + except arch.util.ExecProblem, e: + if e.proc.error and e.proc.error.startswith( + "These files violate naming conventions:"): + raise LintFailure(e.proc.error) + else: + raise + + def get_parser(self): + """ + Returns the options parser to use for the "commit" command. + + :rtype: cmdutil.CmdOptionParser + """ + + parser=cmdutil.CmdOptionParser("fai commit [options] [file1]" + " [file2...]") + parser.add_option("--seal", action="store_true", + dest="seal_version", default=False, + help="seal this version") + parser.add_option("-v", "--version", dest="version", + help="Use the specified version", + metavar="VERSION") + parser.add_option("-s", "--silent", action="store_true", + dest="suppress_chatter", default=False, + help="Suppress chatter messages") + if cmdutil.supports_switch("commit", "--base"): + parser.add_option("--base", dest="base", help="", + metavar="REVISION") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser is None: + parser=self.get_parser() + parser.print_help() + print """ +Updates a working tree to the current archive revision + +If a version is specified, that is used instead + """ +# help_tree_spec() + return + + + +class CatLog(BaseCommand): + """ + Print the log of a given file (from current tree) + """ + def __init__(self): + self.description="Prints the patch log for a revision" + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "cat-log" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: + tree = arch.tree_root() + except arch.errors.TreeRootError, e: + tree = None + spec=None + if len(args) > 0: + spec=args[0] + if len(args) > 1: + raise cmdutil.GetHelp() + try: + if tree: + revision = cmdutil.determine_revision_tree(tree, spec) + else: + revision = cmdutil.determine_revision_arch(tree, spec) + except cmdutil.CantDetermineRevision, e: + raise CommandFailedWrapper(e) + log = None + + use_tree = (options.source == "tree" or \ + (options.source == "any" and tree)) + use_arch = (options.source == "archive" or options.source == "any") + + log = None + if use_tree: + for log in tree.iter_logs(revision.get_version()): + if log.revision == revision: + break + else: + log = None + if log is None and use_arch: + cmdutil.ensure_revision_exists(revision) + log = arch.Patchlog(revision) + if log is not None: + for item in log.items(): + print "%s: %s" % item + print log.description + + def get_parser(self): + """ + Returns the options parser to use for the "cat-log" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai cat-log [revision]") + parser.add_option("--archive", action="store_const", dest="source", + const="archive", default="any", + help="Always get the log from the archive") + parser.add_option("--tree", action="store_const", dest="source", + const="tree", help="Always get the log from the tree") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Prints the log for the specified revision + """ + help_tree_spec() + return + +class Revert(BaseCommand): + """ Reverts a tree (or aspects of it) to a revision + """ + def __init__(self): + self.description="Reverts a tree (or aspects of it) to a revision " + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return iter_modified_file_completions(tree, arg) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revert" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: + tree = arch.tree_root() + except arch.errors.TreeRootError, e: + raise CommandFailed(e) + spec=None + if options.revision is not None: + spec=options.revision + try: + if spec is not None: + revision = cmdutil.determine_revision_tree(tree, spec) + else: + revision = cmdutil.comp_revision(tree) + except cmdutil.CantDetermineRevision, e: + raise CommandFailedWrapper(e) + munger = None + + if options.file_contents or options.file_perms or options.deletions\ + or options.additions or options.renames or options.hunk_prompt: + munger = cmdutil.MungeOpts() + munger.hunk_prompt = options.hunk_prompt + + if len(args) > 0 or options.logs or options.pattern_files or \ + options.control: + if munger is None: + munger = cmdutil.MungeOpts(True) + munger.all_types(True) + if len(args) > 0: + t_cwd = cmdutil.tree_cwd(tree) + for name in args: + if len(t_cwd) > 0: + t_cwd += "/" + name = "./" + t_cwd + name + munger.add_keep_file(name); + + if options.file_perms: + munger.file_perms = True + if options.file_contents: + munger.file_contents = True + if options.deletions: + munger.deletions = True + if options.additions: + munger.additions = True + if options.renames: + munger.renames = True + if options.logs: + munger.add_keep_pattern('^\./\{arch\}/[^=].*') + if options.control: + munger.add_keep_pattern("/\.arch-ids|^\./\{arch\}|"\ + "/\.arch-inventory$") + if options.pattern_files: + munger.add_keep_pattern(options.pattern_files) + + for line in cmdutil.revert(tree, revision, munger, + not options.no_output): + cmdutil.colorize(line) + + + def get_parser(self): + """ + Returns the options parser to use for the "cat-log" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai revert [options] [FILE...]") + parser.add_option("", "--contents", action="store_true", + dest="file_contents", + help="Revert file content changes") + parser.add_option("", "--permissions", action="store_true", + dest="file_perms", + help="Revert file permissions changes") + parser.add_option("", "--deletions", action="store_true", + dest="deletions", + help="Restore deleted files") + parser.add_option("", "--additions", action="store_true", + dest="additions", + help="Remove added files") + parser.add_option("", "--renames", action="store_true", + dest="renames", + help="Revert file names") + parser.add_option("--hunks", action="store_true", + dest="hunk_prompt", default=False, + help="Prompt which hunks to revert") + parser.add_option("--pattern-files", dest="pattern_files", + help="Revert files that match this pattern", + metavar="REGEX") + parser.add_option("--logs", action="store_true", + dest="logs", default=False, + help="Revert only logs") + parser.add_option("--control-files", action="store_true", + dest="control", default=False, + help="Revert logs and other control files") + parser.add_option("-n", "--no-output", action="store_true", + dest="no_output", + help="Don't keep an undo changeset") + parser.add_option("--revision", dest="revision", + help="Revert to the specified revision", + metavar="REVISION") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Reverts changes in the current working tree. If no flags are specified, all +types of changes are reverted. Otherwise, only selected types of changes are +reverted. + +If a revision is specified on the commandline, differences between the current +tree and that revision are reverted. If a version is specified, the current +tree is used to determine the revision. + +If files are specified, only those files listed will have any changes applied. +To specify a renamed file, you can use either the old or new name. (or both!) + +Unless "-n" is specified, reversions can be undone with "redo". + """ + return + +class Revision(BaseCommand): + """ + Print a revision name based on a revision specifier + """ + def __init__(self): + self.description="Prints the name of a revision" + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + tree = None + + spec=None + if len(args) > 0: + spec=args[0] + if len(args) > 1: + raise cmdutil.GetHelp + try: + if tree: + revision = cmdutil.determine_revision_tree(tree, spec) + else: + revision = cmdutil.determine_revision_arch(tree, spec) + except cmdutil.CantDetermineRevision, e: + print str(e) + return + print options.display(revision) + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai revision [revision]") + parser.add_option("", "--location", action="store_const", + const=paths.determine_path, dest="display", + help="Show location instead of name", default=str) + parser.add_option("--import", action="store_const", + const=paths.determine_import_path, dest="display", + help="Show location of import file") + parser.add_option("--log", action="store_const", + const=paths.determine_log_path, dest="display", + help="Show location of log file") + parser.add_option("--patch", action="store_const", + dest="display", const=paths.determine_patch_path, + help="Show location of patchfile") + parser.add_option("--continuation", action="store_const", + const=paths.determine_continuation_path, + dest="display", + help="Show location of continuation file") + parser.add_option("--cacherev", action="store_const", + const=paths.determine_cacherev_path, dest="display", + help="Show location of cacherev file") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Expands aliases and prints the name of the specified revision. Instead of +the name, several options can be used to print locations. If more than one is +specified, the last one is used. + """ + help_tree_spec() + return + +def require_version_exists(version, spec): + if not version.exists(): + raise cmdutil.CantDetermineVersion(spec, + "The version %s does not exist." \ + % version) + +class Revisions(BaseCommand): + """ + Print a revision name based on a revision specifier + """ + def __init__(self): + self.description="Lists revisions" + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + (options, args) = self.get_parser().parse_args(cmdargs) + if len(args) > 1: + raise cmdutil.GetHelp + try: + self.tree = arch.tree_root() + except arch.errors.TreeRootError: + self.tree = None + try: + iter = self.get_iterator(options.type, args, options.reverse, + options.modified) + except cmdutil.CantDetermineRevision, e: + raise CommandFailedWrapper(e) + + if options.skip is not None: + iter = cmdutil.iter_skip(iter, int(options.skip)) + + for revision in iter: + log = None + if isinstance(revision, arch.Patchlog): + log = revision + revision=revision.revision + print options.display(revision) + if log is None and (options.summary or options.creator or + options.date or options.merges): + log = revision.patchlog + if options.creator: + print " %s" % log.creator + if options.date: + print " %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date) + if options.summary: + print " %s" % log.summary + if options.merges: + showed_title = False + for revision in log.merged_patches: + if not showed_title: + print " Merged:" + showed_title = True + print " %s" % revision + + def get_iterator(self, type, args, reverse, modified): + if len(args) > 0: + spec = args[0] + else: + spec = None + if modified is not None: + iter = cmdutil.modified_iter(modified, self.tree) + if reverse: + return iter + else: + return cmdutil.iter_reverse(iter) + elif type == "archive": + if spec is None: + if self.tree is None: + raise cmdutil.CantDetermineRevision("", + "Not in a project tree") + version = cmdutil.determine_version_tree(spec, self.tree) + else: + version = cmdutil.determine_version_arch(spec, self.tree) + cmdutil.ensure_archive_registered(version.archive) + require_version_exists(version, spec) + return version.iter_revisions(reverse) + elif type == "cacherevs": + if spec is None: + if self.tree is None: + raise cmdutil.CantDetermineRevision("", + "Not in a project tree") + version = cmdutil.determine_version_tree(spec, self.tree) + else: + version = cmdutil.determine_version_arch(spec, self.tree) + cmdutil.ensure_archive_registered(version.archive) + require_version_exists(version, spec) + return cmdutil.iter_cacherevs(version, reverse) + elif type == "library": + if spec is None: + if self.tree is None: + raise cmdutil.CantDetermineRevision("", + "Not in a project tree") + version = cmdutil.determine_version_tree(spec, self.tree) + else: + version = cmdutil.determine_version_arch(spec, self.tree) + return version.iter_library_revisions(reverse) + elif type == "logs": + if self.tree is None: + raise cmdutil.CantDetermineRevision("", "Not in a project tree") + return self.tree.iter_logs(cmdutil.determine_version_tree(spec, \ + self.tree), reverse) + elif type == "missing" or type == "skip-present": + if self.tree is None: + raise cmdutil.CantDetermineRevision("", "Not in a project tree") + skip = (type == "skip-present") + version = cmdutil.determine_version_tree(spec, self.tree) + cmdutil.ensure_archive_registered(version.archive) + require_version_exists(version, spec) + return cmdutil.iter_missing(self.tree, version, reverse, + skip_present=skip) + + elif type == "present": + if self.tree is None: + raise cmdutil.CantDetermineRevision("", "Not in a project tree") + version = cmdutil.determine_version_tree(spec, self.tree) + cmdutil.ensure_archive_registered(version.archive) + require_version_exists(version, spec) + return cmdutil.iter_present(self.tree, version, reverse) + + elif type == "new-merges" or type == "direct-merges": + if self.tree is None: + raise cmdutil.CantDetermineRevision("", "Not in a project tree") + version = cmdutil.determine_version_tree(spec, self.tree) + cmdutil.ensure_archive_registered(version.archive) + require_version_exists(version, spec) + iter = cmdutil.iter_new_merges(self.tree, version, reverse) + if type == "new-merges": + return iter + elif type == "direct-merges": + return cmdutil.direct_merges(iter) + + elif type == "missing-from": + if self.tree is None: + raise cmdutil.CantDetermineRevision("", "Not in a project tree") + revision = cmdutil.determine_revision_tree(self.tree, spec) + libtree = cmdutil.find_or_make_local_revision(revision) + return cmdutil.iter_missing(libtree, self.tree.tree_version, + reverse) + + elif type == "partner-missing": + return cmdutil.iter_partner_missing(self.tree, reverse) + + elif type == "ancestry": + revision = cmdutil.determine_revision_tree(self.tree, spec) + iter = cmdutil._iter_ancestry(self.tree, revision) + if reverse: + return iter + else: + return cmdutil.iter_reverse(iter) + + elif type == "dependencies" or type == "non-dependencies": + nondeps = (type == "non-dependencies") + revision = cmdutil.determine_revision_tree(self.tree, spec) + anc_iter = cmdutil._iter_ancestry(self.tree, revision) + iter_depends = cmdutil.iter_depends(anc_iter, nondeps) + if reverse: + return iter_depends + else: + return cmdutil.iter_reverse(iter_depends) + elif type == "micro": + return cmdutil.iter_micro(self.tree) + + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai revisions [revision]") + select = cmdutil.OptionGroup(parser, "Selection options", + "Control which revisions are listed. These options" + " are mutually exclusive. If more than one is" + " specified, the last is used.") + select.add_option("", "--archive", action="store_const", + const="archive", dest="type", default="archive", + help="List all revisions in the archive") + select.add_option("", "--cacherevs", action="store_const", + const="cacherevs", dest="type", + help="List all revisions stored in the archive as " + "complete copies") + select.add_option("", "--logs", action="store_const", + const="logs", dest="type", + help="List revisions that have a patchlog in the " + "tree") + select.add_option("", "--missing", action="store_const", + const="missing", dest="type", + help="List revisions from the specified version that" + " have no patchlog in the tree") + select.add_option("", "--skip-present", action="store_const", + const="skip-present", dest="type", + help="List revisions from the specified version that" + " have no patchlogs at all in the tree") + select.add_option("", "--present", action="store_const", + const="present", dest="type", + help="List revisions from the specified version that" + " have no patchlog in the tree, but can't be merged") + select.add_option("", "--missing-from", action="store_const", + const="missing-from", dest="type", + help="List revisions from the specified revision " + "that have no patchlog for the tree version") + select.add_option("", "--partner-missing", action="store_const", + const="partner-missing", dest="type", + help="List revisions in partner versions that are" + " missing") + select.add_option("", "--new-merges", action="store_const", + const="new-merges", dest="type", + help="List revisions that have had patchlogs added" + " to the tree since the last commit") + select.add_option("", "--direct-merges", action="store_const", + const="direct-merges", dest="type", + help="List revisions that have been directly added" + " to tree since the last commit ") + select.add_option("", "--library", action="store_const", + const="library", dest="type", + help="List revisions in the revision library") + select.add_option("", "--ancestry", action="store_const", + const="ancestry", dest="type", + help="List revisions that are ancestors of the " + "current tree version") + + select.add_option("", "--dependencies", action="store_const", + const="dependencies", dest="type", + help="List revisions that the given revision " + "depends on") + + select.add_option("", "--non-dependencies", action="store_const", + const="non-dependencies", dest="type", + help="List revisions that the given revision " + "does not depend on") + + select.add_option("--micro", action="store_const", + const="micro", dest="type", + help="List partner revisions aimed for this " + "micro-branch") + + select.add_option("", "--modified", dest="modified", + help="List tree ancestor revisions that modified a " + "given file", metavar="FILE[:LINE]") + + parser.add_option("", "--skip", dest="skip", + help="Skip revisions. Positive numbers skip from " + "beginning, negative skip from end.", + metavar="NUMBER") + + parser.add_option_group(select) + + format = cmdutil.OptionGroup(parser, "Revision format options", + "These control the appearance of listed revisions") + format.add_option("", "--location", action="store_const", + const=paths.determine_path, dest="display", + help="Show location instead of name", default=str) + format.add_option("--import", action="store_const", + const=paths.determine_import_path, dest="display", + help="Show location of import file") + format.add_option("--log", action="store_const", + const=paths.determine_log_path, dest="display", + help="Show location of log file") + format.add_option("--patch", action="store_const", + dest="display", const=paths.determine_patch_path, + help="Show location of patchfile") + format.add_option("--continuation", action="store_const", + const=paths.determine_continuation_path, + dest="display", + help="Show location of continuation file") + format.add_option("--cacherev", action="store_const", + const=paths.determine_cacherev_path, dest="display", + help="Show location of cacherev file") + parser.add_option_group(format) + display = cmdutil.OptionGroup(parser, "Display format options", + "These control the display of data") + display.add_option("-r", "--reverse", action="store_true", + dest="reverse", help="Sort from newest to oldest") + display.add_option("-s", "--summary", action="store_true", + dest="summary", help="Show patchlog summary") + display.add_option("-D", "--date", action="store_true", + dest="date", help="Show patchlog date") + display.add_option("-c", "--creator", action="store_true", + dest="creator", help="Show the id that committed the" + " revision") + display.add_option("-m", "--merges", action="store_true", + dest="merges", help="Show the revisions that were" + " merged") + parser.add_option_group(display) + return parser + def help(self, parser=None): + """Attempt to explain the revisions command + + :param parser: If supplied, used to determine options + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """List revisions. + """ + help_tree_spec() + + +class Get(BaseCommand): + """ + Retrieve a revision from the archive + """ + def __init__(self): + self.description="Retrieve a revision from the archive" + self.parser=self.get_parser() + + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + + def do_command(self, cmdargs): + """ + Master function that perfoms the "get" command. + """ + (options, args) = self.parser.parse_args(cmdargs) + if len(args) < 1: + return self.help() + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + tree = None + + arch_loc = None + try: + revision, arch_loc = paths.full_path_decode(args[0]) + except Exception, e: + revision = cmdutil.determine_revision_arch(tree, args[0], + check_existence=False, allow_package=True) + if len(args) > 1: + directory = args[1] + else: + directory = str(revision.nonarch) + if os.path.exists(directory): + raise DirectoryExists(directory) + cmdutil.ensure_archive_registered(revision.archive, arch_loc) + try: + cmdutil.ensure_revision_exists(revision) + except cmdutil.NoSuchRevision, e: + raise CommandFailedWrapper(e) + + link = cmdutil.prompt ("get link") + for line in cmdutil.iter_get(revision, directory, link, + options.no_pristine, + options.no_greedy_add): + cmdutil.colorize(line) + + def get_parser(self): + """ + Returns the options parser to use for the "get" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai get revision [dir]") + parser.add_option("--no-pristine", action="store_true", + dest="no_pristine", + help="Do not make pristine copy for reference") + parser.add_option("--no-greedy-add", action="store_true", + dest="no_greedy_add", + help="Never add to greedy libraries") + + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Expands aliases and constructs a project tree for a revision. If the optional +"dir" argument is provided, the project tree will be stored in this directory. + """ + help_tree_spec() + return + +class PromptCmd(cmd.Cmd): + def __init__(self): + cmd.Cmd.__init__(self) + self.prompt = "Fai> " + try: + self.tree = arch.tree_root() + except: + self.tree = None + self.set_title() + self.set_prompt() + self.fake_aba = abacmds.AbaCmds() + self.identchars += '-' + self.history_file = os.path.expanduser("~/.fai-history") + readline.set_completer_delims(string.whitespace) + if os.access(self.history_file, os.R_OK) and \ + os.path.isfile(self.history_file): + readline.read_history_file(self.history_file) + + def write_history(self): + readline.write_history_file(self.history_file) + + def do_quit(self, args): + self.write_history() + sys.exit(0) + + def do_exit(self, args): + self.do_quit(args) + + def do_EOF(self, args): + print + self.do_quit(args) + + def postcmd(self, line, bar): + self.set_title() + self.set_prompt() + + def set_prompt(self): + if self.tree is not None: + try: + version = " "+self.tree.tree_version.nonarch + except: + version = "" + else: + version = "" + self.prompt = "Fai%s> " % version + + def set_title(self, command=None): + try: + version = self.tree.tree_version.nonarch + except: + version = "[no version]" + if command is None: + command = "" + sys.stdout.write(terminal.term_title("Fai %s %s" % (command, version))) + + def do_cd(self, line): + if line == "": + line = "~" + try: + os.chdir(os.path.expanduser(line)) + except Exception, e: + print e + try: + self.tree = arch.tree_root() + except: + self.tree = None + + def do_help(self, line): + Help()(line) + + def default(self, line): + args = line.split() + if find_command(args[0]): + try: + find_command(args[0]).do_command(args[1:]) + except cmdutil.BadCommandOption, e: + print e + except cmdutil.GetHelp, e: + find_command(args[0]).help() + except CommandFailed, e: + print e + except arch.errors.ArchiveNotRegistered, e: + print e + except KeyboardInterrupt, e: + print "Interrupted" + except arch.util.ExecProblem, e: + print e.proc.error.rstrip('\n') + except cmdutil.CantDetermineVersion, e: + print e + except cmdutil.CantDetermineRevision, e: + print e + except Exception, e: + print "Unhandled error:\n%s" % cmdutil.exception_str(e) + + elif suggestions.has_key(args[0]): + print suggestions[args[0]] + + elif self.fake_aba.is_command(args[0]): + tree = None + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + pass + cmd = self.fake_aba.is_command(args[0]) + try: + cmd.run(cmdutil.expand_prefix_alias(args[1:], tree)) + except KeyboardInterrupt, e: + print "Interrupted" + + elif options.tla_fallthrough and args[0] != "rm" and \ + cmdutil.is_tla_command(args[0]): + try: + tree = None + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + pass + args = cmdutil.expand_prefix_alias(args, tree) + arch.util.exec_safe('tla', args, stderr=sys.stderr, + expected=(0, 1)) + except arch.util.ExecProblem, e: + pass + except KeyboardInterrupt, e: + print "Interrupted" + else: + try: + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + tree = None + args=line.split() + os.system(" ".join(cmdutil.expand_prefix_alias(args, tree))) + except KeyboardInterrupt, e: + print "Interrupted" + + def completenames(self, text, line, begidx, endidx): + completions = [] + iter = iter_command_names(self.fake_aba) + try: + if len(line) > 0: + arg = line.split()[-1] + else: + arg = "" + iter = iter_munged_completions(iter, arg, text) + except Exception, e: + print e + return list(iter) + + def completedefault(self, text, line, begidx, endidx): + """Perform completion for native commands. + + :param text: The text to complete + :type text: str + :param line: The entire line to complete + :type line: str + :param begidx: The start of the text in the line + :type begidx: int + :param endidx: The end of the text in the line + :type endidx: int + """ + try: + (cmd, args, foo) = self.parseline(line) + command_obj=find_command(cmd) + if command_obj is not None: + return command_obj.complete(args.split(), text) + elif not self.fake_aba.is_command(cmd) and \ + cmdutil.is_tla_command(cmd): + iter = cmdutil.iter_supported_switches(cmd) + if len(args) > 0: + arg = args.split()[-1] + else: + arg = "" + if arg.startswith("-"): + return list(iter_munged_completions(iter, arg, text)) + else: + return list(iter_munged_completions( + iter_file_completions(arg), arg, text)) + + + elif cmd == "cd": + if len(args) > 0: + arg = args.split()[-1] + else: + arg = "" + iter = iter_dir_completions(arg) + iter = iter_munged_completions(iter, arg, text) + return list(iter) + elif len(args)>0: + arg = args.split()[-1] + return list(iter_munged_completions(iter_file_completions(arg), + arg, text)) + else: + return self.completenames(text, line, begidx, endidx) + except Exception, e: + print e + + +def iter_command_names(fake_aba): + for entry in cmdutil.iter_combine([commands.iterkeys(), + fake_aba.get_commands(), + cmdutil.iter_tla_commands(False)]): + if not suggestions.has_key(str(entry)): + yield entry + + +def iter_file_completions(arg, only_dirs = False): + """Generate an iterator that iterates through filename completions. + + :param arg: The filename fragment to match + :type arg: str + :param only_dirs: If true, match only directories + :type only_dirs: bool + """ + cwd = os.getcwd() + if cwd != "/": + extras = [".", ".."] + else: + extras = [] + (dir, file) = os.path.split(arg) + if dir != "": + listingdir = os.path.expanduser(dir) + else: + listingdir = cwd + for file in cmdutil.iter_combine([os.listdir(listingdir), extras]): + if dir != "": + userfile = dir+'/'+file + else: + userfile = file + if userfile.startswith(arg): + if os.path.isdir(listingdir+'/'+file): + userfile+='/' + yield userfile + elif not only_dirs: + yield userfile + +def iter_munged_completions(iter, arg, text): + for completion in iter: + completion = str(completion) + if completion.startswith(arg): + yield completion[len(arg)-len(text):] + +def iter_source_file_completions(tree, arg): + treepath = cmdutil.tree_cwd(tree) + if len(treepath) > 0: + dirs = [treepath] + else: + dirs = None + for file in tree.iter_inventory(dirs, source=True, both=True): + file = file_completion_match(file, treepath, arg) + if file is not None: + yield file + + +def iter_untagged(tree, dirs): + for file in arch_core.iter_inventory_filter(tree, dirs, tagged=False, + categories=arch_core.non_root, + control_files=True): + yield file.name + + +def iter_untagged_completions(tree, arg): + """Generate an iterator for all visible untagged files that match arg. + + :param tree: The tree to look for untagged files in + :type tree: `arch.WorkingTree` + :param arg: The argument to match + :type arg: str + :return: An iterator of all matching untagged files + :rtype: iterator of str + """ + treepath = cmdutil.tree_cwd(tree) + if len(treepath) > 0: + dirs = [treepath] + else: + dirs = None + + for file in iter_untagged(tree, dirs): + file = file_completion_match(file, treepath, arg) + if file is not None: + yield file + + +def file_completion_match(file, treepath, arg): + """Determines whether a file within an arch tree matches the argument. + + :param file: The rooted filename + :type file: str + :param treepath: The path to the cwd within the tree + :type treepath: str + :param arg: The prefix to match + :return: The completion name, or None if not a match + :rtype: str + """ + if not file.startswith(treepath): + return None + if treepath != "": + file = file[len(treepath)+1:] + + if not file.startswith(arg): + return None + if os.path.isdir(file): + file += '/' + return file + +def iter_modified_file_completions(tree, arg): + """Returns a list of modified files that match the specified prefix. + + :param tree: The current tree + :type tree: `arch.WorkingTree` + :param arg: The prefix to match + :type arg: str + """ + treepath = cmdutil.tree_cwd(tree) + tmpdir = cmdutil.tmpdir() + changeset = tmpdir+"/changeset" + completions = [] + revision = cmdutil.determine_revision_tree(tree) + for line in arch.iter_delta(revision, tree, changeset): + if isinstance(line, arch.FileModification): + file = file_completion_match(line.name[1:], treepath, arg) + if file is not None: + completions.append(file) + shutil.rmtree(tmpdir) + return completions + +def iter_dir_completions(arg): + """Generate an iterator that iterates through directory name completions. + + :param arg: The directory name fragment to match + :type arg: str + """ + return iter_file_completions(arg, True) + +class Shell(BaseCommand): + def __init__(self): + self.description = "Runs Fai as a shell" + + def do_command(self, cmdargs): + if len(cmdargs)!=0: + raise cmdutil.GetHelp + prompt = PromptCmd() + try: + prompt.cmdloop() + finally: + prompt.write_history() + +class AddID(BaseCommand): + """ + Adds an inventory id for the given file + """ + def __init__(self): + self.description="Add an inventory id for a given file" + + def get_completer(self, arg, index): + tree = arch.tree_root() + return iter_untagged_completions(tree, arg) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + + tree = arch.tree_root() + + if (len(args) == 0) == (options.untagged == False): + raise cmdutil.GetHelp + + #if options.id and len(args) != 1: + # print "If --id is specified, only one file can be named." + # return + + method = tree.tagging_method + + if options.id_type == "tagline": + if method != "tagline": + if not cmdutil.prompt("Tagline in other tree"): + if method == "explicit": + options.id_type == explicit + else: + print "add-id not supported for \"%s\" tagging method"\ + % method + return + + elif options.id_type == "explicit": + if method != "tagline" and method != explicit: + if not prompt("Explicit in other tree"): + print "add-id not supported for \"%s\" tagging method" % \ + method + return + + if options.id_type == "auto": + if method != "tagline" and method != "explicit": + print "add-id not supported for \"%s\" tagging method" % method + return + else: + options.id_type = method + if options.untagged: + args = None + self.add_ids(tree, options.id_type, args) + + def add_ids(self, tree, id_type, files=()): + """Add inventory ids to files. + + :param tree: the tree the files are in + :type tree: `arch.WorkingTree` + :param id_type: the type of id to add: "explicit" or "tagline" + :type id_type: str + :param files: The list of files to add. If None do all untagged. + :type files: tuple of str + """ + + untagged = (files is None) + if untagged: + files = list(iter_untagged(tree, None)) + previous_files = [] + while len(files) > 0: + previous_files.extend(files) + if id_type == "explicit": + cmdutil.add_id(files) + elif id_type == "tagline": + for file in files: + try: + cmdutil.add_tagline_or_explicit_id(file) + except cmdutil.AlreadyTagged: + print "\"%s\" already has a tagline." % file + except cmdutil.NoCommentSyntax: + pass + #do inventory after tagging until no untagged files are encountered + if untagged: + files = [] + for file in iter_untagged(tree, None): + if not file in previous_files: + files.append(file) + + else: + break + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai add-id file1 [file2] [file3]...") +# ddaa suggests removing this to promote GUIDs. Let's see who squalks. +# parser.add_option("-i", "--id", dest="id", +# help="Specify id for a single file", default=None) + parser.add_option("--tltl", action="store_true", + dest="lord_style", help="Use Tom Lord's style of id.") + parser.add_option("--explicit", action="store_const", + const="explicit", dest="id_type", + help="Use an explicit id", default="auto") + parser.add_option("--tagline", action="store_const", + const="tagline", dest="id_type", + help="Use a tagline id") + parser.add_option("--untagged", action="store_true", + dest="untagged", default=False, + help="tag all untagged files") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Adds an inventory to the specified file(s) and directories. If --untagged is +specified, adds inventory to all untagged files and directories. + """ + return + + +class Merge(BaseCommand): + """ + Merges changes from other versions into the current tree + """ + def __init__(self): + self.description="Merges changes from other versions" + try: + self.tree = arch.tree_root() + except: + self.tree = None + + + def get_completer(self, arg, index): + if self.tree is None: + raise arch.errors.TreeRootError + completions = list(ancillary.iter_partners(self.tree, + self.tree.tree_version)) + if len(completions) == 0: + completions = list(self.tree.iter_log_versions()) + + aliases = [] + try: + for completion in completions: + alias = ancillary.compact_alias(str(completion), self.tree) + if alias: + aliases.extend(alias) + + for completion in completions: + if completion.archive == self.tree.tree_version.archive: + aliases.append(completion.nonarch) + + except Exception, e: + print e + + completions.extend(aliases) + return completions + + def do_command(self, cmdargs): + """ + Master function that perfoms the "merge" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + if options.diff3: + action="star-merge" + else: + action = options.action + + if self.tree is None: + raise arch.errors.TreeRootError(os.getcwd()) + if cmdutil.has_changed(self.tree.tree_version): + raise UncommittedChanges(self.tree) + + if len(args) > 0: + revisions = [] + for arg in args: + revisions.append(cmdutil.determine_revision_arch(self.tree, + arg)) + source = "from commandline" + else: + revisions = ancillary.iter_partner_revisions(self.tree, + self.tree.tree_version) + source = "from partner version" + revisions = misc.rewind_iterator(revisions) + try: + revisions.next() + revisions.rewind() + except StopIteration, e: + revision = cmdutil.tag_cur(self.tree) + if revision is None: + raise CantDetermineRevision("", "No version specified, no " + "partner-versions, and no tag" + " source") + revisions = [revision] + source = "from tag source" + for revision in revisions: + cmdutil.ensure_archive_registered(revision.archive) + cmdutil.colorize(arch.Chatter("* Merging %s [%s]" % + (revision, source))) + if action=="native-merge" or action=="update": + if self.native_merge(revision, action) == 0: + continue + elif action=="star-merge": + try: + self.star_merge(revision, options.diff3) + except errors.MergeProblem, e: + break + if cmdutil.has_changed(self.tree.tree_version): + break + + def star_merge(self, revision, diff3): + """Perform a star-merge on the current tree. + + :param revision: The revision to use for the merge + :type revision: `arch.Revision` + :param diff3: If true, do a diff3 merge + :type diff3: bool + """ + try: + for line in self.tree.iter_star_merge(revision, diff3=diff3): + cmdutil.colorize(line) + except arch.util.ExecProblem, e: + if e.proc.status is not None and e.proc.status == 1: + if e.proc.error: + print e.proc.error + raise MergeProblem + else: + raise + + def native_merge(self, other_revision, action): + """Perform a native-merge on the current tree. + + :param other_revision: The revision to use for the merge + :type other_revision: `arch.Revision` + :return: 0 if the merge was skipped, 1 if it was applied + """ + other_tree = cmdutil.find_or_make_local_revision(other_revision) + try: + if action == "native-merge": + ancestor = cmdutil.merge_ancestor2(self.tree, other_tree, + other_revision) + elif action == "update": + ancestor = cmdutil.tree_latest(self.tree, + other_revision.version) + except CantDetermineRevision, e: + raise CommandFailedWrapper(e) + cmdutil.colorize(arch.Chatter("* Found common ancestor %s" % ancestor)) + if (ancestor == other_revision): + cmdutil.colorize(arch.Chatter("* Skipping redundant merge" + % ancestor)) + return 0 + delta = cmdutil.apply_delta(ancestor, other_tree, self.tree) + for line in cmdutil.iter_apply_delta_filter(delta): + cmdutil.colorize(line) + return 1 + + + + def get_parser(self): + """ + Returns the options parser to use for the "merge" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai merge [VERSION]") + parser.add_option("-s", "--star-merge", action="store_const", + dest="action", help="Use star-merge", + const="star-merge", default="native-merge") + parser.add_option("--update", action="store_const", + dest="action", help="Use update picker", + const="update") + parser.add_option("--diff3", action="store_true", + dest="diff3", + help="Use diff3 for merge (implies star-merge)") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Performs a merge operation using the specified version. + """ + return + +class ELog(BaseCommand): + """ + Produces a raw patchlog and invokes the user's editor + """ + def __init__(self): + self.description="Edit a patchlog to commit" + try: + self.tree = arch.tree_root() + except: + self.tree = None + + + def do_command(self, cmdargs): + """ + Master function that perfoms the "elog" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + if self.tree is None: + raise arch.errors.TreeRootError + + edit_log(self.tree) + + def get_parser(self): + """ + Returns the options parser to use for the "merge" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai elog") + return parser + + + def help(self, parser=None): + """ + Invokes $EDITOR to produce a log for committing. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Invokes $EDITOR to produce a log for committing. + """ + return + +def edit_log(tree): + """Makes and edits the log for a tree. Does all kinds of fancy things + like log templates and merge summaries and log-for-merge + + :param tree: The tree to edit the log for + :type tree: `arch.WorkingTree` + """ + #ensure we have an editor before preparing the log + cmdutil.find_editor() + log = tree.log_message(create=False) + log_is_new = False + if log is None or cmdutil.prompt("Overwrite log"): + if log is not None: + os.remove(log.name) + log = tree.log_message(create=True) + log_is_new = True + tmplog = log.name + template = tree+"/{arch}/=log-template" + if not os.path.exists(template): + template = os.path.expanduser("~/.arch-params/=log-template") + if not os.path.exists(template): + template = None + if template: + shutil.copyfile(template, tmplog) + + new_merges = list(cmdutil.iter_new_merges(tree, + tree.tree_version)) + log["Summary"] = merge_summary(new_merges, tree.tree_version) + if len(new_merges) > 0: + if cmdutil.prompt("Log for merge"): + mergestuff = cmdutil.log_for_merge(tree) + log.description += mergestuff + log.save() + try: + cmdutil.invoke_editor(log.name) + except: + if log_is_new: + os.remove(log.name) + raise + +def merge_summary(new_merges, tree_version): + if len(new_merges) == 0: + return "" + if len(new_merges) == 1: + summary = new_merges[0].summary + else: + summary = "Merge" + + credits = [] + for merge in new_merges: + if arch.my_id() != merge.creator: + name = re.sub("<.*>", "", merge.creator).rstrip(" "); + if not name in credits: + credits.append(name) + else: + version = merge.revision.version + if version.archive == tree_version.archive: + if not version.nonarch in credits: + credits.append(version.nonarch) + elif not str(version) in credits: + credits.append(str(version)) + + return ("%s (%s)") % (summary, ", ".join(credits)) + +class MirrorArchive(BaseCommand): + """ + Updates a mirror from an archive + """ + def __init__(self): + self.description="Update a mirror from an archive" + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + if len(args) > 1: + raise GetHelp + try: + tree = arch.tree_root() + except: + tree = None + + if len(args) == 0: + if tree is not None: + name = tree.tree_version() + else: + name = cmdutil.expand_alias(args[0], tree) + name = arch.NameParser(name) + + to_arch = name.get_archive() + from_arch = cmdutil.get_mirror_source(arch.Archive(to_arch)) + limit = name.get_nonarch() + + iter = arch_core.mirror_archive(from_arch,to_arch, limit) + for line in arch.chatter_classifier(iter): + cmdutil.colorize(line) + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai mirror-archive ARCHIVE") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Updates a mirror from an archive. If a branch, package, or version is +supplied, only changes under it are mirrored. + """ + return + +def help_tree_spec(): + print """Specifying revisions (default: tree) +Revisions may be specified by alias, revision, version or patchlevel. +Revisions or versions may be fully qualified. Unqualified revisions, versions, +or patchlevels use the archive of the current project tree. Versions will +use the latest patchlevel in the tree. Patchlevels will use the current tree- +version. + +Use "alias" to list available (user and automatic) aliases.""" + +def help_aliases(tree): + print """Auto-generated aliases + acur : The latest revision in the archive of the tree-version. You can specfy + a different version like so: acur:foo--bar--0 (aliases can be used) + tcur : (tree current) The latest revision in the tree of the tree-version. + You can specify a different version like so: tcur:foo--bar--0 (aliases + can be used). +tprev : (tree previous) The previous revision in the tree of the tree-version. + To specify an older revision, use a number, e.g. "tprev:4" + tanc : (tree ancestor) The ancestor revision of the tree + To specify an older revision, use a number, e.g. "tanc:4" +tdate : (tree date) The latest revision from a given date (e.g. "tdate:July 6") + tmod : (tree modified) The latest revision to modify a given file + (e.g. "tmod:engine.cpp" or "tmod:engine.cpp:16") + ttag : (tree tag) The revision that was tagged into the current tree revision, + according to the tree. +tagcur: (tag current) The latest revision of the version that the current tree + was tagged from. +mergeanc : The common ancestor of the current tree and the specified revision. + Defaults to the first partner-version's latest revision or to tagcur. + """ + print "User aliases" + for parts in ancillary.iter_all_alias(tree): + print parts[0].rjust(10)+" : "+parts[1] + + +class Inventory(BaseCommand): + """List the status of files in the tree""" + def __init__(self): + self.description=self.__doc__ + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + tree = arch.tree_root() + categories = [] + + if (options.source): + categories.append(arch_core.SourceFile) + if (options.precious): + categories.append(arch_core.PreciousFile) + if (options.backup): + categories.append(arch_core.BackupFile) + if (options.junk): + categories.append(arch_core.JunkFile) + + if len(categories) == 1: + show_leading = False + else: + show_leading = True + + if len(categories) == 0: + categories = None + + if options.untagged: + categories = arch_core.non_root + show_leading = False + tagged = False + else: + tagged = None + + for file in arch_core.iter_inventory_filter(tree, None, + control_files=options.control_files, + categories = categories, tagged=tagged): + print arch_core.file_line(file, + category = show_leading, + untagged = show_leading, + id = options.ids) + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai inventory [options]") + parser.add_option("--ids", action="store_true", dest="ids", + help="Show file ids") + parser.add_option("--control", action="store_true", + dest="control_files", help="include control files") + parser.add_option("--source", action="store_true", dest="source", + help="List source files") + parser.add_option("--backup", action="store_true", dest="backup", + help="List backup files") + parser.add_option("--precious", action="store_true", dest="precious", + help="List precious files") + parser.add_option("--junk", action="store_true", dest="junk", + help="List junk files") + parser.add_option("--unrecognized", action="store_true", + dest="unrecognized", help="List unrecognized files") + parser.add_option("--untagged", action="store_true", + dest="untagged", help="List only untagged files") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Lists the status of files in the archive: +S source +P precious +B backup +J junk +U unrecognized +T tree root +? untagged-source +Leading letter are not displayed if only one kind of file is shown + """ + return + + +class Alias(BaseCommand): + """List or adjust aliases""" + def __init__(self): + self.description=self.__doc__ + + def get_completer(self, arg, index): + if index > 2: + return () + try: + self.tree = arch.tree_root() + except: + self.tree = None + + if index == 0: + return [part[0]+" " for part in ancillary.iter_all_alias(self.tree)] + elif index == 1: + return cmdutil.iter_revision_completions(arg, self.tree) + + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: + self.tree = arch.tree_root() + except: + self.tree = None + + + try: + options.action(args, options) + except cmdutil.ForbiddenAliasSyntax, e: + raise CommandFailedWrapper(e) + + def arg_dispatch(self, args, options): + """Add, modify, or list aliases, depending on number of arguments + + :param args: The list of commandline arguments + :type args: list of str + :param options: The commandline options + """ + if len(args) == 0: + help_aliases(self.tree) + return + elif len(args) == 1: + self.print_alias(args[0]) + elif (len(args)) == 2: + self.add(args[0], args[1], options) + else: + raise cmdutil.GetHelp + + def print_alias(self, alias): + answer = None + for pair in ancillary.iter_all_alias(self.tree): + if pair[0] == alias: + answer = pair[1] + if answer is not None: + print answer + else: + print "The alias %s is not assigned." % alias + + def add(self, alias, expansion, options): + """Add or modify aliases + + :param alias: The alias name to create/modify + :type alias: str + :param expansion: The expansion to assign to the alias name + :type expansion: str + :param options: The commandline options + """ + newlist = "" + written = False + new_line = "%s=%s\n" % (alias, cmdutil.expand_alias(expansion, + self.tree)) + ancillary.check_alias(new_line.rstrip("\n"), [alias, expansion]) + + for pair in self.get_iterator(options): + if pair[0] != alias: + newlist+="%s=%s\n" % (pair[0], pair[1]) + elif not written: + newlist+=new_line + written = True + if not written: + newlist+=new_line + self.write_aliases(newlist, options) + + def delete(self, args, options): + """Delete the specified alias + + :param args: The list of arguments + :type args: list of str + :param options: The commandline options + """ + deleted = False + if len(args) != 1: + raise cmdutil.GetHelp + newlist = "" + for pair in self.get_iterator(options): + if pair[0] != args[0]: + newlist+="%s=%s\n" % (pair[0], pair[1]) + else: + deleted = True + if not deleted: + raise errors.NoSuchAlias(args[0]) + self.write_aliases(newlist, options) + + def get_alias_file(self, options): + """Return the name of the alias file to use + + :param options: The commandline options + """ + if options.tree: + if self.tree is None: + self.tree == arch.tree_root() + return str(self.tree)+"/{arch}/+aliases" + else: + return "~/.aba/aliases" + + def get_iterator(self, options): + """Return the alias iterator to use + + :param options: The commandline options + """ + return ancillary.iter_alias(self.get_alias_file(options)) + + def write_aliases(self, newlist, options): + """Safely rewrite the alias file + :param newlist: The new list of aliases + :type newlist: str + :param options: The commandline options + """ + filename = os.path.expanduser(self.get_alias_file(options)) + file = cmdutil.NewFileVersion(filename) + file.write(newlist) + file.commit() + + + def get_parser(self): + """ + Returns the options parser to use for the "alias" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai alias [ALIAS] [NAME]") + parser.add_option("-d", "--delete", action="store_const", dest="action", + const=self.delete, default=self.arg_dispatch, + help="Delete an alias") + parser.add_option("--tree", action="store_true", dest="tree", + help="Create a per-tree alias", default=False) + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Lists current aliases or modifies the list of aliases. + +If no arguments are supplied, aliases will be listed. If two arguments are +supplied, the specified alias will be created or modified. If -d or --delete +is supplied, the specified alias will be deleted. + +You can create aliases that refer to any fully-qualified part of the +Arch namespace, e.g. +archive, +archive/category, +archive/category--branch, +archive/category--branch--version (my favourite) +archive/category--branch--version--patchlevel + +Aliases can be used automatically by native commands. To use them +with external or tla commands, prefix them with ^ (you can do this +with native commands, too). +""" + + +class RequestMerge(BaseCommand): + """Submit a merge request to Bug Goo""" + def __init__(self): + self.description=self.__doc__ + + def do_command(self, cmdargs): + """Submit a merge request + + :param cmdargs: The commandline arguments + :type cmdargs: list of str + """ + cmdutil.find_editor() + parser = self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: + self.tree=arch.tree_root() + except: + self.tree=None + base, revisions = self.revision_specs(args) + message = self.make_headers(base, revisions) + message += self.make_summary(revisions) + path = self.edit_message(message) + message = self.tidy_message(path) + if cmdutil.prompt("Send merge"): + self.send_message(message) + print "Merge request sent" + + def make_headers(self, base, revisions): + """Produce email and Bug Goo header strings + + :param base: The base revision to apply merges to + :type base: `arch.Revision` + :param revisions: The revisions to replay into the base + :type revisions: list of `arch.Patchlog` + :return: The headers + :rtype: str + """ + headers = "To: gnu-arch-users@gnu.org\n" + headers += "From: %s\n" % options.fromaddr + if len(revisions) == 1: + headers += "Subject: [MERGE REQUEST] %s\n" % revisions[0].summary + else: + headers += "Subject: [MERGE REQUEST]\n" + headers += "\n" + headers += "Base-Revision: %s\n" % base + for revision in revisions: + headers += "Revision: %s\n" % revision.revision + headers += "Bug: \n\n" + return headers + + def make_summary(self, logs): + """Generate a summary of merges + + :param logs: the patchlogs that were directly added by the merges + :type logs: list of `arch.Patchlog` + :return: the summary + :rtype: str + """ + summary = "" + for log in logs: + summary+=str(log.revision)+"\n" + summary+=log.summary+"\n" + if log.description.strip(): + summary+=log.description.strip('\n')+"\n\n" + return summary + + def revision_specs(self, args): + """Determine the base and merge revisions from tree and arguments. + + :param args: The parsed arguments + :type args: list of str + :return: The base revision and merge revisions + :rtype: `arch.Revision`, list of `arch.Patchlog` + """ + if len(args) > 0: + target_revision = cmdutil.determine_revision_arch(self.tree, + args[0]) + else: + target_revision = cmdutil.tree_latest(self.tree) + if len(args) > 1: + merges = [ arch.Patchlog(cmdutil.determine_revision_arch( + self.tree, f)) for f in args[1:] ] + else: + if self.tree is None: + raise CantDetermineRevision("", "Not in a project tree") + merge_iter = cmdutil.iter_new_merges(self.tree, + target_revision.version, + False) + merges = [f for f in cmdutil.direct_merges(merge_iter)] + return (target_revision, merges) + + def edit_message(self, message): + """Edit an email message in the user's standard editor + + :param message: The message to edit + :type message: str + :return: the path of the edited message + :rtype: str + """ + if self.tree is None: + path = os.get_cwd() + else: + path = self.tree + path += "/,merge-request" + file = open(path, 'w') + file.write(message) + file.flush() + cmdutil.invoke_editor(path) + return path + + def tidy_message(self, path): + """Validate and clean up message. + + :param path: The path to the message to clean up + :type path: str + :return: The parsed message + :rtype: `email.Message` + """ + mail = email.message_from_file(open(path)) + if mail["Subject"].strip() == "[MERGE REQUEST]": + raise BlandSubject + + request = email.message_from_string(mail.get_payload()) + if request.has_key("Bug"): + if request["Bug"].strip()=="": + del request["Bug"] + mail.set_payload(request.as_string()) + return mail + + def send_message(self, message): + """Send a message, using its headers to address it. + + :param message: The message to send + :type message: `email.Message`""" + server = smtplib.SMTP() + server.sendmail(message['From'], message['To'], message.as_string()) + server.quit() + + def help(self, parser=None): + """Print a usage message + + :param parser: The options parser to use + :type parser: `cmdutil.CmdOptionParser` + """ + if parser is None: + parser = self.get_parser() + parser.print_help() + print """ +Sends a merge request formatted for Bug Goo. Intended use: get the tree +you'd like to merge into. Apply the merges you want. Invoke request-merge. +The merge request will open in your $EDITOR. + +When no TARGET is specified, it uses the current tree revision. When +no MERGE is specified, it uses the direct merges (as in "revisions +--direct-merges"). But you can specify just the TARGET, or all the MERGE +revisions. +""" + + def get_parser(self): + """Produce a commandline parser for this command. + + :rtype: `cmdutil.CmdOptionParser` + """ + parser=cmdutil.CmdOptionParser("request-merge [TARGET] [MERGE1...]") + return parser + +commands = { +'changes' : Changes, +'help' : Help, +'update': Update, +'apply-changes':ApplyChanges, +'cat-log': CatLog, +'commit': Commit, +'revision': Revision, +'revisions': Revisions, +'get': Get, +'revert': Revert, +'shell': Shell, +'add-id': AddID, +'merge': Merge, +'elog': ELog, +'mirror-archive': MirrorArchive, +'ninventory': Inventory, +'alias' : Alias, +'request-merge': RequestMerge, +} +suggestions = { +'apply-delta' : "Try \"apply-changes\".", +'delta' : "To compare two revisions, use \"changes\".", +'diff-rev' : "To compare two revisions, use \"changes\".", +'undo' : "To undo local changes, use \"revert\".", +'undelete' : "To undo only deletions, use \"revert --deletions\"", +'missing-from' : "Try \"revisions --missing-from\".", +'missing' : "Try \"revisions --missing\".", +'missing-merge' : "Try \"revisions --partner-missing\".", +'new-merges' : "Try \"revisions --new-merges\".", +'cachedrevs' : "Try \"revisions --cacherevs\". (no 'd')", +'logs' : "Try \"revisions --logs\"", +'tree-source' : "Use the \"^ttag\" alias (\"revision ^ttag\")", +'latest-revision' : "Use the \"^acur\" alias (\"revision ^acur\")", +'change-version' : "Try \"update REVISION\"", +'tree-revision' : "Use the \"^tcur\" alias (\"revision ^tcur\")", +'rev-depends' : "Use revisions --dependencies", +'auto-get' : "Plain get will do archive lookups", +'tagline' : "Use add-id. It uses taglines in tagline trees", +'emlog' : "Use elog. It automatically adds log-for-merge text, if any", +'library-revisions' : "Use revisions --library", +'file-revert' : "Use revert FILE" +} +# arch-tag: 19d5739d-3708-486c-93ba-deecc3027fc7 *** modified file 'bzrlib/branch.py' --- bzrlib/branch.py +++ bzrlib/branch.py @@ -31,6 +31,8 @@ from revision import Revision from errors import bailout, BzrError from textui import show_status +import patches +from bzrlib import progress BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? @@ -802,3 +804,36 @@ s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) + + +def iter_anno_data(branch, file_id): + later_revision = branch.revno() + q = range(branch.revno()) + q.reverse() + later_text_id = branch.basis_tree().inventory[file_id].text_id + i = 0 + for revno in q: + i += 1 + cur_tree = branch.revision_tree(branch.lookup_revision(revno)) + if file_id not in cur_tree.inventory: + text_id = None + else: + text_id = cur_tree.inventory[file_id].text_id + if text_id != later_text_id: + patch = get_patch(branch, revno, later_revision, file_id) + yield revno, patch.iter_inserted(), patch + later_revision = revno + later_text_id = text_id + yield progress.Progress("revisions", i) + +def get_patch(branch, old_revno, new_revno, file_id): + old_tree = branch.revision_tree(branch.lookup_revision(old_revno)) + new_tree = branch.revision_tree(branch.lookup_revision(new_revno)) + if file_id in old_tree.inventory: + old_file = old_tree.get_file(file_id).readlines() + else: + old_file = [] + ud = difflib.unified_diff(old_file, new_tree.get_file(file_id).readlines()) + return patches.parse_patch(ud) + + *** modified file 'bzrlib/commands.py' --- bzrlib/commands.py +++ bzrlib/commands.py @@ -27,6 +27,9 @@ from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date from bzrlib import merge +from bzrlib.branch import iter_anno_data +from bzrlib import patches +from bzrlib import progress def _squish_command_name(cmd): @@ -882,7 +885,15 @@ print '%3d FAILED!' % mf else: print - + result = bzrlib.patches.test() + resultFailed = len(result.errors) + len(result.failures) + print '%-40s %3d tests' % ('bzrlib.patches', result.testsRun), + if resultFailed: + print '%3d FAILED!' % resultFailed + else: + print + tests += result.testsRun + failures += resultFailed print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures @@ -897,6 +908,27 @@ """Show version of bzr""" def run(self): show_version() + +class cmd_annotate(Command): + """Show which revision added each line in a file""" + + takes_args = ['filename'] + def run(self, filename): + branch = (Branch(filename)) + file_id = branch.working_tree().path2id(filename) + lines = branch.basis_tree().get_file(file_id) + total = branch.revno() + anno_d_iter = iter_anno_data(branch, file_id) + for result in patches.iter_annotate_file(lines, anno_d_iter): + if isinstance(result, progress.Progress): + result.total = total + progress.progress_bar(result) + else: + progress.clear_progress_bar() + anno_lines = result + for line in anno_lines: + sys.stdout.write("%4s:%s" % (str(line.log), line.text)) + def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ commit refs/heads/master mark :646 committer Martin Pool 1118385137 +1000 data 33 - add updated annotate from aaron from :645 M 644 inline patches/annotate4.patch data 274203 *** added file 'bzrlib/patches.py' --- /dev/null +++ bzrlib/patches.py @@ -0,0 +1,497 @@ +# Copyright (C) 2004, 2005 Aaron Bentley +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +import sys +import progress +class PatchSyntax(Exception): + def __init__(self, msg): + Exception.__init__(self, msg) + + +class MalformedPatchHeader(PatchSyntax): + def __init__(self, desc, line): + self.desc = desc + self.line = line + msg = "Malformed patch header. %s\n%s" % (self.desc, self.line) + PatchSyntax.__init__(self, msg) + +class MalformedHunkHeader(PatchSyntax): + def __init__(self, desc, line): + self.desc = desc + self.line = line + msg = "Malformed hunk header. %s\n%s" % (self.desc, self.line) + PatchSyntax.__init__(self, msg) + +class MalformedLine(PatchSyntax): + def __init__(self, desc, line): + self.desc = desc + self.line = line + msg = "Malformed line. %s\n%s" % (self.desc, self.line) + PatchSyntax.__init__(self, msg) + +def get_patch_names(iter_lines): + try: + line = iter_lines.next() + if not line.startswith("--- "): + raise MalformedPatchHeader("No orig name", line) + else: + orig_name = line[4:].rstrip("\n") + except StopIteration: + raise MalformedPatchHeader("No orig line", "") + try: + line = iter_lines.next() + if not line.startswith("+++ "): + raise PatchSyntax("No mod name") + else: + mod_name = line[4:].rstrip("\n") + except StopIteration: + raise MalformedPatchHeader("No mod line", "") + return (orig_name, mod_name) + +def parse_range(textrange): + """Parse a patch range, handling the "1" special-case + + :param textrange: The text to parse + :type textrange: str + :return: the position and range, as a tuple + :rtype: (int, int) + """ + tmp = textrange.split(',') + if len(tmp) == 1: + pos = tmp[0] + range = "1" + else: + (pos, range) = tmp + pos = int(pos) + range = int(range) + return (pos, range) + + +def hunk_from_header(line): + if not line.startswith("@@") or not line.endswith("@@\n") \ + or not len(line) > 4: + raise MalformedHunkHeader("Does not start and end with @@.", line) + try: + (orig, mod) = line[3:-4].split(" ") + except Exception, e: + raise MalformedHunkHeader(str(e), line) + if not orig.startswith('-') or not mod.startswith('+'): + raise MalformedHunkHeader("Positions don't start with + or -.", line) + try: + (orig_pos, orig_range) = parse_range(orig[1:]) + (mod_pos, mod_range) = parse_range(mod[1:]) + except Exception, e: + raise MalformedHunkHeader(str(e), line) + if mod_range < 0 or orig_range < 0: + raise MalformedHunkHeader("Hunk range is negative", line) + return Hunk(orig_pos, orig_range, mod_pos, mod_range) + + +class HunkLine: + def __init__(self, contents): + self.contents = contents + + def get_str(self, leadchar): + if self.contents == "\n" and leadchar == " " and False: + return "\n" + return leadchar + self.contents + +class ContextLine(HunkLine): + def __init__(self, contents): + HunkLine.__init__(self, contents) + + def __str__(self): + return self.get_str(" ") + + +class InsertLine(HunkLine): + def __init__(self, contents): + HunkLine.__init__(self, contents) + + def __str__(self): + return self.get_str("+") + + +class RemoveLine(HunkLine): + def __init__(self, contents): + HunkLine.__init__(self, contents) + + def __str__(self): + return self.get_str("-") + +__pychecker__="no-returnvalues" +def parse_line(line): + if line.startswith("\n"): + return ContextLine(line) + elif line.startswith(" "): + return ContextLine(line[1:]) + elif line.startswith("+"): + return InsertLine(line[1:]) + elif line.startswith("-"): + return RemoveLine(line[1:]) + else: + raise MalformedLine("Unknown line type", line) +__pychecker__="" + + +class Hunk: + def __init__(self, orig_pos, orig_range, mod_pos, mod_range): + self.orig_pos = orig_pos + self.orig_range = orig_range + self.mod_pos = mod_pos + self.mod_range = mod_range + self.lines = [] + + def get_header(self): + return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos, + self.orig_range), + self.range_str(self.mod_pos, + self.mod_range)) + + def range_str(self, pos, range): + """Return a file range, special-casing for 1-line files. + + :param pos: The position in the file + :type pos: int + :range: The range in the file + :type range: int + :return: a string in the format 1,4 except when range == pos == 1 + """ + if range == 1: + return "%i" % pos + else: + return "%i,%i" % (pos, range) + + def __str__(self): + lines = [self.get_header()] + for line in self.lines: + lines.append(str(line)) + return "".join(lines) + + def shift_to_mod(self, pos): + if pos < self.orig_pos-1: + return 0 + elif pos > self.orig_pos+self.orig_range: + return self.mod_range - self.orig_range + else: + return self.shift_to_mod_lines(pos) + + def shift_to_mod_lines(self, pos): + assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range) + position = self.orig_pos-1 + shift = 0 + for line in self.lines: + if isinstance(line, InsertLine): + shift += 1 + elif isinstance(line, RemoveLine): + if position == pos: + return None + shift -= 1 + position += 1 + elif isinstance(line, ContextLine): + position += 1 + if position > pos: + break + return shift + +def iter_hunks(iter_lines): + hunk = None + for line in iter_lines: + if line.startswith("@@"): + if hunk is not None: + yield hunk + hunk = hunk_from_header(line) + else: + hunk.lines.append(parse_line(line)) + + if hunk is not None: + yield hunk + +class Patch: + def __init__(self, oldname, newname): + self.oldname = oldname + self.newname = newname + self.hunks = [] + + def __str__(self): + ret = "--- %s\n+++ %s\n" % (self.oldname, self.newname) + ret += "".join([str(h) for h in self.hunks]) + return ret + + def stats_str(self): + """Return a string of patch statistics""" + removes = 0 + inserts = 0 + for hunk in self.hunks: + for line in hunk.lines: + if isinstance(line, InsertLine): + inserts+=1; + elif isinstance(line, RemoveLine): + removes+=1; + return "%i inserts, %i removes in %i hunks" % \ + (inserts, removes, len(self.hunks)) + + def pos_in_mod(self, position): + newpos = position + for hunk in self.hunks: + shift = hunk.shift_to_mod(position) + if shift is None: + return None + newpos += shift + return newpos + + def iter_inserted(self): + """Iteraties through inserted lines + + :return: Pair of line number, line + :rtype: iterator of (int, InsertLine) + """ + for hunk in self.hunks: + pos = hunk.mod_pos - 1; + for line in hunk.lines: + if isinstance(line, InsertLine): + yield (pos, line) + pos += 1 + if isinstance(line, ContextLine): + pos += 1 + +def parse_patch(iter_lines): + (orig_name, mod_name) = get_patch_names(iter_lines) + patch = Patch(orig_name, mod_name) + for hunk in iter_hunks(iter_lines): + patch.hunks.append(hunk) + return patch + + +class AnnotateLine: + """A line associated with the log that produced it""" + def __init__(self, text, log=None): + self.text = text + self.log = log + +class CantGetRevisionData(Exception): + def __init__(self, revision): + Exception.__init__(self, "Can't get data for revision %s" % revision) + +def annotate_file2(file_lines, anno_iter): + for result in iter_annotate_file(file_lines, anno_iter): + pass + return result + + +def iter_annotate_file(file_lines, anno_iter): + lines = [AnnotateLine(f) for f in file_lines] + patches = [] + try: + for result in anno_iter: + if isinstance(result, progress.Progress): + yield result + continue + log, iter_inserted, patch = result + for (num, line) in iter_inserted: + old_num = num + + for cur_patch in patches: + num = cur_patch.pos_in_mod(num) + if num == None: + break + + if num >= len(lines): + continue + if num is not None and lines[num].log is None: + lines[num].log = log + patches=[patch]+patches + except CantGetRevisionData: + pass + yield lines + + +def difference_index(atext, btext): + """Find the indext of the first character that differs betweeen two texts + + :param atext: The first text + :type atext: str + :param btext: The second text + :type str: str + :return: The index, or None if there are no differences within the range + :rtype: int or NoneType + """ + length = len(atext) + if len(btext) < length: + length = len(btext) + for i in range(length): + if atext[i] != btext[i]: + return i; + return None + + +def test(): + import unittest + class PatchesTester(unittest.TestCase): + def testValidPatchHeader(self): + """Parse a valid patch header""" + lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n') + (orig, mod) = get_patch_names(lines.__iter__()) + assert(orig == "orig/commands.py") + assert(mod == "mod/dommands.py") + + def testInvalidPatchHeader(self): + """Parse an invalid patch header""" + lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n') + self.assertRaises(MalformedPatchHeader, get_patch_names, + lines.__iter__()) + + def testValidHunkHeader(self): + """Parse a valid hunk header""" + header = "@@ -34,11 +50,6 @@\n" + hunk = hunk_from_header(header); + assert (hunk.orig_pos == 34) + assert (hunk.orig_range == 11) + assert (hunk.mod_pos == 50) + assert (hunk.mod_range == 6) + assert (str(hunk) == header) + + def testValidHunkHeader2(self): + """Parse a tricky, valid hunk header""" + header = "@@ -1 +0,0 @@\n" + hunk = hunk_from_header(header); + assert (hunk.orig_pos == 1) + assert (hunk.orig_range == 1) + assert (hunk.mod_pos == 0) + assert (hunk.mod_range == 0) + assert (str(hunk) == header) + + def makeMalformed(self, header): + self.assertRaises(MalformedHunkHeader, hunk_from_header, header) + + def testInvalidHeader(self): + """Parse an invalid hunk header""" + self.makeMalformed(" -34,11 +50,6 \n") + self.makeMalformed("@@ +50,6 -34,11 @@\n") + self.makeMalformed("@@ -34,11 +50,6 @@") + self.makeMalformed("@@ -34.5,11 +50,6 @@\n") + self.makeMalformed("@@-34,11 +50,6@@\n") + self.makeMalformed("@@ 34,11 50,6 @@\n") + self.makeMalformed("@@ -34,11 @@\n") + self.makeMalformed("@@ -34,11 +50,6.5 @@\n") + self.makeMalformed("@@ -34,11 +50,-6 @@\n") + + def lineThing(self,text, type): + line = parse_line(text) + assert(isinstance(line, type)) + assert(str(line)==text) + + def makeMalformedLine(self, text): + self.assertRaises(MalformedLine, parse_line, text) + + def testValidLine(self): + """Parse a valid hunk line""" + self.lineThing(" hello\n", ContextLine) + self.lineThing("+hello\n", InsertLine) + self.lineThing("-hello\n", RemoveLine) + + def testMalformedLine(self): + """Parse invalid valid hunk lines""" + self.makeMalformedLine("hello\n") + + def compare_parsed(self, patchtext): + lines = patchtext.splitlines(True) + patch = parse_patch(lines.__iter__()) + pstr = str(patch) + i = difference_index(patchtext, pstr) + if i is not None: + print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i]) + assert (patchtext == str(patch)) + + def testAll(self): + """Test parsing a whole patch""" + patchtext = """--- orig/commands.py ++++ mod/commands.py +@@ -1337,7 +1337,8 @@ + + def set_title(self, command=None): + try: +- version = self.tree.tree_version.nonarch ++ version = pylon.alias_or_version(self.tree.tree_version, self.tree, ++ full=False) + except: + version = "[no version]" + if command is None: +@@ -1983,7 +1984,11 @@ + version) + if len(new_merges) > 0: + if cmdutil.prompt("Log for merge"): +- mergestuff = cmdutil.log_for_merge(tree, comp_version) ++ if cmdutil.prompt("changelog for merge"): ++ mergestuff = "Patches applied:\\n" ++ mergestuff += pylon.changelog_for_merge(new_merges) ++ else: ++ mergestuff = cmdutil.log_for_merge(tree, comp_version) + log.description += mergestuff + log.save() + try: +""" + self.compare_parsed(patchtext) + + def testInit(self): + """Handle patches missing half the position, range tuple""" + patchtext = \ +"""--- orig/__init__.py ++++ mod/__init__.py +@@ -1 +1,2 @@ + __docformat__ = "restructuredtext en" ++__doc__ = An alternate Arch commandline interface""" + self.compare_parsed(patchtext) + + + + def testLineLookup(self): + """Make sure we can accurately look up mod line from orig""" + patch = parse_patch(open("testdata/diff")) + orig = list(open("testdata/orig")) + mod = list(open("testdata/mod")) + removals = [] + for i in range(len(orig)): + mod_pos = patch.pos_in_mod(i) + if mod_pos is None: + removals.append(orig[i]) + continue + assert(mod[mod_pos]==orig[i]) + rem_iter = removals.__iter__() + for hunk in patch.hunks: + for line in hunk.lines: + if isinstance(line, RemoveLine): + next = rem_iter.next() + if line.contents != next: + sys.stdout.write(" orig:%spatch:%s" % (next, + line.contents)) + assert(line.contents == next) + self.assertRaises(StopIteration, rem_iter.next) + + def testFirstLineRenumber(self): + """Make sure we handle lines at the beginning of the hunk""" + patch = parse_patch(open("testdata/insert_top.patch")) + assert (patch.pos_in_mod(0)==1) + + + patchesTestSuite = unittest.makeSuite(PatchesTester,'test') + runner = unittest.TextTestRunner(verbosity=0) + return runner.run(patchesTestSuite) + + +if __name__ == "__main__": + test() +# arch-tag: d1541a25-eac5-4de9-a476-08a7cecd5683 *** added file 'bzrlib/progress.py' --- /dev/null +++ bzrlib/progress.py @@ -0,0 +1,138 @@ +# Copyright (C) 2005 Aaron Bentley +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import sys +import datetime + +class Progress(object): + def __init__(self, units, current, total=None): + self.units = units + self.current = current + self.total = total + + def _get_percent(self): + if self.total is not None and self.current is not None: + return 100.0 * self.current / self.total + + percent = property(_get_percent) + + def __str__(self): + if self.total is not None: + return "%i of %i %s %.1f%%" % (self.current, self.total, self.units, + self.percent) + else: + return "%i %s" (self.current, self.units) + +class ProgressBar(object): + def __init__(self): + self.start = None + object.__init__(self) + + def __call__(self, progress): + if self.start is None: + self.start = datetime.datetime.now() + progress_bar(progress, start_time=self.start) + +def divide_timedelta(delt, divisor): + """Divides a timedelta object""" + return datetime.timedelta(float(delt.days)/divisor, + float(delt.seconds)/divisor, + float(delt.microseconds)/divisor) + +def str_tdelta(delt): + if delt is None: + return "-:--:--" + return str(datetime.timedelta(delt.days, delt.seconds)) + +def get_eta(start_time, progress, enough_samples=20): + if start_time is None or progress.current == 0: + return None + elif progress.current < enough_samples: + return None + elapsed = datetime.datetime.now() - start_time + total_duration = divide_timedelta((elapsed) * long(progress.total), + progress.current) + if elapsed < total_duration: + eta = total_duration - elapsed + else: + eta = total_duration - total_duration + return eta + +def progress_bar(progress, start_time=None): + eta = get_eta(start_time, progress) + if start_time is not None: + eta_str = " "+str_tdelta(eta) + else: + eta_str = "" + + fmt = " %i of %i %s (%.1f%%)" + f = fmt % (progress.total, progress.total, progress.units, 100.0) + max = len(f) + cols = 77 - max + if start_time is not None: + cols -= len(eta_str) + markers = int (float(cols) * progress.current / progress.total) + txt = fmt % (progress.current, progress.total, progress.units, + progress.percent) + sys.stderr.write("\r[%s%s]%s%s" % ('='*markers, ' '*(cols-markers), txt, + eta_str)) + +def clear_progress_bar(): + sys.stderr.write('\r%s\r' % (' '*79)) + +def spinner_str(progress, show_text=False): + """ + Produces the string for a textual "spinner" progress indicator + :param progress: an object represinting current progress + :param show_text: If true, show progress text as well + :return: The spinner string + + >>> spinner_str(Progress("baloons", 0)) + '|' + >>> spinner_str(Progress("baloons", 5)) + '/' + >>> spinner_str(Progress("baloons", 6), show_text=True) + '- 6 baloons' + """ + positions = ('|', '/', '-', '\\') + text = positions[progress.current % 4] + if show_text: + text+=" %i %s" % (progress.current, progress.units) + return text + +def spinner(progress, show_text=False, output=sys.stderr): + """ + Update a spinner progress indicator on an output + :param progress: The progress to display + :param show_text: If true, show text as well as spinner + :param output: The output to write to + + >>> spinner(Progress("baloons", 6), show_text=True, output=sys.stdout) + \r- 6 baloons + """ + output.write('\r%s' % spinner_str(progress, show_text)) + +def run_tests(): + import doctest + result = doctest.testmod() + if result[1] > 0: + if result[0] == 0: + print "All tests passed" + else: + print "No tests to run" +if __name__ == "__main__": + run_tests() *** added directory 'testdata' *** added file 'testdata/diff' --- /dev/null +++ testdata/diff @@ -0,0 +1,1154 @@ +--- orig/commands.py ++++ mod/commands.py +@@ -19,25 +19,31 @@ + import arch + import arch.util + import arch.arch ++ ++import pylon.errors ++from pylon.errors import * ++from pylon import errors ++from pylon import util ++from pylon import arch_core ++from pylon import arch_compound ++from pylon import ancillary ++from pylon import misc ++from pylon import paths ++ + import abacmds + import cmdutil + import shutil + import os + import options +-import paths + import time + import cmd + import readline + import re + import string +-import arch_core +-from errors import * +-import errors + import terminal +-import ancillary +-import misc + import email + import smtplib ++import textwrap + + __docformat__ = "restructuredtext" + __doc__ = "Implementation of user (sub) commands" +@@ -257,7 +263,7 @@ + + tree=arch.tree_root() + if len(args) == 0: +- a_spec = cmdutil.comp_revision(tree) ++ a_spec = ancillary.comp_revision(tree) + else: + a_spec = cmdutil.determine_revision_tree(tree, args[0]) + cmdutil.ensure_archive_registered(a_spec.archive) +@@ -284,7 +290,7 @@ + changeset=options.changeset + tmpdir = None + else: +- tmpdir=cmdutil.tmpdir() ++ tmpdir=util.tmpdir() + changeset=tmpdir+"/changeset" + try: + delta=arch.iter_delta(a_spec, b_spec, changeset) +@@ -304,14 +310,14 @@ + if status > 1: + return + if (options.perform_diff): +- chan = cmdutil.ChangesetMunger(changeset) ++ chan = arch_compound.ChangesetMunger(changeset) + chan.read_indices() +- if isinstance(b_spec, arch.Revision): +- b_dir = b_spec.library_find() +- else: +- b_dir = b_spec +- a_dir = a_spec.library_find() + if options.diffopts is not None: ++ if isinstance(b_spec, arch.Revision): ++ b_dir = b_spec.library_find() ++ else: ++ b_dir = b_spec ++ a_dir = a_spec.library_find() + diffopts = options.diffopts.split() + cmdutil.show_custom_diffs(chan, diffopts, a_dir, b_dir) + else: +@@ -517,7 +523,7 @@ + except arch.errors.TreeRootError, e: + print e + return +- from_revision=cmdutil.tree_latest(tree) ++ from_revision = arch_compound.tree_latest(tree) + if from_revision==to_revision: + print "Tree is already up to date with:\n"+str(to_revision)+"." + return +@@ -592,6 +598,9 @@ + + if len(args) == 0: + args = None ++ if options.version is None: ++ return options, tree.tree_version, args ++ + revision=cmdutil.determine_revision_arch(tree, options.version) + return options, revision.get_version(), args + +@@ -601,11 +610,16 @@ + """ + tree=arch.tree_root() + options, version, files = self.parse_commandline(cmdargs, tree) ++ ancestor = None + if options.__dict__.has_key("base") and options.base: + base = cmdutil.determine_revision_tree(tree, options.base) ++ ancestor = base + else: +- base = cmdutil.submit_revision(tree) +- ++ base = ancillary.submit_revision(tree) ++ ancestor = base ++ if ancestor is None: ++ ancestor = arch_compound.tree_latest(tree, version) ++ + writeversion=version + archive=version.archive + source=cmdutil.get_mirror_source(archive) +@@ -625,18 +639,26 @@ + try: + last_revision=tree.iter_logs(version, True).next().revision + except StopIteration, e: +- if cmdutil.prompt("Import from commit"): +- return do_import(version) +- else: +- raise NoVersionLogs(version) +- if last_revision!=version.iter_revisions(True).next(): ++ last_revision = None ++ if ancestor is None: ++ if cmdutil.prompt("Import from commit"): ++ return do_import(version) ++ else: ++ raise NoVersionLogs(version) ++ try: ++ arch_last_revision = version.iter_revisions(True).next() ++ except StopIteration, e: ++ arch_last_revision = None ++ ++ if last_revision != arch_last_revision: ++ print "Tree is not up to date with %s" % str(version) + if not cmdutil.prompt("Out of date"): + raise OutOfDate + else: + allow_old=True + + try: +- if not cmdutil.has_changed(version): ++ if not cmdutil.has_changed(ancestor): + if not cmdutil.prompt("Empty commit"): + raise EmptyCommit + except arch.util.ExecProblem, e: +@@ -645,15 +667,15 @@ + raise MissingID(e) + else: + raise +- log = tree.log_message(create=False) ++ log = tree.log_message(create=False, version=version) + if log is None: + try: + if cmdutil.prompt("Create log"): +- edit_log(tree) ++ edit_log(tree, version) + + except cmdutil.NoEditorSpecified, e: + raise CommandFailed(e) +- log = tree.log_message(create=False) ++ log = tree.log_message(create=False, version=version) + if log is None: + raise NoLogMessage + if log["Summary"] is None or len(log["Summary"].strip()) == 0: +@@ -837,23 +859,24 @@ + if spec is not None: + revision = cmdutil.determine_revision_tree(tree, spec) + else: +- revision = cmdutil.comp_revision(tree) ++ revision = ancillary.comp_revision(tree) + except cmdutil.CantDetermineRevision, e: + raise CommandFailedWrapper(e) + munger = None + + if options.file_contents or options.file_perms or options.deletions\ + or options.additions or options.renames or options.hunk_prompt: +- munger = cmdutil.MungeOpts() +- munger.hunk_prompt = options.hunk_prompt ++ munger = arch_compound.MungeOpts() ++ munger.set_hunk_prompt(cmdutil.colorize, cmdutil.user_hunk_confirm, ++ options.hunk_prompt) + + if len(args) > 0 or options.logs or options.pattern_files or \ + options.control: + if munger is None: +- munger = cmdutil.MungeOpts(True) ++ munger = cmdutil.arch_compound.MungeOpts(True) + munger.all_types(True) + if len(args) > 0: +- t_cwd = cmdutil.tree_cwd(tree) ++ t_cwd = arch_compound.tree_cwd(tree) + for name in args: + if len(t_cwd) > 0: + t_cwd += "/" +@@ -878,7 +901,7 @@ + if options.pattern_files: + munger.add_keep_pattern(options.pattern_files) + +- for line in cmdutil.revert(tree, revision, munger, ++ for line in arch_compound.revert(tree, revision, munger, + not options.no_output): + cmdutil.colorize(line) + +@@ -1042,18 +1065,13 @@ + help_tree_spec() + return + +-def require_version_exists(version, spec): +- if not version.exists(): +- raise cmdutil.CantDetermineVersion(spec, +- "The version %s does not exist." \ +- % version) +- + class Revisions(BaseCommand): + """ + Print a revision name based on a revision specifier + """ + def __init__(self): + self.description="Lists revisions" ++ self.cl_revisions = [] + + def do_command(self, cmdargs): + """ +@@ -1066,224 +1084,68 @@ + self.tree = arch.tree_root() + except arch.errors.TreeRootError: + self.tree = None ++ if options.type == "default": ++ options.type = "archive" + try: +- iter = self.get_iterator(options.type, args, options.reverse, +- options.modified) ++ iter = cmdutil.revision_iterator(self.tree, options.type, args, ++ options.reverse, options.modified, ++ options.shallow) + except cmdutil.CantDetermineRevision, e: + raise CommandFailedWrapper(e) +- ++ except cmdutil.CantDetermineVersion, e: ++ raise CommandFailedWrapper(e) + if options.skip is not None: + iter = cmdutil.iter_skip(iter, int(options.skip)) + +- for revision in iter: +- log = None +- if isinstance(revision, arch.Patchlog): +- log = revision +- revision=revision.revision +- print options.display(revision) +- if log is None and (options.summary or options.creator or +- options.date or options.merges): +- log = revision.patchlog +- if options.creator: +- print " %s" % log.creator +- if options.date: +- print " %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date) +- if options.summary: +- print " %s" % log.summary +- if options.merges: +- showed_title = False +- for revision in log.merged_patches: +- if not showed_title: +- print " Merged:" +- showed_title = True +- print " %s" % revision +- +- def get_iterator(self, type, args, reverse, modified): +- if len(args) > 0: +- spec = args[0] +- else: +- spec = None +- if modified is not None: +- iter = cmdutil.modified_iter(modified, self.tree) +- if reverse: +- return iter +- else: +- return cmdutil.iter_reverse(iter) +- elif type == "archive": +- if spec is None: +- if self.tree is None: +- raise cmdutil.CantDetermineRevision("", +- "Not in a project tree") +- version = cmdutil.determine_version_tree(spec, self.tree) +- else: +- version = cmdutil.determine_version_arch(spec, self.tree) +- cmdutil.ensure_archive_registered(version.archive) +- require_version_exists(version, spec) +- return version.iter_revisions(reverse) +- elif type == "cacherevs": +- if spec is None: +- if self.tree is None: +- raise cmdutil.CantDetermineRevision("", +- "Not in a project tree") +- version = cmdutil.determine_version_tree(spec, self.tree) +- else: +- version = cmdutil.determine_version_arch(spec, self.tree) +- cmdutil.ensure_archive_registered(version.archive) +- require_version_exists(version, spec) +- return cmdutil.iter_cacherevs(version, reverse) +- elif type == "library": +- if spec is None: +- if self.tree is None: +- raise cmdutil.CantDetermineRevision("", +- "Not in a project tree") +- version = cmdutil.determine_version_tree(spec, self.tree) +- else: +- version = cmdutil.determine_version_arch(spec, self.tree) +- return version.iter_library_revisions(reverse) +- elif type == "logs": +- if self.tree is None: +- raise cmdutil.CantDetermineRevision("", "Not in a project tree") +- return self.tree.iter_logs(cmdutil.determine_version_tree(spec, \ +- self.tree), reverse) +- elif type == "missing" or type == "skip-present": +- if self.tree is None: +- raise cmdutil.CantDetermineRevision("", "Not in a project tree") +- skip = (type == "skip-present") +- version = cmdutil.determine_version_tree(spec, self.tree) +- cmdutil.ensure_archive_registered(version.archive) +- require_version_exists(version, spec) +- return cmdutil.iter_missing(self.tree, version, reverse, +- skip_present=skip) +- +- elif type == "present": +- if self.tree is None: +- raise cmdutil.CantDetermineRevision("", "Not in a project tree") +- version = cmdutil.determine_version_tree(spec, self.tree) +- cmdutil.ensure_archive_registered(version.archive) +- require_version_exists(version, spec) +- return cmdutil.iter_present(self.tree, version, reverse) +- +- elif type == "new-merges" or type == "direct-merges": +- if self.tree is None: +- raise cmdutil.CantDetermineRevision("", "Not in a project tree") +- version = cmdutil.determine_version_tree(spec, self.tree) +- cmdutil.ensure_archive_registered(version.archive) +- require_version_exists(version, spec) +- iter = cmdutil.iter_new_merges(self.tree, version, reverse) +- if type == "new-merges": +- return iter +- elif type == "direct-merges": +- return cmdutil.direct_merges(iter) +- +- elif type == "missing-from": +- if self.tree is None: +- raise cmdutil.CantDetermineRevision("", "Not in a project tree") +- revision = cmdutil.determine_revision_tree(self.tree, spec) +- libtree = cmdutil.find_or_make_local_revision(revision) +- return cmdutil.iter_missing(libtree, self.tree.tree_version, +- reverse) +- +- elif type == "partner-missing": +- return cmdutil.iter_partner_missing(self.tree, reverse) +- +- elif type == "ancestry": +- revision = cmdutil.determine_revision_tree(self.tree, spec) +- iter = cmdutil._iter_ancestry(self.tree, revision) +- if reverse: +- return iter +- else: +- return cmdutil.iter_reverse(iter) +- +- elif type == "dependencies" or type == "non-dependencies": +- nondeps = (type == "non-dependencies") +- revision = cmdutil.determine_revision_tree(self.tree, spec) +- anc_iter = cmdutil._iter_ancestry(self.tree, revision) +- iter_depends = cmdutil.iter_depends(anc_iter, nondeps) +- if reverse: +- return iter_depends +- else: +- return cmdutil.iter_reverse(iter_depends) +- elif type == "micro": +- return cmdutil.iter_micro(self.tree) +- +- ++ try: ++ for revision in iter: ++ log = None ++ if isinstance(revision, arch.Patchlog): ++ log = revision ++ revision=revision.revision ++ out = options.display(revision) ++ if out is not None: ++ print out ++ if log is None and (options.summary or options.creator or ++ options.date or options.merges): ++ log = revision.patchlog ++ if options.creator: ++ print " %s" % log.creator ++ if options.date: ++ print " %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date) ++ if options.summary: ++ print " %s" % log.summary ++ if options.merges: ++ showed_title = False ++ for revision in log.merged_patches: ++ if not showed_title: ++ print " Merged:" ++ showed_title = True ++ print " %s" % revision ++ if len(self.cl_revisions) > 0: ++ print pylon.changelog_for_merge(self.cl_revisions) ++ except pylon.errors.TreeRootNone: ++ raise CommandFailedWrapper( ++ Exception("This option can only be used in a project tree.")) ++ ++ def changelog_append(self, revision): ++ if isinstance(revision, arch.Revision): ++ revision=arch.Patchlog(revision) ++ self.cl_revisions.append(revision) ++ + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ +- parser=cmdutil.CmdOptionParser("fai revisions [revision]") ++ parser=cmdutil.CmdOptionParser("fai revisions [version/revision]") + select = cmdutil.OptionGroup(parser, "Selection options", + "Control which revisions are listed. These options" + " are mutually exclusive. If more than one is" + " specified, the last is used.") +- select.add_option("", "--archive", action="store_const", +- const="archive", dest="type", default="archive", +- help="List all revisions in the archive") +- select.add_option("", "--cacherevs", action="store_const", +- const="cacherevs", dest="type", +- help="List all revisions stored in the archive as " +- "complete copies") +- select.add_option("", "--logs", action="store_const", +- const="logs", dest="type", +- help="List revisions that have a patchlog in the " +- "tree") +- select.add_option("", "--missing", action="store_const", +- const="missing", dest="type", +- help="List revisions from the specified version that" +- " have no patchlog in the tree") +- select.add_option("", "--skip-present", action="store_const", +- const="skip-present", dest="type", +- help="List revisions from the specified version that" +- " have no patchlogs at all in the tree") +- select.add_option("", "--present", action="store_const", +- const="present", dest="type", +- help="List revisions from the specified version that" +- " have no patchlog in the tree, but can't be merged") +- select.add_option("", "--missing-from", action="store_const", +- const="missing-from", dest="type", +- help="List revisions from the specified revision " +- "that have no patchlog for the tree version") +- select.add_option("", "--partner-missing", action="store_const", +- const="partner-missing", dest="type", +- help="List revisions in partner versions that are" +- " missing") +- select.add_option("", "--new-merges", action="store_const", +- const="new-merges", dest="type", +- help="List revisions that have had patchlogs added" +- " to the tree since the last commit") +- select.add_option("", "--direct-merges", action="store_const", +- const="direct-merges", dest="type", +- help="List revisions that have been directly added" +- " to tree since the last commit ") +- select.add_option("", "--library", action="store_const", +- const="library", dest="type", +- help="List revisions in the revision library") +- select.add_option("", "--ancestry", action="store_const", +- const="ancestry", dest="type", +- help="List revisions that are ancestors of the " +- "current tree version") +- +- select.add_option("", "--dependencies", action="store_const", +- const="dependencies", dest="type", +- help="List revisions that the given revision " +- "depends on") +- +- select.add_option("", "--non-dependencies", action="store_const", +- const="non-dependencies", dest="type", +- help="List revisions that the given revision " +- "does not depend on") +- +- select.add_option("--micro", action="store_const", +- const="micro", dest="type", +- help="List partner revisions aimed for this " +- "micro-branch") +- +- select.add_option("", "--modified", dest="modified", +- help="List tree ancestor revisions that modified a " +- "given file", metavar="FILE[:LINE]") + ++ cmdutil.add_revision_iter_options(select) + parser.add_option("", "--skip", dest="skip", + help="Skip revisions. Positive numbers skip from " + "beginning, negative skip from end.", +@@ -1312,6 +1174,9 @@ + format.add_option("--cacherev", action="store_const", + const=paths.determine_cacherev_path, dest="display", + help="Show location of cacherev file") ++ format.add_option("--changelog", action="store_const", ++ const=self.changelog_append, dest="display", ++ help="Show location of cacherev file") + parser.add_option_group(format) + display = cmdutil.OptionGroup(parser, "Display format options", + "These control the display of data") +@@ -1448,6 +1313,7 @@ + if os.access(self.history_file, os.R_OK) and \ + os.path.isfile(self.history_file): + readline.read_history_file(self.history_file) ++ self.cwd = os.getcwd() + + def write_history(self): + readline.write_history_file(self.history_file) +@@ -1470,16 +1336,21 @@ + def set_prompt(self): + if self.tree is not None: + try: +- version = " "+self.tree.tree_version.nonarch ++ prompt = pylon.alias_or_version(self.tree.tree_version, ++ self.tree, ++ full=False) ++ if prompt is not None: ++ prompt = " " + prompt + except: +- version = "" ++ prompt = "" + else: +- version = "" +- self.prompt = "Fai%s> " % version ++ prompt = "" ++ self.prompt = "Fai%s> " % prompt + + def set_title(self, command=None): + try: +- version = self.tree.tree_version.nonarch ++ version = pylon.alias_or_version(self.tree.tree_version, self.tree, ++ full=False) + except: + version = "[no version]" + if command is None: +@@ -1489,8 +1360,15 @@ + def do_cd(self, line): + if line == "": + line = "~" ++ line = os.path.expanduser(line) ++ if os.path.isabs(line): ++ newcwd = line ++ else: ++ newcwd = self.cwd+'/'+line ++ newcwd = os.path.normpath(newcwd) + try: +- os.chdir(os.path.expanduser(line)) ++ os.chdir(newcwd) ++ self.cwd = newcwd + except Exception, e: + print e + try: +@@ -1523,7 +1401,7 @@ + except cmdutil.CantDetermineRevision, e: + print e + except Exception, e: +- print "Unhandled error:\n%s" % cmdutil.exception_str(e) ++ print "Unhandled error:\n%s" % errors.exception_str(e) + + elif suggestions.has_key(args[0]): + print suggestions[args[0]] +@@ -1574,7 +1452,7 @@ + arg = line.split()[-1] + else: + arg = "" +- iter = iter_munged_completions(iter, arg, text) ++ iter = cmdutil.iter_munged_completions(iter, arg, text) + except Exception, e: + print e + return list(iter) +@@ -1604,10 +1482,11 @@ + else: + arg = "" + if arg.startswith("-"): +- return list(iter_munged_completions(iter, arg, text)) ++ return list(cmdutil.iter_munged_completions(iter, arg, ++ text)) + else: +- return list(iter_munged_completions( +- iter_file_completions(arg), arg, text)) ++ return list(cmdutil.iter_munged_completions( ++ cmdutil.iter_file_completions(arg), arg, text)) + + + elif cmd == "cd": +@@ -1615,13 +1494,13 @@ + arg = args.split()[-1] + else: + arg = "" +- iter = iter_dir_completions(arg) +- iter = iter_munged_completions(iter, arg, text) ++ iter = cmdutil.iter_dir_completions(arg) ++ iter = cmdutil.iter_munged_completions(iter, arg, text) + return list(iter) + elif len(args)>0: + arg = args.split()[-1] +- return list(iter_munged_completions(iter_file_completions(arg), +- arg, text)) ++ iter = cmdutil.iter_file_completions(arg) ++ return list(cmdutil.iter_munged_completions(iter, arg, text)) + else: + return self.completenames(text, line, begidx, endidx) + except Exception, e: +@@ -1636,44 +1515,8 @@ + yield entry + + +-def iter_file_completions(arg, only_dirs = False): +- """Generate an iterator that iterates through filename completions. +- +- :param arg: The filename fragment to match +- :type arg: str +- :param only_dirs: If true, match only directories +- :type only_dirs: bool +- """ +- cwd = os.getcwd() +- if cwd != "/": +- extras = [".", ".."] +- else: +- extras = [] +- (dir, file) = os.path.split(arg) +- if dir != "": +- listingdir = os.path.expanduser(dir) +- else: +- listingdir = cwd +- for file in cmdutil.iter_combine([os.listdir(listingdir), extras]): +- if dir != "": +- userfile = dir+'/'+file +- else: +- userfile = file +- if userfile.startswith(arg): +- if os.path.isdir(listingdir+'/'+file): +- userfile+='/' +- yield userfile +- elif not only_dirs: +- yield userfile +- +-def iter_munged_completions(iter, arg, text): +- for completion in iter: +- completion = str(completion) +- if completion.startswith(arg): +- yield completion[len(arg)-len(text):] +- + def iter_source_file_completions(tree, arg): +- treepath = cmdutil.tree_cwd(tree) ++ treepath = arch_compound.tree_cwd(tree) + if len(treepath) > 0: + dirs = [treepath] + else: +@@ -1701,7 +1544,7 @@ + :return: An iterator of all matching untagged files + :rtype: iterator of str + """ +- treepath = cmdutil.tree_cwd(tree) ++ treepath = arch_compound.tree_cwd(tree) + if len(treepath) > 0: + dirs = [treepath] + else: +@@ -1743,8 +1586,8 @@ + :param arg: The prefix to match + :type arg: str + """ +- treepath = cmdutil.tree_cwd(tree) +- tmpdir = cmdutil.tmpdir() ++ treepath = arch_compound.tree_cwd(tree) ++ tmpdir = util.tmpdir() + changeset = tmpdir+"/changeset" + completions = [] + revision = cmdutil.determine_revision_tree(tree) +@@ -1756,14 +1599,6 @@ + shutil.rmtree(tmpdir) + return completions + +-def iter_dir_completions(arg): +- """Generate an iterator that iterates through directory name completions. +- +- :param arg: The directory name fragment to match +- :type arg: str +- """ +- return iter_file_completions(arg, True) +- + class Shell(BaseCommand): + def __init__(self): + self.description = "Runs Fai as a shell" +@@ -1795,7 +1630,11 @@ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + +- tree = arch.tree_root() ++ try: ++ tree = arch.tree_root() ++ except arch.errors.TreeRootError, e: ++ raise pylon.errors.CommandFailedWrapper(e) ++ + + if (len(args) == 0) == (options.untagged == False): + raise cmdutil.GetHelp +@@ -1809,13 +1648,22 @@ + if options.id_type == "tagline": + if method != "tagline": + if not cmdutil.prompt("Tagline in other tree"): +- if method == "explicit": +- options.id_type == explicit ++ if method == "explicit" or method == "implicit": ++ options.id_type == method + else: + print "add-id not supported for \"%s\" tagging method"\ + % method + return + ++ elif options.id_type == "implicit": ++ if method != "implicit": ++ if not cmdutil.prompt("Implicit in other tree"): ++ if method == "explicit" or method == "tagline": ++ options.id_type == method ++ else: ++ print "add-id not supported for \"%s\" tagging method"\ ++ % method ++ return + elif options.id_type == "explicit": + if method != "tagline" and method != explicit: + if not prompt("Explicit in other tree"): +@@ -1824,7 +1672,8 @@ + return + + if options.id_type == "auto": +- if method != "tagline" and method != "explicit": ++ if method != "tagline" and method != "explicit" \ ++ and method !="implicit": + print "add-id not supported for \"%s\" tagging method" % method + return + else: +@@ -1852,10 +1701,12 @@ + previous_files.extend(files) + if id_type == "explicit": + cmdutil.add_id(files) +- elif id_type == "tagline": ++ elif id_type == "tagline" or id_type == "implicit": + for file in files: + try: +- cmdutil.add_tagline_or_explicit_id(file) ++ implicit = (id_type == "implicit") ++ cmdutil.add_tagline_or_explicit_id(file, False, ++ implicit) + except cmdutil.AlreadyTagged: + print "\"%s\" already has a tagline." % file + except cmdutil.NoCommentSyntax: +@@ -1888,6 +1739,9 @@ + parser.add_option("--tagline", action="store_const", + const="tagline", dest="id_type", + help="Use a tagline id") ++ parser.add_option("--implicit", action="store_const", ++ const="implicit", dest="id_type", ++ help="Use an implicit id (deprecated)") + parser.add_option("--untagged", action="store_true", + dest="untagged", default=False, + help="tag all untagged files") +@@ -1926,27 +1780,7 @@ + def get_completer(self, arg, index): + if self.tree is None: + raise arch.errors.TreeRootError +- completions = list(ancillary.iter_partners(self.tree, +- self.tree.tree_version)) +- if len(completions) == 0: +- completions = list(self.tree.iter_log_versions()) +- +- aliases = [] +- try: +- for completion in completions: +- alias = ancillary.compact_alias(str(completion), self.tree) +- if alias: +- aliases.extend(alias) +- +- for completion in completions: +- if completion.archive == self.tree.tree_version.archive: +- aliases.append(completion.nonarch) +- +- except Exception, e: +- print e +- +- completions.extend(aliases) +- return completions ++ return cmdutil.merge_completions(self.tree, arg, index) + + def do_command(self, cmdargs): + """ +@@ -1961,7 +1795,7 @@ + + if self.tree is None: + raise arch.errors.TreeRootError(os.getcwd()) +- if cmdutil.has_changed(self.tree.tree_version): ++ if cmdutil.has_changed(ancillary.comp_revision(self.tree)): + raise UncommittedChanges(self.tree) + + if len(args) > 0: +@@ -2027,14 +1861,14 @@ + :type other_revision: `arch.Revision` + :return: 0 if the merge was skipped, 1 if it was applied + """ +- other_tree = cmdutil.find_or_make_local_revision(other_revision) ++ other_tree = arch_compound.find_or_make_local_revision(other_revision) + try: + if action == "native-merge": +- ancestor = cmdutil.merge_ancestor2(self.tree, other_tree, +- other_revision) ++ ancestor = arch_compound.merge_ancestor2(self.tree, other_tree, ++ other_revision) + elif action == "update": +- ancestor = cmdutil.tree_latest(self.tree, +- other_revision.version) ++ ancestor = arch_compound.tree_latest(self.tree, ++ other_revision.version) + except CantDetermineRevision, e: + raise CommandFailedWrapper(e) + cmdutil.colorize(arch.Chatter("* Found common ancestor %s" % ancestor)) +@@ -2104,7 +1938,10 @@ + if self.tree is None: + raise arch.errors.TreeRootError + +- edit_log(self.tree) ++ try: ++ edit_log(self.tree, self.tree.tree_version) ++ except pylon.errors.NoEditorSpecified, e: ++ raise pylon.errors.CommandFailedWrapper(e) + + def get_parser(self): + """ +@@ -2132,7 +1969,7 @@ + """ + return + +-def edit_log(tree): ++def edit_log(tree, version): + """Makes and edits the log for a tree. Does all kinds of fancy things + like log templates and merge summaries and log-for-merge + +@@ -2141,28 +1978,29 @@ + """ + #ensure we have an editor before preparing the log + cmdutil.find_editor() +- log = tree.log_message(create=False) ++ log = tree.log_message(create=False, version=version) + log_is_new = False + if log is None or cmdutil.prompt("Overwrite log"): + if log is not None: + os.remove(log.name) +- log = tree.log_message(create=True) ++ log = tree.log_message(create=True, version=version) + log_is_new = True + tmplog = log.name +- template = tree+"/{arch}/=log-template" +- if not os.path.exists(template): +- template = os.path.expanduser("~/.arch-params/=log-template") +- if not os.path.exists(template): +- template = None ++ template = pylon.log_template_path(tree) + if template: + shutil.copyfile(template, tmplog) +- +- new_merges = list(cmdutil.iter_new_merges(tree, +- tree.tree_version)) +- log["Summary"] = merge_summary(new_merges, tree.tree_version) ++ comp_version = ancillary.comp_revision(tree).version ++ new_merges = cmdutil.iter_new_merges(tree, comp_version) ++ new_merges = cmdutil.direct_merges(new_merges) ++ log["Summary"] = pylon.merge_summary(new_merges, ++ version) + if len(new_merges) > 0: + if cmdutil.prompt("Log for merge"): +- mergestuff = cmdutil.log_for_merge(tree) ++ if cmdutil.prompt("changelog for merge"): ++ mergestuff = "Patches applied:\n" ++ mergestuff += pylon.changelog_for_merge(new_merges) ++ else: ++ mergestuff = cmdutil.log_for_merge(tree, comp_version) + log.description += mergestuff + log.save() + try: +@@ -2172,29 +2010,6 @@ + os.remove(log.name) + raise + +-def merge_summary(new_merges, tree_version): +- if len(new_merges) == 0: +- return "" +- if len(new_merges) == 1: +- summary = new_merges[0].summary +- else: +- summary = "Merge" +- +- credits = [] +- for merge in new_merges: +- if arch.my_id() != merge.creator: +- name = re.sub("<.*>", "", merge.creator).rstrip(" "); +- if not name in credits: +- credits.append(name) +- else: +- version = merge.revision.version +- if version.archive == tree_version.archive: +- if not version.nonarch in credits: +- credits.append(version.nonarch) +- elif not str(version) in credits: +- credits.append(str(version)) +- +- return ("%s (%s)") % (summary, ", ".join(credits)) + + class MirrorArchive(BaseCommand): + """ +@@ -2268,31 +2083,73 @@ + + Use "alias" to list available (user and automatic) aliases.""" + ++auto_alias = [ ++"acur", ++"The latest revision in the archive of the tree-version. You can specify \ ++a different version like so: acur:foo--bar--0 (aliases can be used)", ++"tcur", ++"""(tree current) The latest revision in the tree of the tree-version. \ ++You can specify a different version like so: tcur:foo--bar--0 (aliases can be \ ++used).""", ++"tprev" , ++"""(tree previous) The previous revision in the tree of the tree-version. To \ ++specify an older revision, use a number, e.g. "tprev:4" """, ++"tanc" , ++"""(tree ancestor) The ancestor revision of the tree To specify an older \ ++revision, use a number, e.g. "tanc:4".""", ++"tdate" , ++"""(tree date) The latest revision from a given date, e.g. "tdate:July 6".""", ++"tmod" , ++""" (tree modified) The latest revision to modify a given file, e.g. \ ++"tmod:engine.cpp" or "tmod:engine.cpp:16".""", ++"ttag" , ++"""(tree tag) The revision that was tagged into the current tree revision, \ ++according to the tree""", ++"tagcur", ++"""(tag current) The latest revision of the version that the current tree \ ++was tagged from.""", ++"mergeanc" , ++"""The common ancestor of the current tree and the specified revision. \ ++Defaults to the first partner-version's latest revision or to tagcur.""", ++] ++ ++ ++def is_auto_alias(name): ++ """Determine whether a name is an auto alias name ++ ++ :param name: the name to check ++ :type name: str ++ :return: True if the name is an auto alias, false if not ++ :rtype: bool ++ """ ++ return name in [f for (f, v) in pylon.util.iter_pairs(auto_alias)] ++ ++ ++def display_def(iter, wrap = 80): ++ """Display a list of definitions ++ ++ :param iter: iter of name, definition pairs ++ :type iter: iter of (str, str) ++ :param wrap: The width for text wrapping ++ :type wrap: int ++ """ ++ vals = list(iter) ++ maxlen = 0 ++ for (key, value) in vals: ++ if len(key) > maxlen: ++ maxlen = len(key) ++ for (key, value) in vals: ++ tw=textwrap.TextWrapper(width=wrap, ++ initial_indent=key.rjust(maxlen)+" : ", ++ subsequent_indent="".rjust(maxlen+3)) ++ print tw.fill(value) ++ ++ + def help_aliases(tree): +- print """Auto-generated aliases +- acur : The latest revision in the archive of the tree-version. You can specfy +- a different version like so: acur:foo--bar--0 (aliases can be used) +- tcur : (tree current) The latest revision in the tree of the tree-version. +- You can specify a different version like so: tcur:foo--bar--0 (aliases +- can be used). +-tprev : (tree previous) The previous revision in the tree of the tree-version. +- To specify an older revision, use a number, e.g. "tprev:4" +- tanc : (tree ancestor) The ancestor revision of the tree +- To specify an older revision, use a number, e.g. "tanc:4" +-tdate : (tree date) The latest revision from a given date (e.g. "tdate:July 6") +- tmod : (tree modified) The latest revision to modify a given file +- (e.g. "tmod:engine.cpp" or "tmod:engine.cpp:16") +- ttag : (tree tag) The revision that was tagged into the current tree revision, +- according to the tree. +-tagcur: (tag current) The latest revision of the version that the current tree +- was tagged from. +-mergeanc : The common ancestor of the current tree and the specified revision. +- Defaults to the first partner-version's latest revision or to tagcur. +- """ ++ print """Auto-generated aliases""" ++ display_def(pylon.util.iter_pairs(auto_alias)) + print "User aliases" +- for parts in ancillary.iter_all_alias(tree): +- print parts[0].rjust(10)+" : "+parts[1] +- ++ display_def(ancillary.iter_all_alias(tree)) + + class Inventory(BaseCommand): + """List the status of files in the tree""" +@@ -2428,6 +2285,11 @@ + except cmdutil.ForbiddenAliasSyntax, e: + raise CommandFailedWrapper(e) + ++ def no_prefix(self, alias): ++ if alias.startswith("^"): ++ alias = alias[1:] ++ return alias ++ + def arg_dispatch(self, args, options): + """Add, modify, or list aliases, depending on number of arguments + +@@ -2438,15 +2300,20 @@ + if len(args) == 0: + help_aliases(self.tree) + return +- elif len(args) == 1: +- self.print_alias(args[0]) +- elif (len(args)) == 2: +- self.add(args[0], args[1], options) + else: +- raise cmdutil.GetHelp ++ alias = self.no_prefix(args[0]) ++ if len(args) == 1: ++ self.print_alias(alias) ++ elif (len(args)) == 2: ++ self.add(alias, args[1], options) ++ else: ++ raise cmdutil.GetHelp + + def print_alias(self, alias): + answer = None ++ if is_auto_alias(alias): ++ raise pylon.errors.IsAutoAlias(alias, "\"%s\" is an auto alias." ++ " Use \"revision\" to expand auto aliases." % alias) + for pair in ancillary.iter_all_alias(self.tree): + if pair[0] == alias: + answer = pair[1] +@@ -2464,6 +2331,8 @@ + :type expansion: str + :param options: The commandline options + """ ++ if is_auto_alias(alias): ++ raise IsAutoAlias(alias) + newlist = "" + written = False + new_line = "%s=%s\n" % (alias, cmdutil.expand_alias(expansion, +@@ -2490,14 +2359,17 @@ + deleted = False + if len(args) != 1: + raise cmdutil.GetHelp ++ alias = self.no_prefix(args[0]) ++ if is_auto_alias(alias): ++ raise IsAutoAlias(alias) + newlist = "" + for pair in self.get_iterator(options): +- if pair[0] != args[0]: ++ if pair[0] != alias: + newlist+="%s=%s\n" % (pair[0], pair[1]) + else: + deleted = True + if not deleted: +- raise errors.NoSuchAlias(args[0]) ++ raise errors.NoSuchAlias(alias) + self.write_aliases(newlist, options) + + def get_alias_file(self, options): +@@ -2526,7 +2398,7 @@ + :param options: The commandline options + """ + filename = os.path.expanduser(self.get_alias_file(options)) +- file = cmdutil.NewFileVersion(filename) ++ file = util.NewFileVersion(filename) + file.write(newlist) + file.commit() + +@@ -2588,10 +2460,13 @@ + :param cmdargs: The commandline arguments + :type cmdargs: list of str + """ +- cmdutil.find_editor() + parser = self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: ++ cmdutil.find_editor() ++ except pylon.errors.NoEditorSpecified, e: ++ raise pylon.errors.CommandFailedWrapper(e) ++ try: + self.tree=arch.tree_root() + except: + self.tree=None +@@ -2655,7 +2530,7 @@ + target_revision = cmdutil.determine_revision_arch(self.tree, + args[0]) + else: +- target_revision = cmdutil.tree_latest(self.tree) ++ target_revision = arch_compound.tree_latest(self.tree) + if len(args) > 1: + merges = [ arch.Patchlog(cmdutil.determine_revision_arch( + self.tree, f)) for f in args[1:] ] +@@ -2711,7 +2586,7 @@ + + :param message: The message to send + :type message: `email.Message`""" +- server = smtplib.SMTP() ++ server = smtplib.SMTP("localhost") + server.sendmail(message['From'], message['To'], message.as_string()) + server.quit() + +@@ -2763,6 +2638,22 @@ + 'alias' : Alias, + 'request-merge': RequestMerge, + } ++ ++def my_import(mod_name): ++ module = __import__(mod_name) ++ components = mod_name.split('.') ++ for comp in components[1:]: ++ module = getattr(module, comp) ++ return module ++ ++def plugin(mod_name): ++ module = my_import(mod_name) ++ module.add_command(commands) ++ ++for file in os.listdir(sys.path[0]+"/command"): ++ if len(file) > 3 and file[-3:] == ".py" and file != "__init__.py": ++ plugin("command."+file[:-3]) ++ + suggestions = { + 'apply-delta' : "Try \"apply-changes\".", + 'delta' : "To compare two revisions, use \"changes\".", +@@ -2784,6 +2675,7 @@ + 'tagline' : "Use add-id. It uses taglines in tagline trees", + 'emlog' : "Use elog. It automatically adds log-for-merge text, if any", + 'library-revisions' : "Use revisions --library", +-'file-revert' : "Use revert FILE" ++'file-revert' : "Use revert FILE", ++'join-branch' : "Use replay --logs-only" + } + # arch-tag: 19d5739d-3708-486c-93ba-deecc3027fc7 *** added file 'testdata/insert_top.patch' --- /dev/null +++ testdata/insert_top.patch @@ -0,0 +1,7 @@ +--- orig/pylon/patches.py ++++ mod/pylon/patches.py +@@ -1,3 +1,4 @@ ++#test + import util + import sys + class PatchSyntax(Exception): *** added file 'testdata/mod' --- /dev/null +++ testdata/mod @@ -0,0 +1,2681 @@ +# Copyright (C) 2004 Aaron Bentley +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import sys +import arch +import arch.util +import arch.arch + +import pylon.errors +from pylon.errors import * +from pylon import errors +from pylon import util +from pylon import arch_core +from pylon import arch_compound +from pylon import ancillary +from pylon import misc +from pylon import paths + +import abacmds +import cmdutil +import shutil +import os +import options +import time +import cmd +import readline +import re +import string +import terminal +import email +import smtplib +import textwrap + +__docformat__ = "restructuredtext" +__doc__ = "Implementation of user (sub) commands" +commands = {} + +def find_command(cmd): + """ + Return an instance of a command type. Return None if the type isn't + registered. + + :param cmd: the name of the command to look for + :type cmd: the type of the command + """ + if commands.has_key(cmd): + return commands[cmd]() + else: + return None + +class BaseCommand: + def __call__(self, cmdline): + try: + self.do_command(cmdline.split()) + except cmdutil.GetHelp, e: + self.help() + except Exception, e: + print e + + def get_completer(index): + return None + + def complete(self, args, text): + """ + Returns a list of possible completions for the given text. + + :param args: The complete list of arguments + :type args: List of str + :param text: text to complete (may be shorter than args[-1]) + :type text: str + :rtype: list of str + """ + matches = [] + candidates = None + + if len(args) > 0: + realtext = args[-1] + else: + realtext = "" + + try: + parser=self.get_parser() + if realtext.startswith('-'): + candidates = parser.iter_options() + else: + (options, parsed_args) = parser.parse_args(args) + + if len (parsed_args) > 0: + candidates = self.get_completer(parsed_args[-1], len(parsed_args) -1) + else: + candidates = self.get_completer("", 0) + except: + pass + if candidates is None: + return + for candidate in candidates: + candidate = str(candidate) + if candidate.startswith(realtext): + matches.append(candidate[len(realtext)- len(text):]) + return matches + + +class Help(BaseCommand): + """ + Lists commands, prints help messages. + """ + def __init__(self): + self.description="Prints help mesages" + self.parser = None + + def do_command(self, cmdargs): + """ + Prints a help message. + """ + options, args = self.get_parser().parse_args(cmdargs) + if len(args) > 1: + raise cmdutil.GetHelp + + if options.native or options.suggestions or options.external: + native = options.native + suggestions = options.suggestions + external = options.external + else: + native = True + suggestions = False + external = True + + if len(args) == 0: + self.list_commands(native, suggestions, external) + return + elif len(args) == 1: + command_help(args[0]) + return + + def help(self): + self.get_parser().print_help() + print """ +If no command is specified, commands are listed. If a command is +specified, help for that command is listed. + """ + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + if self.parser is not None: + return self.parser + parser=cmdutil.CmdOptionParser("fai help [command]") + parser.add_option("-n", "--native", action="store_true", + dest="native", help="Show native commands") + parser.add_option("-e", "--external", action="store_true", + dest="external", help="Show external commands") + parser.add_option("-s", "--suggest", action="store_true", + dest="suggestions", help="Show suggestions") + self.parser = parser + return parser + + def list_commands(self, native=True, suggest=False, external=True): + """ + Lists supported commands. + + :param native: list native, python-based commands + :type native: bool + :param external: list external aba-style commands + :type external: bool + """ + if native: + print "Native Fai commands" + keys=commands.keys() + keys.sort() + for k in keys: + space="" + for i in range(28-len(k)): + space+=" " + print space+k+" : "+commands[k]().description + print + if suggest: + print "Unavailable commands and suggested alternatives" + key_list = suggestions.keys() + key_list.sort() + for key in key_list: + print "%28s : %s" % (key, suggestions[key]) + print + if external: + fake_aba = abacmds.AbaCmds() + if (fake_aba.abadir == ""): + return + print "External commands" + fake_aba.list_commands() + print + if not suggest: + print "Use help --suggest to list alternatives to tla and aba"\ + " commands." + if options.tla_fallthrough and (native or external): + print "Fai also supports tla commands." + +def command_help(cmd): + """ + Prints help for a command. + + :param cmd: The name of the command to print help for + :type cmd: str + """ + fake_aba = abacmds.AbaCmds() + cmdobj = find_command(cmd) + if cmdobj != None: + cmdobj.help() + elif suggestions.has_key(cmd): + print "Not available\n" + suggestions[cmd] + else: + abacmd = fake_aba.is_command(cmd) + if abacmd: + abacmd.help() + else: + print "No help is available for \""+cmd+"\". Maybe try \"tla "+cmd+" -H\"?" + + + +class Changes(BaseCommand): + """ + the "changes" command: lists differences between trees/revisions: + """ + + def __init__(self): + self.description="Lists what files have changed in the project tree" + + def get_completer(self, arg, index): + if index > 1: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def parse_commandline(self, cmdline): + """ + Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. + + :param cmdline: A list of arguments to parse + :rtype: (options, Revision, Revision/WorkingTree) + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdline) + if len(args) > 2: + raise cmdutil.GetHelp + + tree=arch.tree_root() + if len(args) == 0: + a_spec = ancillary.comp_revision(tree) + else: + a_spec = cmdutil.determine_revision_tree(tree, args[0]) + cmdutil.ensure_archive_registered(a_spec.archive) + if len(args) == 2: + b_spec = cmdutil.determine_revision_tree(tree, args[1]) + cmdutil.ensure_archive_registered(b_spec.archive) + else: + b_spec=tree + return options, a_spec, b_spec + + def do_command(self, cmdargs): + """ + Master function that perfoms the "changes" command. + """ + try: + options, a_spec, b_spec = self.parse_commandline(cmdargs); + except cmdutil.CantDetermineRevision, e: + print e + return + except arch.errors.TreeRootError, e: + print e + return + if options.changeset: + changeset=options.changeset + tmpdir = None + else: + tmpdir=util.tmpdir() + changeset=tmpdir+"/changeset" + try: + delta=arch.iter_delta(a_spec, b_spec, changeset) + try: + for line in delta: + if cmdutil.chattermatch(line, "changeset:"): + pass + else: + cmdutil.colorize(line, options.suppress_chatter) + except arch.util.ExecProblem, e: + if e.proc.error and e.proc.error.startswith( + "missing explicit id for file"): + raise MissingID(e) + else: + raise + status=delta.status + if status > 1: + return + if (options.perform_diff): + chan = arch_compound.ChangesetMunger(changeset) + chan.read_indices() + if options.diffopts is not None: + if isinstance(b_spec, arch.Revision): + b_dir = b_spec.library_find() + else: + b_dir = b_spec + a_dir = a_spec.library_find() + diffopts = options.diffopts.split() + cmdutil.show_custom_diffs(chan, diffopts, a_dir, b_dir) + else: + cmdutil.show_diffs(delta.changeset) + finally: + if tmpdir and (os.access(tmpdir, os.X_OK)): + shutil.rmtree(tmpdir) + + def get_parser(self): + """ + Returns the options parser to use for the "changes" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai changes [options] [revision]" + " [revision]") + parser.add_option("-d", "--diff", action="store_true", + dest="perform_diff", default=False, + help="Show diffs in summary") + parser.add_option("-c", "--changeset", dest="changeset", + help="Store a changeset in the given directory", + metavar="DIRECTORY") + parser.add_option("-s", "--silent", action="store_true", + dest="suppress_chatter", default=False, + help="Suppress chatter messages") + parser.add_option("--diffopts", dest="diffopts", + help="Use the specified diff options", + metavar="OPTIONS") + + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser is None: + parser=self.get_parser() + parser.print_help() + print """ +Performs source-tree comparisons + +If no revision is specified, the current project tree is compared to the +last-committed revision. If one revision is specified, the current project +tree is compared to that revision. If two revisions are specified, they are +compared to each other. + """ + help_tree_spec() + return + + +class ApplyChanges(BaseCommand): + """ + Apply differences between two revisions to a tree + """ + + def __init__(self): + self.description="Applies changes to a project tree" + + def get_completer(self, arg, index): + if index > 1: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def parse_commandline(self, cmdline, tree): + """ + Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. + + :param cmdline: A list of arguments to parse + :rtype: (options, Revision, Revision/WorkingTree) + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdline) + if len(args) != 2: + raise cmdutil.GetHelp + + a_spec = cmdutil.determine_revision_tree(tree, args[0]) + cmdutil.ensure_archive_registered(a_spec.archive) + b_spec = cmdutil.determine_revision_tree(tree, args[1]) + cmdutil.ensure_archive_registered(b_spec.archive) + return options, a_spec, b_spec + + def do_command(self, cmdargs): + """ + Master function that performs "apply-changes". + """ + try: + tree = arch.tree_root() + options, a_spec, b_spec = self.parse_commandline(cmdargs, tree); + except cmdutil.CantDetermineRevision, e: + print e + return + except arch.errors.TreeRootError, e: + print e + return + delta=cmdutil.apply_delta(a_spec, b_spec, tree) + for line in cmdutil.iter_apply_delta_filter(delta): + cmdutil.colorize(line, options.suppress_chatter) + + def get_parser(self): + """ + Returns the options parser to use for the "apply-changes" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai apply-changes [options] revision" + " revision") + parser.add_option("-d", "--diff", action="store_true", + dest="perform_diff", default=False, + help="Show diffs in summary") + parser.add_option("-c", "--changeset", dest="changeset", + help="Store a changeset in the given directory", + metavar="DIRECTORY") + parser.add_option("-s", "--silent", action="store_true", + dest="suppress_chatter", default=False, + help="Suppress chatter messages") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser is None: + parser=self.get_parser() + parser.print_help() + print """ +Applies changes to a project tree + +Compares two revisions and applies the difference between them to the current +tree. + """ + help_tree_spec() + return + +class Update(BaseCommand): + """ + Updates a project tree to a given revision, preserving un-committed hanges. + """ + + def __init__(self): + self.description="Apply the latest changes to the current directory" + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def parse_commandline(self, cmdline, tree): + """ + Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. + + :param cmdline: A list of arguments to parse + :rtype: (options, Revision, Revision/WorkingTree) + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdline) + if len(args) > 2: + raise cmdutil.GetHelp + + spec=None + if len(args)>0: + spec=args[0] + revision=cmdutil.determine_revision_arch(tree, spec) + cmdutil.ensure_archive_registered(revision.archive) + + mirror_source = cmdutil.get_mirror_source(revision.archive) + if mirror_source != None: + if cmdutil.prompt("Mirror update"): + cmd=cmdutil.mirror_archive(mirror_source, + revision.archive, arch.NameParser(revision).get_package_version()) + for line in arch.chatter_classifier(cmd): + cmdutil.colorize(line, options.suppress_chatter) + + revision=cmdutil.determine_revision_arch(tree, spec) + + return options, revision + + def do_command(self, cmdargs): + """ + Master function that perfoms the "update" command. + """ + tree=arch.tree_root() + try: + options, to_revision = self.parse_commandline(cmdargs, tree); + except cmdutil.CantDetermineRevision, e: + print e + return + except arch.errors.TreeRootError, e: + print e + return + from_revision = arch_compound.tree_latest(tree) + if from_revision==to_revision: + print "Tree is already up to date with:\n"+str(to_revision)+"." + return + cmdutil.ensure_archive_registered(from_revision.archive) + cmd=cmdutil.apply_delta(from_revision, to_revision, tree, + options.patch_forward) + for line in cmdutil.iter_apply_delta_filter(cmd): + cmdutil.colorize(line) + if to_revision.version != tree.tree_version: + if cmdutil.prompt("Update version"): + tree.tree_version = to_revision.version + + def get_parser(self): + """ + Returns the options parser to use for the "update" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai update [options]" + " [revision/version]") + parser.add_option("-f", "--forward", action="store_true", + dest="patch_forward", default=False, + help="pass the --forward option to 'patch'") + parser.add_option("-s", "--silent", action="store_true", + dest="suppress_chatter", default=False, + help="Suppress chatter messages") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser is None: + parser=self.get_parser() + parser.print_help() + print """ +Updates a working tree to the current archive revision + +If a revision or version is specified, that is used instead + """ + help_tree_spec() + return + + +class Commit(BaseCommand): + """ + Create a revision based on the changes in the current tree. + """ + + def __init__(self): + self.description="Write local changes to the archive" + + def get_completer(self, arg, index): + if arg is None: + arg = "" + return iter_modified_file_completions(arch.tree_root(), arg) +# return iter_source_file_completions(arch.tree_root(), arg) + + def parse_commandline(self, cmdline, tree): + """ + Parse commandline arguments. Raise cmtutil.GetHelp if help is needed. + + :param cmdline: A list of arguments to parse + :rtype: (options, Revision, Revision/WorkingTree) + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdline) + + if len(args) == 0: + args = None + if options.version is None: + return options, tree.tree_version, args + + revision=cmdutil.determine_revision_arch(tree, options.version) + return options, revision.get_version(), args + + def do_command(self, cmdargs): + """ + Master function that perfoms the "commit" command. + """ + tree=arch.tree_root() + options, version, files = self.parse_commandline(cmdargs, tree) + ancestor = None + if options.__dict__.has_key("base") and options.base: + base = cmdutil.determine_revision_tree(tree, options.base) + ancestor = base + else: + base = ancillary.submit_revision(tree) + ancestor = base + if ancestor is None: + ancestor = arch_compound.tree_latest(tree, version) + + writeversion=version + archive=version.archive + source=cmdutil.get_mirror_source(archive) + allow_old=False + writethrough="implicit" + + if source!=None: + if writethrough=="explicit" and \ + cmdutil.prompt("Writethrough"): + writeversion=arch.Version(str(source)+"/"+str(version.get_nonarch())) + elif writethrough=="none": + raise CommitToMirror(archive) + + elif archive.is_mirror: + raise CommitToMirror(archive) + + try: + last_revision=tree.iter_logs(version, True).next().revision + except StopIteration, e: + last_revision = None + if ancestor is None: + if cmdutil.prompt("Import from commit"): + return do_import(version) + else: + raise NoVersionLogs(version) + try: + arch_last_revision = version.iter_revisions(True).next() + except StopIteration, e: + arch_last_revision = None + + if last_revision != arch_last_revision: + print "Tree is not up to date with %s" % str(version) + if not cmdutil.prompt("Out of date"): + raise OutOfDate + else: + allow_old=True + + try: + if not cmdutil.has_changed(ancestor): + if not cmdutil.prompt("Empty commit"): + raise EmptyCommit + except arch.util.ExecProblem, e: + if e.proc.error and e.proc.error.startswith( + "missing explicit id for file"): + raise MissingID(e) + else: + raise + log = tree.log_message(create=False, version=version) + if log is None: + try: + if cmdutil.prompt("Create log"): + edit_log(tree, version) + + except cmdutil.NoEditorSpecified, e: + raise CommandFailed(e) + log = tree.log_message(create=False, version=version) + if log is None: + raise NoLogMessage + if log["Summary"] is None or len(log["Summary"].strip()) == 0: + if not cmdutil.prompt("Omit log summary"): + raise errors.NoLogSummary + try: + for line in tree.iter_commit(version, seal=options.seal_version, + base=base, out_of_date_ok=allow_old, file_list=files): + cmdutil.colorize(line, options.suppress_chatter) + + except arch.util.ExecProblem, e: + if e.proc.error and e.proc.error.startswith( + "These files violate naming conventions:"): + raise LintFailure(e.proc.error) + else: + raise + + def get_parser(self): + """ + Returns the options parser to use for the "commit" command. + + :rtype: cmdutil.CmdOptionParser + """ + + parser=cmdutil.CmdOptionParser("fai commit [options] [file1]" + " [file2...]") + parser.add_option("--seal", action="store_true", + dest="seal_version", default=False, + help="seal this version") + parser.add_option("-v", "--version", dest="version", + help="Use the specified version", + metavar="VERSION") + parser.add_option("-s", "--silent", action="store_true", + dest="suppress_chatter", default=False, + help="Suppress chatter messages") + if cmdutil.supports_switch("commit", "--base"): + parser.add_option("--base", dest="base", help="", + metavar="REVISION") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser is None: + parser=self.get_parser() + parser.print_help() + print """ +Updates a working tree to the current archive revision + +If a version is specified, that is used instead + """ +# help_tree_spec() + return + + + +class CatLog(BaseCommand): + """ + Print the log of a given file (from current tree) + """ + def __init__(self): + self.description="Prints the patch log for a revision" + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "cat-log" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: + tree = arch.tree_root() + except arch.errors.TreeRootError, e: + tree = None + spec=None + if len(args) > 0: + spec=args[0] + if len(args) > 1: + raise cmdutil.GetHelp() + try: + if tree: + revision = cmdutil.determine_revision_tree(tree, spec) + else: + revision = cmdutil.determine_revision_arch(tree, spec) + except cmdutil.CantDetermineRevision, e: + raise CommandFailedWrapper(e) + log = None + + use_tree = (options.source == "tree" or \ + (options.source == "any" and tree)) + use_arch = (options.source == "archive" or options.source == "any") + + log = None + if use_tree: + for log in tree.iter_logs(revision.get_version()): + if log.revision == revision: + break + else: + log = None + if log is None and use_arch: + cmdutil.ensure_revision_exists(revision) + log = arch.Patchlog(revision) + if log is not None: + for item in log.items(): + print "%s: %s" % item + print log.description + + def get_parser(self): + """ + Returns the options parser to use for the "cat-log" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai cat-log [revision]") + parser.add_option("--archive", action="store_const", dest="source", + const="archive", default="any", + help="Always get the log from the archive") + parser.add_option("--tree", action="store_const", dest="source", + const="tree", help="Always get the log from the tree") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Prints the log for the specified revision + """ + help_tree_spec() + return + +class Revert(BaseCommand): + """ Reverts a tree (or aspects of it) to a revision + """ + def __init__(self): + self.description="Reverts a tree (or aspects of it) to a revision " + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return iter_modified_file_completions(tree, arg) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revert" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: + tree = arch.tree_root() + except arch.errors.TreeRootError, e: + raise CommandFailed(e) + spec=None + if options.revision is not None: + spec=options.revision + try: + if spec is not None: + revision = cmdutil.determine_revision_tree(tree, spec) + else: + revision = ancillary.comp_revision(tree) + except cmdutil.CantDetermineRevision, e: + raise CommandFailedWrapper(e) + munger = None + + if options.file_contents or options.file_perms or options.deletions\ + or options.additions or options.renames or options.hunk_prompt: + munger = arch_compound.MungeOpts() + munger.set_hunk_prompt(cmdutil.colorize, cmdutil.user_hunk_confirm, + options.hunk_prompt) + + if len(args) > 0 or options.logs or options.pattern_files or \ + options.control: + if munger is None: + munger = cmdutil.arch_compound.MungeOpts(True) + munger.all_types(True) + if len(args) > 0: + t_cwd = arch_compound.tree_cwd(tree) + for name in args: + if len(t_cwd) > 0: + t_cwd += "/" + name = "./" + t_cwd + name + munger.add_keep_file(name); + + if options.file_perms: + munger.file_perms = True + if options.file_contents: + munger.file_contents = True + if options.deletions: + munger.deletions = True + if options.additions: + munger.additions = True + if options.renames: + munger.renames = True + if options.logs: + munger.add_keep_pattern('^\./\{arch\}/[^=].*') + if options.control: + munger.add_keep_pattern("/\.arch-ids|^\./\{arch\}|"\ + "/\.arch-inventory$") + if options.pattern_files: + munger.add_keep_pattern(options.pattern_files) + + for line in arch_compound.revert(tree, revision, munger, + not options.no_output): + cmdutil.colorize(line) + + + def get_parser(self): + """ + Returns the options parser to use for the "cat-log" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai revert [options] [FILE...]") + parser.add_option("", "--contents", action="store_true", + dest="file_contents", + help="Revert file content changes") + parser.add_option("", "--permissions", action="store_true", + dest="file_perms", + help="Revert file permissions changes") + parser.add_option("", "--deletions", action="store_true", + dest="deletions", + help="Restore deleted files") + parser.add_option("", "--additions", action="store_true", + dest="additions", + help="Remove added files") + parser.add_option("", "--renames", action="store_true", + dest="renames", + help="Revert file names") + parser.add_option("--hunks", action="store_true", + dest="hunk_prompt", default=False, + help="Prompt which hunks to revert") + parser.add_option("--pattern-files", dest="pattern_files", + help="Revert files that match this pattern", + metavar="REGEX") + parser.add_option("--logs", action="store_true", + dest="logs", default=False, + help="Revert only logs") + parser.add_option("--control-files", action="store_true", + dest="control", default=False, + help="Revert logs and other control files") + parser.add_option("-n", "--no-output", action="store_true", + dest="no_output", + help="Don't keep an undo changeset") + parser.add_option("--revision", dest="revision", + help="Revert to the specified revision", + metavar="REVISION") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Reverts changes in the current working tree. If no flags are specified, all +types of changes are reverted. Otherwise, only selected types of changes are +reverted. + +If a revision is specified on the commandline, differences between the current +tree and that revision are reverted. If a version is specified, the current +tree is used to determine the revision. + +If files are specified, only those files listed will have any changes applied. +To specify a renamed file, you can use either the old or new name. (or both!) + +Unless "-n" is specified, reversions can be undone with "redo". + """ + return + +class Revision(BaseCommand): + """ + Print a revision name based on a revision specifier + """ + def __init__(self): + self.description="Prints the name of a revision" + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + tree = None + + spec=None + if len(args) > 0: + spec=args[0] + if len(args) > 1: + raise cmdutil.GetHelp + try: + if tree: + revision = cmdutil.determine_revision_tree(tree, spec) + else: + revision = cmdutil.determine_revision_arch(tree, spec) + except cmdutil.CantDetermineRevision, e: + print str(e) + return + print options.display(revision) + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai revision [revision]") + parser.add_option("", "--location", action="store_const", + const=paths.determine_path, dest="display", + help="Show location instead of name", default=str) + parser.add_option("--import", action="store_const", + const=paths.determine_import_path, dest="display", + help="Show location of import file") + parser.add_option("--log", action="store_const", + const=paths.determine_log_path, dest="display", + help="Show location of log file") + parser.add_option("--patch", action="store_const", + dest="display", const=paths.determine_patch_path, + help="Show location of patchfile") + parser.add_option("--continuation", action="store_const", + const=paths.determine_continuation_path, + dest="display", + help="Show location of continuation file") + parser.add_option("--cacherev", action="store_const", + const=paths.determine_cacherev_path, dest="display", + help="Show location of cacherev file") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Expands aliases and prints the name of the specified revision. Instead of +the name, several options can be used to print locations. If more than one is +specified, the last one is used. + """ + help_tree_spec() + return + +class Revisions(BaseCommand): + """ + Print a revision name based on a revision specifier + """ + def __init__(self): + self.description="Lists revisions" + self.cl_revisions = [] + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + (options, args) = self.get_parser().parse_args(cmdargs) + if len(args) > 1: + raise cmdutil.GetHelp + try: + self.tree = arch.tree_root() + except arch.errors.TreeRootError: + self.tree = None + if options.type == "default": + options.type = "archive" + try: + iter = cmdutil.revision_iterator(self.tree, options.type, args, + options.reverse, options.modified, + options.shallow) + except cmdutil.CantDetermineRevision, e: + raise CommandFailedWrapper(e) + except cmdutil.CantDetermineVersion, e: + raise CommandFailedWrapper(e) + if options.skip is not None: + iter = cmdutil.iter_skip(iter, int(options.skip)) + + try: + for revision in iter: + log = None + if isinstance(revision, arch.Patchlog): + log = revision + revision=revision.revision + out = options.display(revision) + if out is not None: + print out + if log is None and (options.summary or options.creator or + options.date or options.merges): + log = revision.patchlog + if options.creator: + print " %s" % log.creator + if options.date: + print " %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date) + if options.summary: + print " %s" % log.summary + if options.merges: + showed_title = False + for revision in log.merged_patches: + if not showed_title: + print " Merged:" + showed_title = True + print " %s" % revision + if len(self.cl_revisions) > 0: + print pylon.changelog_for_merge(self.cl_revisions) + except pylon.errors.TreeRootNone: + raise CommandFailedWrapper( + Exception("This option can only be used in a project tree.")) + + def changelog_append(self, revision): + if isinstance(revision, arch.Revision): + revision=arch.Patchlog(revision) + self.cl_revisions.append(revision) + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai revisions [version/revision]") + select = cmdutil.OptionGroup(parser, "Selection options", + "Control which revisions are listed. These options" + " are mutually exclusive. If more than one is" + " specified, the last is used.") + + cmdutil.add_revision_iter_options(select) + parser.add_option("", "--skip", dest="skip", + help="Skip revisions. Positive numbers skip from " + "beginning, negative skip from end.", + metavar="NUMBER") + + parser.add_option_group(select) + + format = cmdutil.OptionGroup(parser, "Revision format options", + "These control the appearance of listed revisions") + format.add_option("", "--location", action="store_const", + const=paths.determine_path, dest="display", + help="Show location instead of name", default=str) + format.add_option("--import", action="store_const", + const=paths.determine_import_path, dest="display", + help="Show location of import file") + format.add_option("--log", action="store_const", + const=paths.determine_log_path, dest="display", + help="Show location of log file") + format.add_option("--patch", action="store_const", + dest="display", const=paths.determine_patch_path, + help="Show location of patchfile") + format.add_option("--continuation", action="store_const", + const=paths.determine_continuation_path, + dest="display", + help="Show location of continuation file") + format.add_option("--cacherev", action="store_const", + const=paths.determine_cacherev_path, dest="display", + help="Show location of cacherev file") + format.add_option("--changelog", action="store_const", + const=self.changelog_append, dest="display", + help="Show location of cacherev file") + parser.add_option_group(format) + display = cmdutil.OptionGroup(parser, "Display format options", + "These control the display of data") + display.add_option("-r", "--reverse", action="store_true", + dest="reverse", help="Sort from newest to oldest") + display.add_option("-s", "--summary", action="store_true", + dest="summary", help="Show patchlog summary") + display.add_option("-D", "--date", action="store_true", + dest="date", help="Show patchlog date") + display.add_option("-c", "--creator", action="store_true", + dest="creator", help="Show the id that committed the" + " revision") + display.add_option("-m", "--merges", action="store_true", + dest="merges", help="Show the revisions that were" + " merged") + parser.add_option_group(display) + return parser + def help(self, parser=None): + """Attempt to explain the revisions command + + :param parser: If supplied, used to determine options + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """List revisions. + """ + help_tree_spec() + + +class Get(BaseCommand): + """ + Retrieve a revision from the archive + """ + def __init__(self): + self.description="Retrieve a revision from the archive" + self.parser=self.get_parser() + + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + + def do_command(self, cmdargs): + """ + Master function that perfoms the "get" command. + """ + (options, args) = self.parser.parse_args(cmdargs) + if len(args) < 1: + return self.help() + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + tree = None + + arch_loc = None + try: + revision, arch_loc = paths.full_path_decode(args[0]) + except Exception, e: + revision = cmdutil.determine_revision_arch(tree, args[0], + check_existence=False, allow_package=True) + if len(args) > 1: + directory = args[1] + else: + directory = str(revision.nonarch) + if os.path.exists(directory): + raise DirectoryExists(directory) + cmdutil.ensure_archive_registered(revision.archive, arch_loc) + try: + cmdutil.ensure_revision_exists(revision) + except cmdutil.NoSuchRevision, e: + raise CommandFailedWrapper(e) + + link = cmdutil.prompt ("get link") + for line in cmdutil.iter_get(revision, directory, link, + options.no_pristine, + options.no_greedy_add): + cmdutil.colorize(line) + + def get_parser(self): + """ + Returns the options parser to use for the "get" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai get revision [dir]") + parser.add_option("--no-pristine", action="store_true", + dest="no_pristine", + help="Do not make pristine copy for reference") + parser.add_option("--no-greedy-add", action="store_true", + dest="no_greedy_add", + help="Never add to greedy libraries") + + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Expands aliases and constructs a project tree for a revision. If the optional +"dir" argument is provided, the project tree will be stored in this directory. + """ + help_tree_spec() + return + +class PromptCmd(cmd.Cmd): + def __init__(self): + cmd.Cmd.__init__(self) + self.prompt = "Fai> " + try: + self.tree = arch.tree_root() + except: + self.tree = None + self.set_title() + self.set_prompt() + self.fake_aba = abacmds.AbaCmds() + self.identchars += '-' + self.history_file = os.path.expanduser("~/.fai-history") + readline.set_completer_delims(string.whitespace) + if os.access(self.history_file, os.R_OK) and \ + os.path.isfile(self.history_file): + readline.read_history_file(self.history_file) + self.cwd = os.getcwd() + + def write_history(self): + readline.write_history_file(self.history_file) + + def do_quit(self, args): + self.write_history() + sys.exit(0) + + def do_exit(self, args): + self.do_quit(args) + + def do_EOF(self, args): + print + self.do_quit(args) + + def postcmd(self, line, bar): + self.set_title() + self.set_prompt() + + def set_prompt(self): + if self.tree is not None: + try: + prompt = pylon.alias_or_version(self.tree.tree_version, + self.tree, + full=False) + if prompt is not None: + prompt = " " + prompt + except: + prompt = "" + else: + prompt = "" + self.prompt = "Fai%s> " % prompt + + def set_title(self, command=None): + try: + version = pylon.alias_or_version(self.tree.tree_version, self.tree, + full=False) + except: + version = "[no version]" + if command is None: + command = "" + sys.stdout.write(terminal.term_title("Fai %s %s" % (command, version))) + + def do_cd(self, line): + if line == "": + line = "~" + line = os.path.expanduser(line) + if os.path.isabs(line): + newcwd = line + else: + newcwd = self.cwd+'/'+line + newcwd = os.path.normpath(newcwd) + try: + os.chdir(newcwd) + self.cwd = newcwd + except Exception, e: + print e + try: + self.tree = arch.tree_root() + except: + self.tree = None + + def do_help(self, line): + Help()(line) + + def default(self, line): + args = line.split() + if find_command(args[0]): + try: + find_command(args[0]).do_command(args[1:]) + except cmdutil.BadCommandOption, e: + print e + except cmdutil.GetHelp, e: + find_command(args[0]).help() + except CommandFailed, e: + print e + except arch.errors.ArchiveNotRegistered, e: + print e + except KeyboardInterrupt, e: + print "Interrupted" + except arch.util.ExecProblem, e: + print e.proc.error.rstrip('\n') + except cmdutil.CantDetermineVersion, e: + print e + except cmdutil.CantDetermineRevision, e: + print e + except Exception, e: + print "Unhandled error:\n%s" % errors.exception_str(e) + + elif suggestions.has_key(args[0]): + print suggestions[args[0]] + + elif self.fake_aba.is_command(args[0]): + tree = None + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + pass + cmd = self.fake_aba.is_command(args[0]) + try: + cmd.run(cmdutil.expand_prefix_alias(args[1:], tree)) + except KeyboardInterrupt, e: + print "Interrupted" + + elif options.tla_fallthrough and args[0] != "rm" and \ + cmdutil.is_tla_command(args[0]): + try: + tree = None + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + pass + args = cmdutil.expand_prefix_alias(args, tree) + arch.util.exec_safe('tla', args, stderr=sys.stderr, + expected=(0, 1)) + except arch.util.ExecProblem, e: + pass + except KeyboardInterrupt, e: + print "Interrupted" + else: + try: + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + tree = None + args=line.split() + os.system(" ".join(cmdutil.expand_prefix_alias(args, tree))) + except KeyboardInterrupt, e: + print "Interrupted" + + def completenames(self, text, line, begidx, endidx): + completions = [] + iter = iter_command_names(self.fake_aba) + try: + if len(line) > 0: + arg = line.split()[-1] + else: + arg = "" + iter = cmdutil.iter_munged_completions(iter, arg, text) + except Exception, e: + print e + return list(iter) + + def completedefault(self, text, line, begidx, endidx): + """Perform completion for native commands. + + :param text: The text to complete + :type text: str + :param line: The entire line to complete + :type line: str + :param begidx: The start of the text in the line + :type begidx: int + :param endidx: The end of the text in the line + :type endidx: int + """ + try: + (cmd, args, foo) = self.parseline(line) + command_obj=find_command(cmd) + if command_obj is not None: + return command_obj.complete(args.split(), text) + elif not self.fake_aba.is_command(cmd) and \ + cmdutil.is_tla_command(cmd): + iter = cmdutil.iter_supported_switches(cmd) + if len(args) > 0: + arg = args.split()[-1] + else: + arg = "" + if arg.startswith("-"): + return list(cmdutil.iter_munged_completions(iter, arg, + text)) + else: + return list(cmdutil.iter_munged_completions( + cmdutil.iter_file_completions(arg), arg, text)) + + + elif cmd == "cd": + if len(args) > 0: + arg = args.split()[-1] + else: + arg = "" + iter = cmdutil.iter_dir_completions(arg) + iter = cmdutil.iter_munged_completions(iter, arg, text) + return list(iter) + elif len(args)>0: + arg = args.split()[-1] + iter = cmdutil.iter_file_completions(arg) + return list(cmdutil.iter_munged_completions(iter, arg, text)) + else: + return self.completenames(text, line, begidx, endidx) + except Exception, e: + print e + + +def iter_command_names(fake_aba): + for entry in cmdutil.iter_combine([commands.iterkeys(), + fake_aba.get_commands(), + cmdutil.iter_tla_commands(False)]): + if not suggestions.has_key(str(entry)): + yield entry + + +def iter_source_file_completions(tree, arg): + treepath = arch_compound.tree_cwd(tree) + if len(treepath) > 0: + dirs = [treepath] + else: + dirs = None + for file in tree.iter_inventory(dirs, source=True, both=True): + file = file_completion_match(file, treepath, arg) + if file is not None: + yield file + + +def iter_untagged(tree, dirs): + for file in arch_core.iter_inventory_filter(tree, dirs, tagged=False, + categories=arch_core.non_root, + control_files=True): + yield file.name + + +def iter_untagged_completions(tree, arg): + """Generate an iterator for all visible untagged files that match arg. + + :param tree: The tree to look for untagged files in + :type tree: `arch.WorkingTree` + :param arg: The argument to match + :type arg: str + :return: An iterator of all matching untagged files + :rtype: iterator of str + """ + treepath = arch_compound.tree_cwd(tree) + if len(treepath) > 0: + dirs = [treepath] + else: + dirs = None + + for file in iter_untagged(tree, dirs): + file = file_completion_match(file, treepath, arg) + if file is not None: + yield file + + +def file_completion_match(file, treepath, arg): + """Determines whether a file within an arch tree matches the argument. + + :param file: The rooted filename + :type file: str + :param treepath: The path to the cwd within the tree + :type treepath: str + :param arg: The prefix to match + :return: The completion name, or None if not a match + :rtype: str + """ + if not file.startswith(treepath): + return None + if treepath != "": + file = file[len(treepath)+1:] + + if not file.startswith(arg): + return None + if os.path.isdir(file): + file += '/' + return file + +def iter_modified_file_completions(tree, arg): + """Returns a list of modified files that match the specified prefix. + + :param tree: The current tree + :type tree: `arch.WorkingTree` + :param arg: The prefix to match + :type arg: str + """ + treepath = arch_compound.tree_cwd(tree) + tmpdir = util.tmpdir() + changeset = tmpdir+"/changeset" + completions = [] + revision = cmdutil.determine_revision_tree(tree) + for line in arch.iter_delta(revision, tree, changeset): + if isinstance(line, arch.FileModification): + file = file_completion_match(line.name[1:], treepath, arg) + if file is not None: + completions.append(file) + shutil.rmtree(tmpdir) + return completions + +class Shell(BaseCommand): + def __init__(self): + self.description = "Runs Fai as a shell" + + def do_command(self, cmdargs): + if len(cmdargs)!=0: + raise cmdutil.GetHelp + prompt = PromptCmd() + try: + prompt.cmdloop() + finally: + prompt.write_history() + +class AddID(BaseCommand): + """ + Adds an inventory id for the given file + """ + def __init__(self): + self.description="Add an inventory id for a given file" + + def get_completer(self, arg, index): + tree = arch.tree_root() + return iter_untagged_completions(tree, arg) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + + try: + tree = arch.tree_root() + except arch.errors.TreeRootError, e: + raise pylon.errors.CommandFailedWrapper(e) + + + if (len(args) == 0) == (options.untagged == False): + raise cmdutil.GetHelp + + #if options.id and len(args) != 1: + # print "If --id is specified, only one file can be named." + # return + + method = tree.tagging_method + + if options.id_type == "tagline": + if method != "tagline": + if not cmdutil.prompt("Tagline in other tree"): + if method == "explicit" or method == "implicit": + options.id_type == method + else: + print "add-id not supported for \"%s\" tagging method"\ + % method + return + + elif options.id_type == "implicit": + if method != "implicit": + if not cmdutil.prompt("Implicit in other tree"): + if method == "explicit" or method == "tagline": + options.id_type == method + else: + print "add-id not supported for \"%s\" tagging method"\ + % method + return + elif options.id_type == "explicit": + if method != "tagline" and method != explicit: + if not prompt("Explicit in other tree"): + print "add-id not supported for \"%s\" tagging method" % \ + method + return + + if options.id_type == "auto": + if method != "tagline" and method != "explicit" \ + and method !="implicit": + print "add-id not supported for \"%s\" tagging method" % method + return + else: + options.id_type = method + if options.untagged: + args = None + self.add_ids(tree, options.id_type, args) + + def add_ids(self, tree, id_type, files=()): + """Add inventory ids to files. + + :param tree: the tree the files are in + :type tree: `arch.WorkingTree` + :param id_type: the type of id to add: "explicit" or "tagline" + :type id_type: str + :param files: The list of files to add. If None do all untagged. + :type files: tuple of str + """ + + untagged = (files is None) + if untagged: + files = list(iter_untagged(tree, None)) + previous_files = [] + while len(files) > 0: + previous_files.extend(files) + if id_type == "explicit": + cmdutil.add_id(files) + elif id_type == "tagline" or id_type == "implicit": + for file in files: + try: + implicit = (id_type == "implicit") + cmdutil.add_tagline_or_explicit_id(file, False, + implicit) + except cmdutil.AlreadyTagged: + print "\"%s\" already has a tagline." % file + except cmdutil.NoCommentSyntax: + pass + #do inventory after tagging until no untagged files are encountered + if untagged: + files = [] + for file in iter_untagged(tree, None): + if not file in previous_files: + files.append(file) + + else: + break + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai add-id file1 [file2] [file3]...") +# ddaa suggests removing this to promote GUIDs. Let's see who squalks. +# parser.add_option("-i", "--id", dest="id", +# help="Specify id for a single file", default=None) + parser.add_option("--tltl", action="store_true", + dest="lord_style", help="Use Tom Lord's style of id.") + parser.add_option("--explicit", action="store_const", + const="explicit", dest="id_type", + help="Use an explicit id", default="auto") + parser.add_option("--tagline", action="store_const", + const="tagline", dest="id_type", + help="Use a tagline id") + parser.add_option("--implicit", action="store_const", + const="implicit", dest="id_type", + help="Use an implicit id (deprecated)") + parser.add_option("--untagged", action="store_true", + dest="untagged", default=False, + help="tag all untagged files") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Adds an inventory to the specified file(s) and directories. If --untagged is +specified, adds inventory to all untagged files and directories. + """ + return + + +class Merge(BaseCommand): + """ + Merges changes from other versions into the current tree + """ + def __init__(self): + self.description="Merges changes from other versions" + try: + self.tree = arch.tree_root() + except: + self.tree = None + + + def get_completer(self, arg, index): + if self.tree is None: + raise arch.errors.TreeRootError + return cmdutil.merge_completions(self.tree, arg, index) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "merge" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + if options.diff3: + action="star-merge" + else: + action = options.action + + if self.tree is None: + raise arch.errors.TreeRootError(os.getcwd()) + if cmdutil.has_changed(ancillary.comp_revision(self.tree)): + raise UncommittedChanges(self.tree) + + if len(args) > 0: + revisions = [] + for arg in args: + revisions.append(cmdutil.determine_revision_arch(self.tree, + arg)) + source = "from commandline" + else: + revisions = ancillary.iter_partner_revisions(self.tree, + self.tree.tree_version) + source = "from partner version" + revisions = misc.rewind_iterator(revisions) + try: + revisions.next() + revisions.rewind() + except StopIteration, e: + revision = cmdutil.tag_cur(self.tree) + if revision is None: + raise CantDetermineRevision("", "No version specified, no " + "partner-versions, and no tag" + " source") + revisions = [revision] + source = "from tag source" + for revision in revisions: + cmdutil.ensure_archive_registered(revision.archive) + cmdutil.colorize(arch.Chatter("* Merging %s [%s]" % + (revision, source))) + if action=="native-merge" or action=="update": + if self.native_merge(revision, action) == 0: + continue + elif action=="star-merge": + try: + self.star_merge(revision, options.diff3) + except errors.MergeProblem, e: + break + if cmdutil.has_changed(self.tree.tree_version): + break + + def star_merge(self, revision, diff3): + """Perform a star-merge on the current tree. + + :param revision: The revision to use for the merge + :type revision: `arch.Revision` + :param diff3: If true, do a diff3 merge + :type diff3: bool + """ + try: + for line in self.tree.iter_star_merge(revision, diff3=diff3): + cmdutil.colorize(line) + except arch.util.ExecProblem, e: + if e.proc.status is not None and e.proc.status == 1: + if e.proc.error: + print e.proc.error + raise MergeProblem + else: + raise + + def native_merge(self, other_revision, action): + """Perform a native-merge on the current tree. + + :param other_revision: The revision to use for the merge + :type other_revision: `arch.Revision` + :return: 0 if the merge was skipped, 1 if it was applied + """ + other_tree = arch_compound.find_or_make_local_revision(other_revision) + try: + if action == "native-merge": + ancestor = arch_compound.merge_ancestor2(self.tree, other_tree, + other_revision) + elif action == "update": + ancestor = arch_compound.tree_latest(self.tree, + other_revision.version) + except CantDetermineRevision, e: + raise CommandFailedWrapper(e) + cmdutil.colorize(arch.Chatter("* Found common ancestor %s" % ancestor)) + if (ancestor == other_revision): + cmdutil.colorize(arch.Chatter("* Skipping redundant merge" + % ancestor)) + return 0 + delta = cmdutil.apply_delta(ancestor, other_tree, self.tree) + for line in cmdutil.iter_apply_delta_filter(delta): + cmdutil.colorize(line) + return 1 + + + + def get_parser(self): + """ + Returns the options parser to use for the "merge" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai merge [VERSION]") + parser.add_option("-s", "--star-merge", action="store_const", + dest="action", help="Use star-merge", + const="star-merge", default="native-merge") + parser.add_option("--update", action="store_const", + dest="action", help="Use update picker", + const="update") + parser.add_option("--diff3", action="store_true", + dest="diff3", + help="Use diff3 for merge (implies star-merge)") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Performs a merge operation using the specified version. + """ + return + +class ELog(BaseCommand): + """ + Produces a raw patchlog and invokes the user's editor + """ + def __init__(self): + self.description="Edit a patchlog to commit" + try: + self.tree = arch.tree_root() + except: + self.tree = None + + + def do_command(self, cmdargs): + """ + Master function that perfoms the "elog" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + if self.tree is None: + raise arch.errors.TreeRootError + + try: + edit_log(self.tree, self.tree.tree_version) + except pylon.errors.NoEditorSpecified, e: + raise pylon.errors.CommandFailedWrapper(e) + + def get_parser(self): + """ + Returns the options parser to use for the "merge" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai elog") + return parser + + + def help(self, parser=None): + """ + Invokes $EDITOR to produce a log for committing. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Invokes $EDITOR to produce a log for committing. + """ + return + +def edit_log(tree, version): + """Makes and edits the log for a tree. Does all kinds of fancy things + like log templates and merge summaries and log-for-merge + + :param tree: The tree to edit the log for + :type tree: `arch.WorkingTree` + """ + #ensure we have an editor before preparing the log + cmdutil.find_editor() + log = tree.log_message(create=False, version=version) + log_is_new = False + if log is None or cmdutil.prompt("Overwrite log"): + if log is not None: + os.remove(log.name) + log = tree.log_message(create=True, version=version) + log_is_new = True + tmplog = log.name + template = pylon.log_template_path(tree) + if template: + shutil.copyfile(template, tmplog) + comp_version = ancillary.comp_revision(tree).version + new_merges = cmdutil.iter_new_merges(tree, comp_version) + new_merges = cmdutil.direct_merges(new_merges) + log["Summary"] = pylon.merge_summary(new_merges, + version) + if len(new_merges) > 0: + if cmdutil.prompt("Log for merge"): + if cmdutil.prompt("changelog for merge"): + mergestuff = "Patches applied:\n" + mergestuff += pylon.changelog_for_merge(new_merges) + else: + mergestuff = cmdutil.log_for_merge(tree, comp_version) + log.description += mergestuff + log.save() + try: + cmdutil.invoke_editor(log.name) + except: + if log_is_new: + os.remove(log.name) + raise + + +class MirrorArchive(BaseCommand): + """ + Updates a mirror from an archive + """ + def __init__(self): + self.description="Update a mirror from an archive" + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + if len(args) > 1: + raise GetHelp + try: + tree = arch.tree_root() + except: + tree = None + + if len(args) == 0: + if tree is not None: + name = tree.tree_version() + else: + name = cmdutil.expand_alias(args[0], tree) + name = arch.NameParser(name) + + to_arch = name.get_archive() + from_arch = cmdutil.get_mirror_source(arch.Archive(to_arch)) + limit = name.get_nonarch() + + iter = arch_core.mirror_archive(from_arch,to_arch, limit) + for line in arch.chatter_classifier(iter): + cmdutil.colorize(line) + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai mirror-archive ARCHIVE") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Updates a mirror from an archive. If a branch, package, or version is +supplied, only changes under it are mirrored. + """ + return + +def help_tree_spec(): + print """Specifying revisions (default: tree) +Revisions may be specified by alias, revision, version or patchlevel. +Revisions or versions may be fully qualified. Unqualified revisions, versions, +or patchlevels use the archive of the current project tree. Versions will +use the latest patchlevel in the tree. Patchlevels will use the current tree- +version. + +Use "alias" to list available (user and automatic) aliases.""" + +auto_alias = [ +"acur", +"The latest revision in the archive of the tree-version. You can specify \ +a different version like so: acur:foo--bar--0 (aliases can be used)", +"tcur", +"""(tree current) The latest revision in the tree of the tree-version. \ +You can specify a different version like so: tcur:foo--bar--0 (aliases can be \ +used).""", +"tprev" , +"""(tree previous) The previous revision in the tree of the tree-version. To \ +specify an older revision, use a number, e.g. "tprev:4" """, +"tanc" , +"""(tree ancestor) The ancestor revision of the tree To specify an older \ +revision, use a number, e.g. "tanc:4".""", +"tdate" , +"""(tree date) The latest revision from a given date, e.g. "tdate:July 6".""", +"tmod" , +""" (tree modified) The latest revision to modify a given file, e.g. \ +"tmod:engine.cpp" or "tmod:engine.cpp:16".""", +"ttag" , +"""(tree tag) The revision that was tagged into the current tree revision, \ +according to the tree""", +"tagcur", +"""(tag current) The latest revision of the version that the current tree \ +was tagged from.""", +"mergeanc" , +"""The common ancestor of the current tree and the specified revision. \ +Defaults to the first partner-version's latest revision or to tagcur.""", +] + + +def is_auto_alias(name): + """Determine whether a name is an auto alias name + + :param name: the name to check + :type name: str + :return: True if the name is an auto alias, false if not + :rtype: bool + """ + return name in [f for (f, v) in pylon.util.iter_pairs(auto_alias)] + + +def display_def(iter, wrap = 80): + """Display a list of definitions + + :param iter: iter of name, definition pairs + :type iter: iter of (str, str) + :param wrap: The width for text wrapping + :type wrap: int + """ + vals = list(iter) + maxlen = 0 + for (key, value) in vals: + if len(key) > maxlen: + maxlen = len(key) + for (key, value) in vals: + tw=textwrap.TextWrapper(width=wrap, + initial_indent=key.rjust(maxlen)+" : ", + subsequent_indent="".rjust(maxlen+3)) + print tw.fill(value) + + +def help_aliases(tree): + print """Auto-generated aliases""" + display_def(pylon.util.iter_pairs(auto_alias)) + print "User aliases" + display_def(ancillary.iter_all_alias(tree)) + +class Inventory(BaseCommand): + """List the status of files in the tree""" + def __init__(self): + self.description=self.__doc__ + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + tree = arch.tree_root() + categories = [] + + if (options.source): + categories.append(arch_core.SourceFile) + if (options.precious): + categories.append(arch_core.PreciousFile) + if (options.backup): + categories.append(arch_core.BackupFile) + if (options.junk): + categories.append(arch_core.JunkFile) + + if len(categories) == 1: + show_leading = False + else: + show_leading = True + + if len(categories) == 0: + categories = None + + if options.untagged: + categories = arch_core.non_root + show_leading = False + tagged = False + else: + tagged = None + + for file in arch_core.iter_inventory_filter(tree, None, + control_files=options.control_files, + categories = categories, tagged=tagged): + print arch_core.file_line(file, + category = show_leading, + untagged = show_leading, + id = options.ids) + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai inventory [options]") + parser.add_option("--ids", action="store_true", dest="ids", + help="Show file ids") + parser.add_option("--control", action="store_true", + dest="control_files", help="include control files") + parser.add_option("--source", action="store_true", dest="source", + help="List source files") + parser.add_option("--backup", action="store_true", dest="backup", + help="List backup files") + parser.add_option("--precious", action="store_true", dest="precious", + help="List precious files") + parser.add_option("--junk", action="store_true", dest="junk", + help="List junk files") + parser.add_option("--unrecognized", action="store_true", + dest="unrecognized", help="List unrecognized files") + parser.add_option("--untagged", action="store_true", + dest="untagged", help="List only untagged files") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Lists the status of files in the archive: +S source +P precious +B backup +J junk +U unrecognized +T tree root +? untagged-source +Leading letter are not displayed if only one kind of file is shown + """ + return + + +class Alias(BaseCommand): + """List or adjust aliases""" + def __init__(self): + self.description=self.__doc__ + + def get_completer(self, arg, index): + if index > 2: + return () + try: + self.tree = arch.tree_root() + except: + self.tree = None + + if index == 0: + return [part[0]+" " for part in ancillary.iter_all_alias(self.tree)] + elif index == 1: + return cmdutil.iter_revision_completions(arg, self.tree) + + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: + self.tree = arch.tree_root() + except: + self.tree = None + + + try: + options.action(args, options) + except cmdutil.ForbiddenAliasSyntax, e: + raise CommandFailedWrapper(e) + + def no_prefix(self, alias): + if alias.startswith("^"): + alias = alias[1:] + return alias + + def arg_dispatch(self, args, options): + """Add, modify, or list aliases, depending on number of arguments + + :param args: The list of commandline arguments + :type args: list of str + :param options: The commandline options + """ + if len(args) == 0: + help_aliases(self.tree) + return + else: + alias = self.no_prefix(args[0]) + if len(args) == 1: + self.print_alias(alias) + elif (len(args)) == 2: + self.add(alias, args[1], options) + else: + raise cmdutil.GetHelp + + def print_alias(self, alias): + answer = None + if is_auto_alias(alias): + raise pylon.errors.IsAutoAlias(alias, "\"%s\" is an auto alias." + " Use \"revision\" to expand auto aliases." % alias) + for pair in ancillary.iter_all_alias(self.tree): + if pair[0] == alias: + answer = pair[1] + if answer is not None: + print answer + else: + print "The alias %s is not assigned." % alias + + def add(self, alias, expansion, options): + """Add or modify aliases + + :param alias: The alias name to create/modify + :type alias: str + :param expansion: The expansion to assign to the alias name + :type expansion: str + :param options: The commandline options + """ + if is_auto_alias(alias): + raise IsAutoAlias(alias) + newlist = "" + written = False + new_line = "%s=%s\n" % (alias, cmdutil.expand_alias(expansion, + self.tree)) + ancillary.check_alias(new_line.rstrip("\n"), [alias, expansion]) + + for pair in self.get_iterator(options): + if pair[0] != alias: + newlist+="%s=%s\n" % (pair[0], pair[1]) + elif not written: + newlist+=new_line + written = True + if not written: + newlist+=new_line + self.write_aliases(newlist, options) + + def delete(self, args, options): + """Delete the specified alias + + :param args: The list of arguments + :type args: list of str + :param options: The commandline options + """ + deleted = False + if len(args) != 1: + raise cmdutil.GetHelp + alias = self.no_prefix(args[0]) + if is_auto_alias(alias): + raise IsAutoAlias(alias) + newlist = "" + for pair in self.get_iterator(options): + if pair[0] != alias: + newlist+="%s=%s\n" % (pair[0], pair[1]) + else: + deleted = True + if not deleted: + raise errors.NoSuchAlias(alias) + self.write_aliases(newlist, options) + + def get_alias_file(self, options): + """Return the name of the alias file to use + + :param options: The commandline options + """ + if options.tree: + if self.tree is None: + self.tree == arch.tree_root() + return str(self.tree)+"/{arch}/+aliases" + else: + return "~/.aba/aliases" + + def get_iterator(self, options): + """Return the alias iterator to use + + :param options: The commandline options + """ + return ancillary.iter_alias(self.get_alias_file(options)) + + def write_aliases(self, newlist, options): + """Safely rewrite the alias file + :param newlist: The new list of aliases + :type newlist: str + :param options: The commandline options + """ + filename = os.path.expanduser(self.get_alias_file(options)) + file = util.NewFileVersion(filename) + file.write(newlist) + file.commit() + + + def get_parser(self): + """ + Returns the options parser to use for the "alias" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai alias [ALIAS] [NAME]") + parser.add_option("-d", "--delete", action="store_const", dest="action", + const=self.delete, default=self.arg_dispatch, + help="Delete an alias") + parser.add_option("--tree", action="store_true", dest="tree", + help="Create a per-tree alias", default=False) + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Lists current aliases or modifies the list of aliases. + +If no arguments are supplied, aliases will be listed. If two arguments are +supplied, the specified alias will be created or modified. If -d or --delete +is supplied, the specified alias will be deleted. + +You can create aliases that refer to any fully-qualified part of the +Arch namespace, e.g. +archive, +archive/category, +archive/category--branch, +archive/category--branch--version (my favourite) +archive/category--branch--version--patchlevel + +Aliases can be used automatically by native commands. To use them +with external or tla commands, prefix them with ^ (you can do this +with native commands, too). +""" + + +class RequestMerge(BaseCommand): + """Submit a merge request to Bug Goo""" + def __init__(self): + self.description=self.__doc__ + + def do_command(self, cmdargs): + """Submit a merge request + + :param cmdargs: The commandline arguments + :type cmdargs: list of str + """ + parser = self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: + cmdutil.find_editor() + except pylon.errors.NoEditorSpecified, e: + raise pylon.errors.CommandFailedWrapper(e) + try: + self.tree=arch.tree_root() + except: + self.tree=None + base, revisions = self.revision_specs(args) + message = self.make_headers(base, revisions) + message += self.make_summary(revisions) + path = self.edit_message(message) + message = self.tidy_message(path) + if cmdutil.prompt("Send merge"): + self.send_message(message) + print "Merge request sent" + + def make_headers(self, base, revisions): + """Produce email and Bug Goo header strings + + :param base: The base revision to apply merges to + :type base: `arch.Revision` + :param revisions: The revisions to replay into the base + :type revisions: list of `arch.Patchlog` + :return: The headers + :rtype: str + """ + headers = "To: gnu-arch-users@gnu.org\n" + headers += "From: %s\n" % options.fromaddr + if len(revisions) == 1: + headers += "Subject: [MERGE REQUEST] %s\n" % revisions[0].summary + else: + headers += "Subject: [MERGE REQUEST]\n" + headers += "\n" + headers += "Base-Revision: %s\n" % base + for revision in revisions: + headers += "Revision: %s\n" % revision.revision + headers += "Bug: \n\n" + return headers + + def make_summary(self, logs): + """Generate a summary of merges + + :param logs: the patchlogs that were directly added by the merges + :type logs: list of `arch.Patchlog` + :return: the summary + :rtype: str + """ + summary = "" + for log in logs: + summary+=str(log.revision)+"\n" + summary+=log.summary+"\n" + if log.description.strip(): + summary+=log.description.strip('\n')+"\n\n" + return summary + + def revision_specs(self, args): + """Determine the base and merge revisions from tree and arguments. + + :param args: The parsed arguments + :type args: list of str + :return: The base revision and merge revisions + :rtype: `arch.Revision`, list of `arch.Patchlog` + """ + if len(args) > 0: + target_revision = cmdutil.determine_revision_arch(self.tree, + args[0]) + else: + target_revision = arch_compound.tree_latest(self.tree) + if len(args) > 1: + merges = [ arch.Patchlog(cmdutil.determine_revision_arch( + self.tree, f)) for f in args[1:] ] + else: + if self.tree is None: + raise CantDetermineRevision("", "Not in a project tree") + merge_iter = cmdutil.iter_new_merges(self.tree, + target_revision.version, + False) + merges = [f for f in cmdutil.direct_merges(merge_iter)] + return (target_revision, merges) + + def edit_message(self, message): + """Edit an email message in the user's standard editor + + :param message: The message to edit + :type message: str + :return: the path of the edited message + :rtype: str + """ + if self.tree is None: + path = os.get_cwd() + else: + path = self.tree + path += "/,merge-request" + file = open(path, 'w') + file.write(message) + file.flush() + cmdutil.invoke_editor(path) + return path + + def tidy_message(self, path): + """Validate and clean up message. + + :param path: The path to the message to clean up + :type path: str + :return: The parsed message + :rtype: `email.Message` + """ + mail = email.message_from_file(open(path)) + if mail["Subject"].strip() == "[MERGE REQUEST]": + raise BlandSubject + + request = email.message_from_string(mail.get_payload()) + if request.has_key("Bug"): + if request["Bug"].strip()=="": + del request["Bug"] + mail.set_payload(request.as_string()) + return mail + + def send_message(self, message): + """Send a message, using its headers to address it. + + :param message: The message to send + :type message: `email.Message`""" + server = smtplib.SMTP("localhost") + server.sendmail(message['From'], message['To'], message.as_string()) + server.quit() + + def help(self, parser=None): + """Print a usage message + + :param parser: The options parser to use + :type parser: `cmdutil.CmdOptionParser` + """ + if parser is None: + parser = self.get_parser() + parser.print_help() + print """ +Sends a merge request formatted for Bug Goo. Intended use: get the tree +you'd like to merge into. Apply the merges you want. Invoke request-merge. +The merge request will open in your $EDITOR. + +When no TARGET is specified, it uses the current tree revision. When +no MERGE is specified, it uses the direct merges (as in "revisions +--direct-merges"). But you can specify just the TARGET, or all the MERGE +revisions. +""" + + def get_parser(self): + """Produce a commandline parser for this command. + + :rtype: `cmdutil.CmdOptionParser` + """ + parser=cmdutil.CmdOptionParser("request-merge [TARGET] [MERGE1...]") + return parser + +commands = { +'changes' : Changes, +'help' : Help, +'update': Update, +'apply-changes':ApplyChanges, +'cat-log': CatLog, +'commit': Commit, +'revision': Revision, +'revisions': Revisions, +'get': Get, +'revert': Revert, +'shell': Shell, +'add-id': AddID, +'merge': Merge, +'elog': ELog, +'mirror-archive': MirrorArchive, +'ninventory': Inventory, +'alias' : Alias, +'request-merge': RequestMerge, +} + +def my_import(mod_name): + module = __import__(mod_name) + components = mod_name.split('.') + for comp in components[1:]: + module = getattr(module, comp) + return module + +def plugin(mod_name): + module = my_import(mod_name) + module.add_command(commands) + +for file in os.listdir(sys.path[0]+"/command"): + if len(file) > 3 and file[-3:] == ".py" and file != "__init__.py": + plugin("command."+file[:-3]) + +suggestions = { +'apply-delta' : "Try \"apply-changes\".", +'delta' : "To compare two revisions, use \"changes\".", +'diff-rev' : "To compare two revisions, use \"changes\".", +'undo' : "To undo local changes, use \"revert\".", +'undelete' : "To undo only deletions, use \"revert --deletions\"", +'missing-from' : "Try \"revisions --missing-from\".", +'missing' : "Try \"revisions --missing\".", +'missing-merge' : "Try \"revisions --partner-missing\".", +'new-merges' : "Try \"revisions --new-merges\".", +'cachedrevs' : "Try \"revisions --cacherevs\". (no 'd')", +'logs' : "Try \"revisions --logs\"", +'tree-source' : "Use the \"^ttag\" alias (\"revision ^ttag\")", +'latest-revision' : "Use the \"^acur\" alias (\"revision ^acur\")", +'change-version' : "Try \"update REVISION\"", +'tree-revision' : "Use the \"^tcur\" alias (\"revision ^tcur\")", +'rev-depends' : "Use revisions --dependencies", +'auto-get' : "Plain get will do archive lookups", +'tagline' : "Use add-id. It uses taglines in tagline trees", +'emlog' : "Use elog. It automatically adds log-for-merge text, if any", +'library-revisions' : "Use revisions --library", +'file-revert' : "Use revert FILE", +'join-branch' : "Use replay --logs-only" +} +# arch-tag: 19d5739d-3708-486c-93ba-deecc3027fc7 *** added file 'testdata/orig' --- /dev/null +++ testdata/orig @@ -0,0 +1,2789 @@ +# Copyright (C) 2004 Aaron Bentley +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import sys +import arch +import arch.util +import arch.arch +import abacmds +import cmdutil +import shutil +import os +import options +import paths +import time +import cmd +import readline +import re +import string +import arch_core +from errors import * +import errors +import terminal +import ancillary +import misc +import email +import smtplib + +__docformat__ = "restructuredtext" +__doc__ = "Implementation of user (sub) commands" +commands = {} + +def find_command(cmd): + """ + Return an instance of a command type. Return None if the type isn't + registered. + + :param cmd: the name of the command to look for + :type cmd: the type of the command + """ + if commands.has_key(cmd): + return commands[cmd]() + else: + return None + +class BaseCommand: + def __call__(self, cmdline): + try: + self.do_command(cmdline.split()) + except cmdutil.GetHelp, e: + self.help() + except Exception, e: + print e + + def get_completer(index): + return None + + def complete(self, args, text): + """ + Returns a list of possible completions for the given text. + + :param args: The complete list of arguments + :type args: List of str + :param text: text to complete (may be shorter than args[-1]) + :type text: str + :rtype: list of str + """ + matches = [] + candidates = None + + if len(args) > 0: + realtext = args[-1] + else: + realtext = "" + + try: + parser=self.get_parser() + if realtext.startswith('-'): + candidates = parser.iter_options() + else: + (options, parsed_args) = parser.parse_args(args) + + if len (parsed_args) > 0: + candidates = self.get_completer(parsed_args[-1], len(parsed_args) -1) + else: + candidates = self.get_completer("", 0) + except: + pass + if candidates is None: + return + for candidate in candidates: + candidate = str(candidate) + if candidate.startswith(realtext): + matches.append(candidate[len(realtext)- len(text):]) + return matches + + +class Help(BaseCommand): + """ + Lists commands, prints help messages. + """ + def __init__(self): + self.description="Prints help mesages" + self.parser = None + + def do_command(self, cmdargs): + """ + Prints a help message. + """ + options, args = self.get_parser().parse_args(cmdargs) + if len(args) > 1: + raise cmdutil.GetHelp + + if options.native or options.suggestions or options.external: + native = options.native + suggestions = options.suggestions + external = options.external + else: + native = True + suggestions = False + external = True + + if len(args) == 0: + self.list_commands(native, suggestions, external) + return + elif len(args) == 1: + command_help(args[0]) + return + + def help(self): + self.get_parser().print_help() + print """ +If no command is specified, commands are listed. If a command is +specified, help for that command is listed. + """ + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + if self.parser is not None: + return self.parser + parser=cmdutil.CmdOptionParser("fai help [command]") + parser.add_option("-n", "--native", action="store_true", + dest="native", help="Show native commands") + parser.add_option("-e", "--external", action="store_true", + dest="external", help="Show external commands") + parser.add_option("-s", "--suggest", action="store_true", + dest="suggestions", help="Show suggestions") + self.parser = parser + return parser + + def list_commands(self, native=True, suggest=False, external=True): + """ + Lists supported commands. + + :param native: list native, python-based commands + :type native: bool + :param external: list external aba-style commands + :type external: bool + """ + if native: + print "Native Fai commands" + keys=commands.keys() + keys.sort() + for k in keys: + space="" + for i in range(28-len(k)): + space+=" " + print space+k+" : "+commands[k]().description + print + if suggest: + print "Unavailable commands and suggested alternatives" + key_list = suggestions.keys() + key_list.sort() + for key in key_list: + print "%28s : %s" % (key, suggestions[key]) + print + if external: + fake_aba = abacmds.AbaCmds() + if (fake_aba.abadir == ""): + return + print "External commands" + fake_aba.list_commands() + print + if not suggest: + print "Use help --suggest to list alternatives to tla and aba"\ + " commands." + if options.tla_fallthrough and (native or external): + print "Fai also supports tla commands." + +def command_help(cmd): + """ + Prints help for a command. + + :param cmd: The name of the command to print help for + :type cmd: str + """ + fake_aba = abacmds.AbaCmds() + cmdobj = find_command(cmd) + if cmdobj != None: + cmdobj.help() + elif suggestions.has_key(cmd): + print "Not available\n" + suggestions[cmd] + else: + abacmd = fake_aba.is_command(cmd) + if abacmd: + abacmd.help() + else: + print "No help is available for \""+cmd+"\". Maybe try \"tla "+cmd+" -H\"?" + + + +class Changes(BaseCommand): + """ + the "changes" command: lists differences between trees/revisions: + """ + + def __init__(self): + self.description="Lists what files have changed in the project tree" + + def get_completer(self, arg, index): + if index > 1: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def parse_commandline(self, cmdline): + """ + Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. + + :param cmdline: A list of arguments to parse + :rtype: (options, Revision, Revision/WorkingTree) + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdline) + if len(args) > 2: + raise cmdutil.GetHelp + + tree=arch.tree_root() + if len(args) == 0: + a_spec = cmdutil.comp_revision(tree) + else: + a_spec = cmdutil.determine_revision_tree(tree, args[0]) + cmdutil.ensure_archive_registered(a_spec.archive) + if len(args) == 2: + b_spec = cmdutil.determine_revision_tree(tree, args[1]) + cmdutil.ensure_archive_registered(b_spec.archive) + else: + b_spec=tree + return options, a_spec, b_spec + + def do_command(self, cmdargs): + """ + Master function that perfoms the "changes" command. + """ + try: + options, a_spec, b_spec = self.parse_commandline(cmdargs); + except cmdutil.CantDetermineRevision, e: + print e + return + except arch.errors.TreeRootError, e: + print e + return + if options.changeset: + changeset=options.changeset + tmpdir = None + else: + tmpdir=cmdutil.tmpdir() + changeset=tmpdir+"/changeset" + try: + delta=arch.iter_delta(a_spec, b_spec, changeset) + try: + for line in delta: + if cmdutil.chattermatch(line, "changeset:"): + pass + else: + cmdutil.colorize(line, options.suppress_chatter) + except arch.util.ExecProblem, e: + if e.proc.error and e.proc.error.startswith( + "missing explicit id for file"): + raise MissingID(e) + else: + raise + status=delta.status + if status > 1: + return + if (options.perform_diff): + chan = cmdutil.ChangesetMunger(changeset) + chan.read_indices() + if isinstance(b_spec, arch.Revision): + b_dir = b_spec.library_find() + else: + b_dir = b_spec + a_dir = a_spec.library_find() + if options.diffopts is not None: + diffopts = options.diffopts.split() + cmdutil.show_custom_diffs(chan, diffopts, a_dir, b_dir) + else: + cmdutil.show_diffs(delta.changeset) + finally: + if tmpdir and (os.access(tmpdir, os.X_OK)): + shutil.rmtree(tmpdir) + + def get_parser(self): + """ + Returns the options parser to use for the "changes" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai changes [options] [revision]" + " [revision]") + parser.add_option("-d", "--diff", action="store_true", + dest="perform_diff", default=False, + help="Show diffs in summary") + parser.add_option("-c", "--changeset", dest="changeset", + help="Store a changeset in the given directory", + metavar="DIRECTORY") + parser.add_option("-s", "--silent", action="store_true", + dest="suppress_chatter", default=False, + help="Suppress chatter messages") + parser.add_option("--diffopts", dest="diffopts", + help="Use the specified diff options", + metavar="OPTIONS") + + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser is None: + parser=self.get_parser() + parser.print_help() + print """ +Performs source-tree comparisons + +If no revision is specified, the current project tree is compared to the +last-committed revision. If one revision is specified, the current project +tree is compared to that revision. If two revisions are specified, they are +compared to each other. + """ + help_tree_spec() + return + + +class ApplyChanges(BaseCommand): + """ + Apply differences between two revisions to a tree + """ + + def __init__(self): + self.description="Applies changes to a project tree" + + def get_completer(self, arg, index): + if index > 1: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def parse_commandline(self, cmdline, tree): + """ + Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. + + :param cmdline: A list of arguments to parse + :rtype: (options, Revision, Revision/WorkingTree) + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdline) + if len(args) != 2: + raise cmdutil.GetHelp + + a_spec = cmdutil.determine_revision_tree(tree, args[0]) + cmdutil.ensure_archive_registered(a_spec.archive) + b_spec = cmdutil.determine_revision_tree(tree, args[1]) + cmdutil.ensure_archive_registered(b_spec.archive) + return options, a_spec, b_spec + + def do_command(self, cmdargs): + """ + Master function that performs "apply-changes". + """ + try: + tree = arch.tree_root() + options, a_spec, b_spec = self.parse_commandline(cmdargs, tree); + except cmdutil.CantDetermineRevision, e: + print e + return + except arch.errors.TreeRootError, e: + print e + return + delta=cmdutil.apply_delta(a_spec, b_spec, tree) + for line in cmdutil.iter_apply_delta_filter(delta): + cmdutil.colorize(line, options.suppress_chatter) + + def get_parser(self): + """ + Returns the options parser to use for the "apply-changes" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai apply-changes [options] revision" + " revision") + parser.add_option("-d", "--diff", action="store_true", + dest="perform_diff", default=False, + help="Show diffs in summary") + parser.add_option("-c", "--changeset", dest="changeset", + help="Store a changeset in the given directory", + metavar="DIRECTORY") + parser.add_option("-s", "--silent", action="store_true", + dest="suppress_chatter", default=False, + help="Suppress chatter messages") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser is None: + parser=self.get_parser() + parser.print_help() + print """ +Applies changes to a project tree + +Compares two revisions and applies the difference between them to the current +tree. + """ + help_tree_spec() + return + +class Update(BaseCommand): + """ + Updates a project tree to a given revision, preserving un-committed hanges. + """ + + def __init__(self): + self.description="Apply the latest changes to the current directory" + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def parse_commandline(self, cmdline, tree): + """ + Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. + + :param cmdline: A list of arguments to parse + :rtype: (options, Revision, Revision/WorkingTree) + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdline) + if len(args) > 2: + raise cmdutil.GetHelp + + spec=None + if len(args)>0: + spec=args[0] + revision=cmdutil.determine_revision_arch(tree, spec) + cmdutil.ensure_archive_registered(revision.archive) + + mirror_source = cmdutil.get_mirror_source(revision.archive) + if mirror_source != None: + if cmdutil.prompt("Mirror update"): + cmd=cmdutil.mirror_archive(mirror_source, + revision.archive, arch.NameParser(revision).get_package_version()) + for line in arch.chatter_classifier(cmd): + cmdutil.colorize(line, options.suppress_chatter) + + revision=cmdutil.determine_revision_arch(tree, spec) + + return options, revision + + def do_command(self, cmdargs): + """ + Master function that perfoms the "update" command. + """ + tree=arch.tree_root() + try: + options, to_revision = self.parse_commandline(cmdargs, tree); + except cmdutil.CantDetermineRevision, e: + print e + return + except arch.errors.TreeRootError, e: + print e + return + from_revision=cmdutil.tree_latest(tree) + if from_revision==to_revision: + print "Tree is already up to date with:\n"+str(to_revision)+"." + return + cmdutil.ensure_archive_registered(from_revision.archive) + cmd=cmdutil.apply_delta(from_revision, to_revision, tree, + options.patch_forward) + for line in cmdutil.iter_apply_delta_filter(cmd): + cmdutil.colorize(line) + if to_revision.version != tree.tree_version: + if cmdutil.prompt("Update version"): + tree.tree_version = to_revision.version + + def get_parser(self): + """ + Returns the options parser to use for the "update" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai update [options]" + " [revision/version]") + parser.add_option("-f", "--forward", action="store_true", + dest="patch_forward", default=False, + help="pass the --forward option to 'patch'") + parser.add_option("-s", "--silent", action="store_true", + dest="suppress_chatter", default=False, + help="Suppress chatter messages") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser is None: + parser=self.get_parser() + parser.print_help() + print """ +Updates a working tree to the current archive revision + +If a revision or version is specified, that is used instead + """ + help_tree_spec() + return + + +class Commit(BaseCommand): + """ + Create a revision based on the changes in the current tree. + """ + + def __init__(self): + self.description="Write local changes to the archive" + + def get_completer(self, arg, index): + if arg is None: + arg = "" + return iter_modified_file_completions(arch.tree_root(), arg) +# return iter_source_file_completions(arch.tree_root(), arg) + + def parse_commandline(self, cmdline, tree): + """ + Parse commandline arguments. Raise cmtutil.GetHelp if help is needed. + + :param cmdline: A list of arguments to parse + :rtype: (options, Revision, Revision/WorkingTree) + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdline) + + if len(args) == 0: + args = None + revision=cmdutil.determine_revision_arch(tree, options.version) + return options, revision.get_version(), args + + def do_command(self, cmdargs): + """ + Master function that perfoms the "commit" command. + """ + tree=arch.tree_root() + options, version, files = self.parse_commandline(cmdargs, tree) + if options.__dict__.has_key("base") and options.base: + base = cmdutil.determine_revision_tree(tree, options.base) + else: + base = cmdutil.submit_revision(tree) + + writeversion=version + archive=version.archive + source=cmdutil.get_mirror_source(archive) + allow_old=False + writethrough="implicit" + + if source!=None: + if writethrough=="explicit" and \ + cmdutil.prompt("Writethrough"): + writeversion=arch.Version(str(source)+"/"+str(version.get_nonarch())) + elif writethrough=="none": + raise CommitToMirror(archive) + + elif archive.is_mirror: + raise CommitToMirror(archive) + + try: + last_revision=tree.iter_logs(version, True).next().revision + except StopIteration, e: + if cmdutil.prompt("Import from commit"): + return do_import(version) + else: + raise NoVersionLogs(version) + if last_revision!=version.iter_revisions(True).next(): + if not cmdutil.prompt("Out of date"): + raise OutOfDate + else: + allow_old=True + + try: + if not cmdutil.has_changed(version): + if not cmdutil.prompt("Empty commit"): + raise EmptyCommit + except arch.util.ExecProblem, e: + if e.proc.error and e.proc.error.startswith( + "missing explicit id for file"): + raise MissingID(e) + else: + raise + log = tree.log_message(create=False) + if log is None: + try: + if cmdutil.prompt("Create log"): + edit_log(tree) + + except cmdutil.NoEditorSpecified, e: + raise CommandFailed(e) + log = tree.log_message(create=False) + if log is None: + raise NoLogMessage + if log["Summary"] is None or len(log["Summary"].strip()) == 0: + if not cmdutil.prompt("Omit log summary"): + raise errors.NoLogSummary + try: + for line in tree.iter_commit(version, seal=options.seal_version, + base=base, out_of_date_ok=allow_old, file_list=files): + cmdutil.colorize(line, options.suppress_chatter) + + except arch.util.ExecProblem, e: + if e.proc.error and e.proc.error.startswith( + "These files violate naming conventions:"): + raise LintFailure(e.proc.error) + else: + raise + + def get_parser(self): + """ + Returns the options parser to use for the "commit" command. + + :rtype: cmdutil.CmdOptionParser + """ + + parser=cmdutil.CmdOptionParser("fai commit [options] [file1]" + " [file2...]") + parser.add_option("--seal", action="store_true", + dest="seal_version", default=False, + help="seal this version") + parser.add_option("-v", "--version", dest="version", + help="Use the specified version", + metavar="VERSION") + parser.add_option("-s", "--silent", action="store_true", + dest="suppress_chatter", default=False, + help="Suppress chatter messages") + if cmdutil.supports_switch("commit", "--base"): + parser.add_option("--base", dest="base", help="", + metavar="REVISION") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser is None: + parser=self.get_parser() + parser.print_help() + print """ +Updates a working tree to the current archive revision + +If a version is specified, that is used instead + """ +# help_tree_spec() + return + + + +class CatLog(BaseCommand): + """ + Print the log of a given file (from current tree) + """ + def __init__(self): + self.description="Prints the patch log for a revision" + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "cat-log" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: + tree = arch.tree_root() + except arch.errors.TreeRootError, e: + tree = None + spec=None + if len(args) > 0: + spec=args[0] + if len(args) > 1: + raise cmdutil.GetHelp() + try: + if tree: + revision = cmdutil.determine_revision_tree(tree, spec) + else: + revision = cmdutil.determine_revision_arch(tree, spec) + except cmdutil.CantDetermineRevision, e: + raise CommandFailedWrapper(e) + log = None + + use_tree = (options.source == "tree" or \ + (options.source == "any" and tree)) + use_arch = (options.source == "archive" or options.source == "any") + + log = None + if use_tree: + for log in tree.iter_logs(revision.get_version()): + if log.revision == revision: + break + else: + log = None + if log is None and use_arch: + cmdutil.ensure_revision_exists(revision) + log = arch.Patchlog(revision) + if log is not None: + for item in log.items(): + print "%s: %s" % item + print log.description + + def get_parser(self): + """ + Returns the options parser to use for the "cat-log" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai cat-log [revision]") + parser.add_option("--archive", action="store_const", dest="source", + const="archive", default="any", + help="Always get the log from the archive") + parser.add_option("--tree", action="store_const", dest="source", + const="tree", help="Always get the log from the tree") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Prints the log for the specified revision + """ + help_tree_spec() + return + +class Revert(BaseCommand): + """ Reverts a tree (or aspects of it) to a revision + """ + def __init__(self): + self.description="Reverts a tree (or aspects of it) to a revision " + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return iter_modified_file_completions(tree, arg) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revert" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: + tree = arch.tree_root() + except arch.errors.TreeRootError, e: + raise CommandFailed(e) + spec=None + if options.revision is not None: + spec=options.revision + try: + if spec is not None: + revision = cmdutil.determine_revision_tree(tree, spec) + else: + revision = cmdutil.comp_revision(tree) + except cmdutil.CantDetermineRevision, e: + raise CommandFailedWrapper(e) + munger = None + + if options.file_contents or options.file_perms or options.deletions\ + or options.additions or options.renames or options.hunk_prompt: + munger = cmdutil.MungeOpts() + munger.hunk_prompt = options.hunk_prompt + + if len(args) > 0 or options.logs or options.pattern_files or \ + options.control: + if munger is None: + munger = cmdutil.MungeOpts(True) + munger.all_types(True) + if len(args) > 0: + t_cwd = cmdutil.tree_cwd(tree) + for name in args: + if len(t_cwd) > 0: + t_cwd += "/" + name = "./" + t_cwd + name + munger.add_keep_file(name); + + if options.file_perms: + munger.file_perms = True + if options.file_contents: + munger.file_contents = True + if options.deletions: + munger.deletions = True + if options.additions: + munger.additions = True + if options.renames: + munger.renames = True + if options.logs: + munger.add_keep_pattern('^\./\{arch\}/[^=].*') + if options.control: + munger.add_keep_pattern("/\.arch-ids|^\./\{arch\}|"\ + "/\.arch-inventory$") + if options.pattern_files: + munger.add_keep_pattern(options.pattern_files) + + for line in cmdutil.revert(tree, revision, munger, + not options.no_output): + cmdutil.colorize(line) + + + def get_parser(self): + """ + Returns the options parser to use for the "cat-log" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai revert [options] [FILE...]") + parser.add_option("", "--contents", action="store_true", + dest="file_contents", + help="Revert file content changes") + parser.add_option("", "--permissions", action="store_true", + dest="file_perms", + help="Revert file permissions changes") + parser.add_option("", "--deletions", action="store_true", + dest="deletions", + help="Restore deleted files") + parser.add_option("", "--additions", action="store_true", + dest="additions", + help="Remove added files") + parser.add_option("", "--renames", action="store_true", + dest="renames", + help="Revert file names") + parser.add_option("--hunks", action="store_true", + dest="hunk_prompt", default=False, + help="Prompt which hunks to revert") + parser.add_option("--pattern-files", dest="pattern_files", + help="Revert files that match this pattern", + metavar="REGEX") + parser.add_option("--logs", action="store_true", + dest="logs", default=False, + help="Revert only logs") + parser.add_option("--control-files", action="store_true", + dest="control", default=False, + help="Revert logs and other control files") + parser.add_option("-n", "--no-output", action="store_true", + dest="no_output", + help="Don't keep an undo changeset") + parser.add_option("--revision", dest="revision", + help="Revert to the specified revision", + metavar="REVISION") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Reverts changes in the current working tree. If no flags are specified, all +types of changes are reverted. Otherwise, only selected types of changes are +reverted. + +If a revision is specified on the commandline, differences between the current +tree and that revision are reverted. If a version is specified, the current +tree is used to determine the revision. + +If files are specified, only those files listed will have any changes applied. +To specify a renamed file, you can use either the old or new name. (or both!) + +Unless "-n" is specified, reversions can be undone with "redo". + """ + return + +class Revision(BaseCommand): + """ + Print a revision name based on a revision specifier + """ + def __init__(self): + self.description="Prints the name of a revision" + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + tree = None + + spec=None + if len(args) > 0: + spec=args[0] + if len(args) > 1: + raise cmdutil.GetHelp + try: + if tree: + revision = cmdutil.determine_revision_tree(tree, spec) + else: + revision = cmdutil.determine_revision_arch(tree, spec) + except cmdutil.CantDetermineRevision, e: + print str(e) + return + print options.display(revision) + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai revision [revision]") + parser.add_option("", "--location", action="store_const", + const=paths.determine_path, dest="display", + help="Show location instead of name", default=str) + parser.add_option("--import", action="store_const", + const=paths.determine_import_path, dest="display", + help="Show location of import file") + parser.add_option("--log", action="store_const", + const=paths.determine_log_path, dest="display", + help="Show location of log file") + parser.add_option("--patch", action="store_const", + dest="display", const=paths.determine_patch_path, + help="Show location of patchfile") + parser.add_option("--continuation", action="store_const", + const=paths.determine_continuation_path, + dest="display", + help="Show location of continuation file") + parser.add_option("--cacherev", action="store_const", + const=paths.determine_cacherev_path, dest="display", + help="Show location of cacherev file") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Expands aliases and prints the name of the specified revision. Instead of +the name, several options can be used to print locations. If more than one is +specified, the last one is used. + """ + help_tree_spec() + return + +def require_version_exists(version, spec): + if not version.exists(): + raise cmdutil.CantDetermineVersion(spec, + "The version %s does not exist." \ + % version) + +class Revisions(BaseCommand): + """ + Print a revision name based on a revision specifier + """ + def __init__(self): + self.description="Lists revisions" + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + (options, args) = self.get_parser().parse_args(cmdargs) + if len(args) > 1: + raise cmdutil.GetHelp + try: + self.tree = arch.tree_root() + except arch.errors.TreeRootError: + self.tree = None + try: + iter = self.get_iterator(options.type, args, options.reverse, + options.modified) + except cmdutil.CantDetermineRevision, e: + raise CommandFailedWrapper(e) + + if options.skip is not None: + iter = cmdutil.iter_skip(iter, int(options.skip)) + + for revision in iter: + log = None + if isinstance(revision, arch.Patchlog): + log = revision + revision=revision.revision + print options.display(revision) + if log is None and (options.summary or options.creator or + options.date or options.merges): + log = revision.patchlog + if options.creator: + print " %s" % log.creator + if options.date: + print " %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date) + if options.summary: + print " %s" % log.summary + if options.merges: + showed_title = False + for revision in log.merged_patches: + if not showed_title: + print " Merged:" + showed_title = True + print " %s" % revision + + def get_iterator(self, type, args, reverse, modified): + if len(args) > 0: + spec = args[0] + else: + spec = None + if modified is not None: + iter = cmdutil.modified_iter(modified, self.tree) + if reverse: + return iter + else: + return cmdutil.iter_reverse(iter) + elif type == "archive": + if spec is None: + if self.tree is None: + raise cmdutil.CantDetermineRevision("", + "Not in a project tree") + version = cmdutil.determine_version_tree(spec, self.tree) + else: + version = cmdutil.determine_version_arch(spec, self.tree) + cmdutil.ensure_archive_registered(version.archive) + require_version_exists(version, spec) + return version.iter_revisions(reverse) + elif type == "cacherevs": + if spec is None: + if self.tree is None: + raise cmdutil.CantDetermineRevision("", + "Not in a project tree") + version = cmdutil.determine_version_tree(spec, self.tree) + else: + version = cmdutil.determine_version_arch(spec, self.tree) + cmdutil.ensure_archive_registered(version.archive) + require_version_exists(version, spec) + return cmdutil.iter_cacherevs(version, reverse) + elif type == "library": + if spec is None: + if self.tree is None: + raise cmdutil.CantDetermineRevision("", + "Not in a project tree") + version = cmdutil.determine_version_tree(spec, self.tree) + else: + version = cmdutil.determine_version_arch(spec, self.tree) + return version.iter_library_revisions(reverse) + elif type == "logs": + if self.tree is None: + raise cmdutil.CantDetermineRevision("", "Not in a project tree") + return self.tree.iter_logs(cmdutil.determine_version_tree(spec, \ + self.tree), reverse) + elif type == "missing" or type == "skip-present": + if self.tree is None: + raise cmdutil.CantDetermineRevision("", "Not in a project tree") + skip = (type == "skip-present") + version = cmdutil.determine_version_tree(spec, self.tree) + cmdutil.ensure_archive_registered(version.archive) + require_version_exists(version, spec) + return cmdutil.iter_missing(self.tree, version, reverse, + skip_present=skip) + + elif type == "present": + if self.tree is None: + raise cmdutil.CantDetermineRevision("", "Not in a project tree") + version = cmdutil.determine_version_tree(spec, self.tree) + cmdutil.ensure_archive_registered(version.archive) + require_version_exists(version, spec) + return cmdutil.iter_present(self.tree, version, reverse) + + elif type == "new-merges" or type == "direct-merges": + if self.tree is None: + raise cmdutil.CantDetermineRevision("", "Not in a project tree") + version = cmdutil.determine_version_tree(spec, self.tree) + cmdutil.ensure_archive_registered(version.archive) + require_version_exists(version, spec) + iter = cmdutil.iter_new_merges(self.tree, version, reverse) + if type == "new-merges": + return iter + elif type == "direct-merges": + return cmdutil.direct_merges(iter) + + elif type == "missing-from": + if self.tree is None: + raise cmdutil.CantDetermineRevision("", "Not in a project tree") + revision = cmdutil.determine_revision_tree(self.tree, spec) + libtree = cmdutil.find_or_make_local_revision(revision) + return cmdutil.iter_missing(libtree, self.tree.tree_version, + reverse) + + elif type == "partner-missing": + return cmdutil.iter_partner_missing(self.tree, reverse) + + elif type == "ancestry": + revision = cmdutil.determine_revision_tree(self.tree, spec) + iter = cmdutil._iter_ancestry(self.tree, revision) + if reverse: + return iter + else: + return cmdutil.iter_reverse(iter) + + elif type == "dependencies" or type == "non-dependencies": + nondeps = (type == "non-dependencies") + revision = cmdutil.determine_revision_tree(self.tree, spec) + anc_iter = cmdutil._iter_ancestry(self.tree, revision) + iter_depends = cmdutil.iter_depends(anc_iter, nondeps) + if reverse: + return iter_depends + else: + return cmdutil.iter_reverse(iter_depends) + elif type == "micro": + return cmdutil.iter_micro(self.tree) + + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai revisions [revision]") + select = cmdutil.OptionGroup(parser, "Selection options", + "Control which revisions are listed. These options" + " are mutually exclusive. If more than one is" + " specified, the last is used.") + select.add_option("", "--archive", action="store_const", + const="archive", dest="type", default="archive", + help="List all revisions in the archive") + select.add_option("", "--cacherevs", action="store_const", + const="cacherevs", dest="type", + help="List all revisions stored in the archive as " + "complete copies") + select.add_option("", "--logs", action="store_const", + const="logs", dest="type", + help="List revisions that have a patchlog in the " + "tree") + select.add_option("", "--missing", action="store_const", + const="missing", dest="type", + help="List revisions from the specified version that" + " have no patchlog in the tree") + select.add_option("", "--skip-present", action="store_const", + const="skip-present", dest="type", + help="List revisions from the specified version that" + " have no patchlogs at all in the tree") + select.add_option("", "--present", action="store_const", + const="present", dest="type", + help="List revisions from the specified version that" + " have no patchlog in the tree, but can't be merged") + select.add_option("", "--missing-from", action="store_const", + const="missing-from", dest="type", + help="List revisions from the specified revision " + "that have no patchlog for the tree version") + select.add_option("", "--partner-missing", action="store_const", + const="partner-missing", dest="type", + help="List revisions in partner versions that are" + " missing") + select.add_option("", "--new-merges", action="store_const", + const="new-merges", dest="type", + help="List revisions that have had patchlogs added" + " to the tree since the last commit") + select.add_option("", "--direct-merges", action="store_const", + const="direct-merges", dest="type", + help="List revisions that have been directly added" + " to tree since the last commit ") + select.add_option("", "--library", action="store_const", + const="library", dest="type", + help="List revisions in the revision library") + select.add_option("", "--ancestry", action="store_const", + const="ancestry", dest="type", + help="List revisions that are ancestors of the " + "current tree version") + + select.add_option("", "--dependencies", action="store_const", + const="dependencies", dest="type", + help="List revisions that the given revision " + "depends on") + + select.add_option("", "--non-dependencies", action="store_const", + const="non-dependencies", dest="type", + help="List revisions that the given revision " + "does not depend on") + + select.add_option("--micro", action="store_const", + const="micro", dest="type", + help="List partner revisions aimed for this " + "micro-branch") + + select.add_option("", "--modified", dest="modified", + help="List tree ancestor revisions that modified a " + "given file", metavar="FILE[:LINE]") + + parser.add_option("", "--skip", dest="skip", + help="Skip revisions. Positive numbers skip from " + "beginning, negative skip from end.", + metavar="NUMBER") + + parser.add_option_group(select) + + format = cmdutil.OptionGroup(parser, "Revision format options", + "These control the appearance of listed revisions") + format.add_option("", "--location", action="store_const", + const=paths.determine_path, dest="display", + help="Show location instead of name", default=str) + format.add_option("--import", action="store_const", + const=paths.determine_import_path, dest="display", + help="Show location of import file") + format.add_option("--log", action="store_const", + const=paths.determine_log_path, dest="display", + help="Show location of log file") + format.add_option("--patch", action="store_const", + dest="display", const=paths.determine_patch_path, + help="Show location of patchfile") + format.add_option("--continuation", action="store_const", + const=paths.determine_continuation_path, + dest="display", + help="Show location of continuation file") + format.add_option("--cacherev", action="store_const", + const=paths.determine_cacherev_path, dest="display", + help="Show location of cacherev file") + parser.add_option_group(format) + display = cmdutil.OptionGroup(parser, "Display format options", + "These control the display of data") + display.add_option("-r", "--reverse", action="store_true", + dest="reverse", help="Sort from newest to oldest") + display.add_option("-s", "--summary", action="store_true", + dest="summary", help="Show patchlog summary") + display.add_option("-D", "--date", action="store_true", + dest="date", help="Show patchlog date") + display.add_option("-c", "--creator", action="store_true", + dest="creator", help="Show the id that committed the" + " revision") + display.add_option("-m", "--merges", action="store_true", + dest="merges", help="Show the revisions that were" + " merged") + parser.add_option_group(display) + return parser + def help(self, parser=None): + """Attempt to explain the revisions command + + :param parser: If supplied, used to determine options + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """List revisions. + """ + help_tree_spec() + + +class Get(BaseCommand): + """ + Retrieve a revision from the archive + """ + def __init__(self): + self.description="Retrieve a revision from the archive" + self.parser=self.get_parser() + + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + + def do_command(self, cmdargs): + """ + Master function that perfoms the "get" command. + """ + (options, args) = self.parser.parse_args(cmdargs) + if len(args) < 1: + return self.help() + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + tree = None + + arch_loc = None + try: + revision, arch_loc = paths.full_path_decode(args[0]) + except Exception, e: + revision = cmdutil.determine_revision_arch(tree, args[0], + check_existence=False, allow_package=True) + if len(args) > 1: + directory = args[1] + else: + directory = str(revision.nonarch) + if os.path.exists(directory): + raise DirectoryExists(directory) + cmdutil.ensure_archive_registered(revision.archive, arch_loc) + try: + cmdutil.ensure_revision_exists(revision) + except cmdutil.NoSuchRevision, e: + raise CommandFailedWrapper(e) + + link = cmdutil.prompt ("get link") + for line in cmdutil.iter_get(revision, directory, link, + options.no_pristine, + options.no_greedy_add): + cmdutil.colorize(line) + + def get_parser(self): + """ + Returns the options parser to use for the "get" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai get revision [dir]") + parser.add_option("--no-pristine", action="store_true", + dest="no_pristine", + help="Do not make pristine copy for reference") + parser.add_option("--no-greedy-add", action="store_true", + dest="no_greedy_add", + help="Never add to greedy libraries") + + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Expands aliases and constructs a project tree for a revision. If the optional +"dir" argument is provided, the project tree will be stored in this directory. + """ + help_tree_spec() + return + +class PromptCmd(cmd.Cmd): + def __init__(self): + cmd.Cmd.__init__(self) + self.prompt = "Fai> " + try: + self.tree = arch.tree_root() + except: + self.tree = None + self.set_title() + self.set_prompt() + self.fake_aba = abacmds.AbaCmds() + self.identchars += '-' + self.history_file = os.path.expanduser("~/.fai-history") + readline.set_completer_delims(string.whitespace) + if os.access(self.history_file, os.R_OK) and \ + os.path.isfile(self.history_file): + readline.read_history_file(self.history_file) + + def write_history(self): + readline.write_history_file(self.history_file) + + def do_quit(self, args): + self.write_history() + sys.exit(0) + + def do_exit(self, args): + self.do_quit(args) + + def do_EOF(self, args): + print + self.do_quit(args) + + def postcmd(self, line, bar): + self.set_title() + self.set_prompt() + + def set_prompt(self): + if self.tree is not None: + try: + version = " "+self.tree.tree_version.nonarch + except: + version = "" + else: + version = "" + self.prompt = "Fai%s> " % version + + def set_title(self, command=None): + try: + version = self.tree.tree_version.nonarch + except: + version = "[no version]" + if command is None: + command = "" + sys.stdout.write(terminal.term_title("Fai %s %s" % (command, version))) + + def do_cd(self, line): + if line == "": + line = "~" + try: + os.chdir(os.path.expanduser(line)) + except Exception, e: + print e + try: + self.tree = arch.tree_root() + except: + self.tree = None + + def do_help(self, line): + Help()(line) + + def default(self, line): + args = line.split() + if find_command(args[0]): + try: + find_command(args[0]).do_command(args[1:]) + except cmdutil.BadCommandOption, e: + print e + except cmdutil.GetHelp, e: + find_command(args[0]).help() + except CommandFailed, e: + print e + except arch.errors.ArchiveNotRegistered, e: + print e + except KeyboardInterrupt, e: + print "Interrupted" + except arch.util.ExecProblem, e: + print e.proc.error.rstrip('\n') + except cmdutil.CantDetermineVersion, e: + print e + except cmdutil.CantDetermineRevision, e: + print e + except Exception, e: + print "Unhandled error:\n%s" % cmdutil.exception_str(e) + + elif suggestions.has_key(args[0]): + print suggestions[args[0]] + + elif self.fake_aba.is_command(args[0]): + tree = None + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + pass + cmd = self.fake_aba.is_command(args[0]) + try: + cmd.run(cmdutil.expand_prefix_alias(args[1:], tree)) + except KeyboardInterrupt, e: + print "Interrupted" + + elif options.tla_fallthrough and args[0] != "rm" and \ + cmdutil.is_tla_command(args[0]): + try: + tree = None + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + pass + args = cmdutil.expand_prefix_alias(args, tree) + arch.util.exec_safe('tla', args, stderr=sys.stderr, + expected=(0, 1)) + except arch.util.ExecProblem, e: + pass + except KeyboardInterrupt, e: + print "Interrupted" + else: + try: + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + tree = None + args=line.split() + os.system(" ".join(cmdutil.expand_prefix_alias(args, tree))) + except KeyboardInterrupt, e: + print "Interrupted" + + def completenames(self, text, line, begidx, endidx): + completions = [] + iter = iter_command_names(self.fake_aba) + try: + if len(line) > 0: + arg = line.split()[-1] + else: + arg = "" + iter = iter_munged_completions(iter, arg, text) + except Exception, e: + print e + return list(iter) + + def completedefault(self, text, line, begidx, endidx): + """Perform completion for native commands. + + :param text: The text to complete + :type text: str + :param line: The entire line to complete + :type line: str + :param begidx: The start of the text in the line + :type begidx: int + :param endidx: The end of the text in the line + :type endidx: int + """ + try: + (cmd, args, foo) = self.parseline(line) + command_obj=find_command(cmd) + if command_obj is not None: + return command_obj.complete(args.split(), text) + elif not self.fake_aba.is_command(cmd) and \ + cmdutil.is_tla_command(cmd): + iter = cmdutil.iter_supported_switches(cmd) + if len(args) > 0: + arg = args.split()[-1] + else: + arg = "" + if arg.startswith("-"): + return list(iter_munged_completions(iter, arg, text)) + else: + return list(iter_munged_completions( + iter_file_completions(arg), arg, text)) + + + elif cmd == "cd": + if len(args) > 0: + arg = args.split()[-1] + else: + arg = "" + iter = iter_dir_completions(arg) + iter = iter_munged_completions(iter, arg, text) + return list(iter) + elif len(args)>0: + arg = args.split()[-1] + return list(iter_munged_completions(iter_file_completions(arg), + arg, text)) + else: + return self.completenames(text, line, begidx, endidx) + except Exception, e: + print e + + +def iter_command_names(fake_aba): + for entry in cmdutil.iter_combine([commands.iterkeys(), + fake_aba.get_commands(), + cmdutil.iter_tla_commands(False)]): + if not suggestions.has_key(str(entry)): + yield entry + + +def iter_file_completions(arg, only_dirs = False): + """Generate an iterator that iterates through filename completions. + + :param arg: The filename fragment to match + :type arg: str + :param only_dirs: If true, match only directories + :type only_dirs: bool + """ + cwd = os.getcwd() + if cwd != "/": + extras = [".", ".."] + else: + extras = [] + (dir, file) = os.path.split(arg) + if dir != "": + listingdir = os.path.expanduser(dir) + else: + listingdir = cwd + for file in cmdutil.iter_combine([os.listdir(listingdir), extras]): + if dir != "": + userfile = dir+'/'+file + else: + userfile = file + if userfile.startswith(arg): + if os.path.isdir(listingdir+'/'+file): + userfile+='/' + yield userfile + elif not only_dirs: + yield userfile + +def iter_munged_completions(iter, arg, text): + for completion in iter: + completion = str(completion) + if completion.startswith(arg): + yield completion[len(arg)-len(text):] + +def iter_source_file_completions(tree, arg): + treepath = cmdutil.tree_cwd(tree) + if len(treepath) > 0: + dirs = [treepath] + else: + dirs = None + for file in tree.iter_inventory(dirs, source=True, both=True): + file = file_completion_match(file, treepath, arg) + if file is not None: + yield file + + +def iter_untagged(tree, dirs): + for file in arch_core.iter_inventory_filter(tree, dirs, tagged=False, + categories=arch_core.non_root, + control_files=True): + yield file.name + + +def iter_untagged_completions(tree, arg): + """Generate an iterator for all visible untagged files that match arg. + + :param tree: The tree to look for untagged files in + :type tree: `arch.WorkingTree` + :param arg: The argument to match + :type arg: str + :return: An iterator of all matching untagged files + :rtype: iterator of str + """ + treepath = cmdutil.tree_cwd(tree) + if len(treepath) > 0: + dirs = [treepath] + else: + dirs = None + + for file in iter_untagged(tree, dirs): + file = file_completion_match(file, treepath, arg) + if file is not None: + yield file + + +def file_completion_match(file, treepath, arg): + """Determines whether a file within an arch tree matches the argument. + + :param file: The rooted filename + :type file: str + :param treepath: The path to the cwd within the tree + :type treepath: str + :param arg: The prefix to match + :return: The completion name, or None if not a match + :rtype: str + """ + if not file.startswith(treepath): + return None + if treepath != "": + file = file[len(treepath)+1:] + + if not file.startswith(arg): + return None + if os.path.isdir(file): + file += '/' + return file + +def iter_modified_file_completions(tree, arg): + """Returns a list of modified files that match the specified prefix. + + :param tree: The current tree + :type tree: `arch.WorkingTree` + :param arg: The prefix to match + :type arg: str + """ + treepath = cmdutil.tree_cwd(tree) + tmpdir = cmdutil.tmpdir() + changeset = tmpdir+"/changeset" + completions = [] + revision = cmdutil.determine_revision_tree(tree) + for line in arch.iter_delta(revision, tree, changeset): + if isinstance(line, arch.FileModification): + file = file_completion_match(line.name[1:], treepath, arg) + if file is not None: + completions.append(file) + shutil.rmtree(tmpdir) + return completions + +def iter_dir_completions(arg): + """Generate an iterator that iterates through directory name completions. + + :param arg: The directory name fragment to match + :type arg: str + """ + return iter_file_completions(arg, True) + +class Shell(BaseCommand): + def __init__(self): + self.description = "Runs Fai as a shell" + + def do_command(self, cmdargs): + if len(cmdargs)!=0: + raise cmdutil.GetHelp + prompt = PromptCmd() + try: + prompt.cmdloop() + finally: + prompt.write_history() + +class AddID(BaseCommand): + """ + Adds an inventory id for the given file + """ + def __init__(self): + self.description="Add an inventory id for a given file" + + def get_completer(self, arg, index): + tree = arch.tree_root() + return iter_untagged_completions(tree, arg) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + + tree = arch.tree_root() + + if (len(args) == 0) == (options.untagged == False): + raise cmdutil.GetHelp + + #if options.id and len(args) != 1: + # print "If --id is specified, only one file can be named." + # return + + method = tree.tagging_method + + if options.id_type == "tagline": + if method != "tagline": + if not cmdutil.prompt("Tagline in other tree"): + if method == "explicit": + options.id_type == explicit + else: + print "add-id not supported for \"%s\" tagging method"\ + % method + return + + elif options.id_type == "explicit": + if method != "tagline" and method != explicit: + if not prompt("Explicit in other tree"): + print "add-id not supported for \"%s\" tagging method" % \ + method + return + + if options.id_type == "auto": + if method != "tagline" and method != "explicit": + print "add-id not supported for \"%s\" tagging method" % method + return + else: + options.id_type = method + if options.untagged: + args = None + self.add_ids(tree, options.id_type, args) + + def add_ids(self, tree, id_type, files=()): + """Add inventory ids to files. + + :param tree: the tree the files are in + :type tree: `arch.WorkingTree` + :param id_type: the type of id to add: "explicit" or "tagline" + :type id_type: str + :param files: The list of files to add. If None do all untagged. + :type files: tuple of str + """ + + untagged = (files is None) + if untagged: + files = list(iter_untagged(tree, None)) + previous_files = [] + while len(files) > 0: + previous_files.extend(files) + if id_type == "explicit": + cmdutil.add_id(files) + elif id_type == "tagline": + for file in files: + try: + cmdutil.add_tagline_or_explicit_id(file) + except cmdutil.AlreadyTagged: + print "\"%s\" already has a tagline." % file + except cmdutil.NoCommentSyntax: + pass + #do inventory after tagging until no untagged files are encountered + if untagged: + files = [] + for file in iter_untagged(tree, None): + if not file in previous_files: + files.append(file) + + else: + break + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai add-id file1 [file2] [file3]...") +# ddaa suggests removing this to promote GUIDs. Let's see who squalks. +# parser.add_option("-i", "--id", dest="id", +# help="Specify id for a single file", default=None) + parser.add_option("--tltl", action="store_true", + dest="lord_style", help="Use Tom Lord's style of id.") + parser.add_option("--explicit", action="store_const", + const="explicit", dest="id_type", + help="Use an explicit id", default="auto") + parser.add_option("--tagline", action="store_const", + const="tagline", dest="id_type", + help="Use a tagline id") + parser.add_option("--untagged", action="store_true", + dest="untagged", default=False, + help="tag all untagged files") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Adds an inventory to the specified file(s) and directories. If --untagged is +specified, adds inventory to all untagged files and directories. + """ + return + + +class Merge(BaseCommand): + """ + Merges changes from other versions into the current tree + """ + def __init__(self): + self.description="Merges changes from other versions" + try: + self.tree = arch.tree_root() + except: + self.tree = None + + + def get_completer(self, arg, index): + if self.tree is None: + raise arch.errors.TreeRootError + completions = list(ancillary.iter_partners(self.tree, + self.tree.tree_version)) + if len(completions) == 0: + completions = list(self.tree.iter_log_versions()) + + aliases = [] + try: + for completion in completions: + alias = ancillary.compact_alias(str(completion), self.tree) + if alias: + aliases.extend(alias) + + for completion in completions: + if completion.archive == self.tree.tree_version.archive: + aliases.append(completion.nonarch) + + except Exception, e: + print e + + completions.extend(aliases) + return completions + + def do_command(self, cmdargs): + """ + Master function that perfoms the "merge" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + if options.diff3: + action="star-merge" + else: + action = options.action + + if self.tree is None: + raise arch.errors.TreeRootError(os.getcwd()) + if cmdutil.has_changed(self.tree.tree_version): + raise UncommittedChanges(self.tree) + + if len(args) > 0: + revisions = [] + for arg in args: + revisions.append(cmdutil.determine_revision_arch(self.tree, + arg)) + source = "from commandline" + else: + revisions = ancillary.iter_partner_revisions(self.tree, + self.tree.tree_version) + source = "from partner version" + revisions = misc.rewind_iterator(revisions) + try: + revisions.next() + revisions.rewind() + except StopIteration, e: + revision = cmdutil.tag_cur(self.tree) + if revision is None: + raise CantDetermineRevision("", "No version specified, no " + "partner-versions, and no tag" + " source") + revisions = [revision] + source = "from tag source" + for revision in revisions: + cmdutil.ensure_archive_registered(revision.archive) + cmdutil.colorize(arch.Chatter("* Merging %s [%s]" % + (revision, source))) + if action=="native-merge" or action=="update": + if self.native_merge(revision, action) == 0: + continue + elif action=="star-merge": + try: + self.star_merge(revision, options.diff3) + except errors.MergeProblem, e: + break + if cmdutil.has_changed(self.tree.tree_version): + break + + def star_merge(self, revision, diff3): + """Perform a star-merge on the current tree. + + :param revision: The revision to use for the merge + :type revision: `arch.Revision` + :param diff3: If true, do a diff3 merge + :type diff3: bool + """ + try: + for line in self.tree.iter_star_merge(revision, diff3=diff3): + cmdutil.colorize(line) + except arch.util.ExecProblem, e: + if e.proc.status is not None and e.proc.status == 1: + if e.proc.error: + print e.proc.error + raise MergeProblem + else: + raise + + def native_merge(self, other_revision, action): + """Perform a native-merge on the current tree. + + :param other_revision: The revision to use for the merge + :type other_revision: `arch.Revision` + :return: 0 if the merge was skipped, 1 if it was applied + """ + other_tree = cmdutil.find_or_make_local_revision(other_revision) + try: + if action == "native-merge": + ancestor = cmdutil.merge_ancestor2(self.tree, other_tree, + other_revision) + elif action == "update": + ancestor = cmdutil.tree_latest(self.tree, + other_revision.version) + except CantDetermineRevision, e: + raise CommandFailedWrapper(e) + cmdutil.colorize(arch.Chatter("* Found common ancestor %s" % ancestor)) + if (ancestor == other_revision): + cmdutil.colorize(arch.Chatter("* Skipping redundant merge" + % ancestor)) + return 0 + delta = cmdutil.apply_delta(ancestor, other_tree, self.tree) + for line in cmdutil.iter_apply_delta_filter(delta): + cmdutil.colorize(line) + return 1 + + + + def get_parser(self): + """ + Returns the options parser to use for the "merge" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai merge [VERSION]") + parser.add_option("-s", "--star-merge", action="store_const", + dest="action", help="Use star-merge", + const="star-merge", default="native-merge") + parser.add_option("--update", action="store_const", + dest="action", help="Use update picker", + const="update") + parser.add_option("--diff3", action="store_true", + dest="diff3", + help="Use diff3 for merge (implies star-merge)") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Performs a merge operation using the specified version. + """ + return + +class ELog(BaseCommand): + """ + Produces a raw patchlog and invokes the user's editor + """ + def __init__(self): + self.description="Edit a patchlog to commit" + try: + self.tree = arch.tree_root() + except: + self.tree = None + + + def do_command(self, cmdargs): + """ + Master function that perfoms the "elog" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + if self.tree is None: + raise arch.errors.TreeRootError + + edit_log(self.tree) + + def get_parser(self): + """ + Returns the options parser to use for the "merge" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai elog") + return parser + + + def help(self, parser=None): + """ + Invokes $EDITOR to produce a log for committing. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Invokes $EDITOR to produce a log for committing. + """ + return + +def edit_log(tree): + """Makes and edits the log for a tree. Does all kinds of fancy things + like log templates and merge summaries and log-for-merge + + :param tree: The tree to edit the log for + :type tree: `arch.WorkingTree` + """ + #ensure we have an editor before preparing the log + cmdutil.find_editor() + log = tree.log_message(create=False) + log_is_new = False + if log is None or cmdutil.prompt("Overwrite log"): + if log is not None: + os.remove(log.name) + log = tree.log_message(create=True) + log_is_new = True + tmplog = log.name + template = tree+"/{arch}/=log-template" + if not os.path.exists(template): + template = os.path.expanduser("~/.arch-params/=log-template") + if not os.path.exists(template): + template = None + if template: + shutil.copyfile(template, tmplog) + + new_merges = list(cmdutil.iter_new_merges(tree, + tree.tree_version)) + log["Summary"] = merge_summary(new_merges, tree.tree_version) + if len(new_merges) > 0: + if cmdutil.prompt("Log for merge"): + mergestuff = cmdutil.log_for_merge(tree) + log.description += mergestuff + log.save() + try: + cmdutil.invoke_editor(log.name) + except: + if log_is_new: + os.remove(log.name) + raise + +def merge_summary(new_merges, tree_version): + if len(new_merges) == 0: + return "" + if len(new_merges) == 1: + summary = new_merges[0].summary + else: + summary = "Merge" + + credits = [] + for merge in new_merges: + if arch.my_id() != merge.creator: + name = re.sub("<.*>", "", merge.creator).rstrip(" "); + if not name in credits: + credits.append(name) + else: + version = merge.revision.version + if version.archive == tree_version.archive: + if not version.nonarch in credits: + credits.append(version.nonarch) + elif not str(version) in credits: + credits.append(str(version)) + + return ("%s (%s)") % (summary, ", ".join(credits)) + +class MirrorArchive(BaseCommand): + """ + Updates a mirror from an archive + """ + def __init__(self): + self.description="Update a mirror from an archive" + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + if len(args) > 1: + raise GetHelp + try: + tree = arch.tree_root() + except: + tree = None + + if len(args) == 0: + if tree is not None: + name = tree.tree_version() + else: + name = cmdutil.expand_alias(args[0], tree) + name = arch.NameParser(name) + + to_arch = name.get_archive() + from_arch = cmdutil.get_mirror_source(arch.Archive(to_arch)) + limit = name.get_nonarch() + + iter = arch_core.mirror_archive(from_arch,to_arch, limit) + for line in arch.chatter_classifier(iter): + cmdutil.colorize(line) + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai mirror-archive ARCHIVE") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Updates a mirror from an archive. If a branch, package, or version is +supplied, only changes under it are mirrored. + """ + return + +def help_tree_spec(): + print """Specifying revisions (default: tree) +Revisions may be specified by alias, revision, version or patchlevel. +Revisions or versions may be fully qualified. Unqualified revisions, versions, +or patchlevels use the archive of the current project tree. Versions will +use the latest patchlevel in the tree. Patchlevels will use the current tree- +version. + +Use "alias" to list available (user and automatic) aliases.""" + +def help_aliases(tree): + print """Auto-generated aliases + acur : The latest revision in the archive of the tree-version. You can specfy + a different version like so: acur:foo--bar--0 (aliases can be used) + tcur : (tree current) The latest revision in the tree of the tree-version. + You can specify a different version like so: tcur:foo--bar--0 (aliases + can be used). +tprev : (tree previous) The previous revision in the tree of the tree-version. + To specify an older revision, use a number, e.g. "tprev:4" + tanc : (tree ancestor) The ancestor revision of the tree + To specify an older revision, use a number, e.g. "tanc:4" +tdate : (tree date) The latest revision from a given date (e.g. "tdate:July 6") + tmod : (tree modified) The latest revision to modify a given file + (e.g. "tmod:engine.cpp" or "tmod:engine.cpp:16") + ttag : (tree tag) The revision that was tagged into the current tree revision, + according to the tree. +tagcur: (tag current) The latest revision of the version that the current tree + was tagged from. +mergeanc : The common ancestor of the current tree and the specified revision. + Defaults to the first partner-version's latest revision or to tagcur. + """ + print "User aliases" + for parts in ancillary.iter_all_alias(tree): + print parts[0].rjust(10)+" : "+parts[1] + + +class Inventory(BaseCommand): + """List the status of files in the tree""" + def __init__(self): + self.description=self.__doc__ + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + tree = arch.tree_root() + categories = [] + + if (options.source): + categories.append(arch_core.SourceFile) + if (options.precious): + categories.append(arch_core.PreciousFile) + if (options.backup): + categories.append(arch_core.BackupFile) + if (options.junk): + categories.append(arch_core.JunkFile) + + if len(categories) == 1: + show_leading = False + else: + show_leading = True + + if len(categories) == 0: + categories = None + + if options.untagged: + categories = arch_core.non_root + show_leading = False + tagged = False + else: + tagged = None + + for file in arch_core.iter_inventory_filter(tree, None, + control_files=options.control_files, + categories = categories, tagged=tagged): + print arch_core.file_line(file, + category = show_leading, + untagged = show_leading, + id = options.ids) + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai inventory [options]") + parser.add_option("--ids", action="store_true", dest="ids", + help="Show file ids") + parser.add_option("--control", action="store_true", + dest="control_files", help="include control files") + parser.add_option("--source", action="store_true", dest="source", + help="List source files") + parser.add_option("--backup", action="store_true", dest="backup", + help="List backup files") + parser.add_option("--precious", action="store_true", dest="precious", + help="List precious files") + parser.add_option("--junk", action="store_true", dest="junk", + help="List junk files") + parser.add_option("--unrecognized", action="store_true", + dest="unrecognized", help="List unrecognized files") + parser.add_option("--untagged", action="store_true", + dest="untagged", help="List only untagged files") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Lists the status of files in the archive: +S source +P precious +B backup +J junk +U unrecognized +T tree root +? untagged-source +Leading letter are not displayed if only one kind of file is shown + """ + return + + +class Alias(BaseCommand): + """List or adjust aliases""" + def __init__(self): + self.description=self.__doc__ + + def get_completer(self, arg, index): + if index > 2: + return () + try: + self.tree = arch.tree_root() + except: + self.tree = None + + if index == 0: + return [part[0]+" " for part in ancillary.iter_all_alias(self.tree)] + elif index == 1: + return cmdutil.iter_revision_completions(arg, self.tree) + + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: + self.tree = arch.tree_root() + except: + self.tree = None + + + try: + options.action(args, options) + except cmdutil.ForbiddenAliasSyntax, e: + raise CommandFailedWrapper(e) + + def arg_dispatch(self, args, options): + """Add, modify, or list aliases, depending on number of arguments + + :param args: The list of commandline arguments + :type args: list of str + :param options: The commandline options + """ + if len(args) == 0: + help_aliases(self.tree) + return + elif len(args) == 1: + self.print_alias(args[0]) + elif (len(args)) == 2: + self.add(args[0], args[1], options) + else: + raise cmdutil.GetHelp + + def print_alias(self, alias): + answer = None + for pair in ancillary.iter_all_alias(self.tree): + if pair[0] == alias: + answer = pair[1] + if answer is not None: + print answer + else: + print "The alias %s is not assigned." % alias + + def add(self, alias, expansion, options): + """Add or modify aliases + + :param alias: The alias name to create/modify + :type alias: str + :param expansion: The expansion to assign to the alias name + :type expansion: str + :param options: The commandline options + """ + newlist = "" + written = False + new_line = "%s=%s\n" % (alias, cmdutil.expand_alias(expansion, + self.tree)) + ancillary.check_alias(new_line.rstrip("\n"), [alias, expansion]) + + for pair in self.get_iterator(options): + if pair[0] != alias: + newlist+="%s=%s\n" % (pair[0], pair[1]) + elif not written: + newlist+=new_line + written = True + if not written: + newlist+=new_line + self.write_aliases(newlist, options) + + def delete(self, args, options): + """Delete the specified alias + + :param args: The list of arguments + :type args: list of str + :param options: The commandline options + """ + deleted = False + if len(args) != 1: + raise cmdutil.GetHelp + newlist = "" + for pair in self.get_iterator(options): + if pair[0] != args[0]: + newlist+="%s=%s\n" % (pair[0], pair[1]) + else: + deleted = True + if not deleted: + raise errors.NoSuchAlias(args[0]) + self.write_aliases(newlist, options) + + def get_alias_file(self, options): + """Return the name of the alias file to use + + :param options: The commandline options + """ + if options.tree: + if self.tree is None: + self.tree == arch.tree_root() + return str(self.tree)+"/{arch}/+aliases" + else: + return "~/.aba/aliases" + + def get_iterator(self, options): + """Return the alias iterator to use + + :param options: The commandline options + """ + return ancillary.iter_alias(self.get_alias_file(options)) + + def write_aliases(self, newlist, options): + """Safely rewrite the alias file + :param newlist: The new list of aliases + :type newlist: str + :param options: The commandline options + """ + filename = os.path.expanduser(self.get_alias_file(options)) + file = cmdutil.NewFileVersion(filename) + file.write(newlist) + file.commit() + + + def get_parser(self): + """ + Returns the options parser to use for the "alias" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai alias [ALIAS] [NAME]") + parser.add_option("-d", "--delete", action="store_const", dest="action", + const=self.delete, default=self.arg_dispatch, + help="Delete an alias") + parser.add_option("--tree", action="store_true", dest="tree", + help="Create a per-tree alias", default=False) + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Lists current aliases or modifies the list of aliases. + +If no arguments are supplied, aliases will be listed. If two arguments are +supplied, the specified alias will be created or modified. If -d or --delete +is supplied, the specified alias will be deleted. + +You can create aliases that refer to any fully-qualified part of the +Arch namespace, e.g. +archive, +archive/category, +archive/category--branch, +archive/category--branch--version (my favourite) +archive/category--branch--version--patchlevel + +Aliases can be used automatically by native commands. To use them +with external or tla commands, prefix them with ^ (you can do this +with native commands, too). +""" + + +class RequestMerge(BaseCommand): + """Submit a merge request to Bug Goo""" + def __init__(self): + self.description=self.__doc__ + + def do_command(self, cmdargs): + """Submit a merge request + + :param cmdargs: The commandline arguments + :type cmdargs: list of str + """ + cmdutil.find_editor() + parser = self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: + self.tree=arch.tree_root() + except: + self.tree=None + base, revisions = self.revision_specs(args) + message = self.make_headers(base, revisions) + message += self.make_summary(revisions) + path = self.edit_message(message) + message = self.tidy_message(path) + if cmdutil.prompt("Send merge"): + self.send_message(message) + print "Merge request sent" + + def make_headers(self, base, revisions): + """Produce email and Bug Goo header strings + + :param base: The base revision to apply merges to + :type base: `arch.Revision` + :param revisions: The revisions to replay into the base + :type revisions: list of `arch.Patchlog` + :return: The headers + :rtype: str + """ + headers = "To: gnu-arch-users@gnu.org\n" + headers += "From: %s\n" % options.fromaddr + if len(revisions) == 1: + headers += "Subject: [MERGE REQUEST] %s\n" % revisions[0].summary + else: + headers += "Subject: [MERGE REQUEST]\n" + headers += "\n" + headers += "Base-Revision: %s\n" % base + for revision in revisions: + headers += "Revision: %s\n" % revision.revision + headers += "Bug: \n\n" + return headers + + def make_summary(self, logs): + """Generate a summary of merges + + :param logs: the patchlogs that were directly added by the merges + :type logs: list of `arch.Patchlog` + :return: the summary + :rtype: str + """ + summary = "" + for log in logs: + summary+=str(log.revision)+"\n" + summary+=log.summary+"\n" + if log.description.strip(): + summary+=log.description.strip('\n')+"\n\n" + return summary + + def revision_specs(self, args): + """Determine the base and merge revisions from tree and arguments. + + :param args: The parsed arguments + :type args: list of str + :return: The base revision and merge revisions + :rtype: `arch.Revision`, list of `arch.Patchlog` + """ + if len(args) > 0: + target_revision = cmdutil.determine_revision_arch(self.tree, + args[0]) + else: + target_revision = cmdutil.tree_latest(self.tree) + if len(args) > 1: + merges = [ arch.Patchlog(cmdutil.determine_revision_arch( + self.tree, f)) for f in args[1:] ] + else: + if self.tree is None: + raise CantDetermineRevision("", "Not in a project tree") + merge_iter = cmdutil.iter_new_merges(self.tree, + target_revision.version, + False) + merges = [f for f in cmdutil.direct_merges(merge_iter)] + return (target_revision, merges) + + def edit_message(self, message): + """Edit an email message in the user's standard editor + + :param message: The message to edit + :type message: str + :return: the path of the edited message + :rtype: str + """ + if self.tree is None: + path = os.get_cwd() + else: + path = self.tree + path += "/,merge-request" + file = open(path, 'w') + file.write(message) + file.flush() + cmdutil.invoke_editor(path) + return path + + def tidy_message(self, path): + """Validate and clean up message. + + :param path: The path to the message to clean up + :type path: str + :return: The parsed message + :rtype: `email.Message` + """ + mail = email.message_from_file(open(path)) + if mail["Subject"].strip() == "[MERGE REQUEST]": + raise BlandSubject + + request = email.message_from_string(mail.get_payload()) + if request.has_key("Bug"): + if request["Bug"].strip()=="": + del request["Bug"] + mail.set_payload(request.as_string()) + return mail + + def send_message(self, message): + """Send a message, using its headers to address it. + + :param message: The message to send + :type message: `email.Message`""" + server = smtplib.SMTP() + server.sendmail(message['From'], message['To'], message.as_string()) + server.quit() + + def help(self, parser=None): + """Print a usage message + + :param parser: The options parser to use + :type parser: `cmdutil.CmdOptionParser` + """ + if parser is None: + parser = self.get_parser() + parser.print_help() + print """ +Sends a merge request formatted for Bug Goo. Intended use: get the tree +you'd like to merge into. Apply the merges you want. Invoke request-merge. +The merge request will open in your $EDITOR. + +When no TARGET is specified, it uses the current tree revision. When +no MERGE is specified, it uses the direct merges (as in "revisions +--direct-merges"). But you can specify just the TARGET, or all the MERGE +revisions. +""" + + def get_parser(self): + """Produce a commandline parser for this command. + + :rtype: `cmdutil.CmdOptionParser` + """ + parser=cmdutil.CmdOptionParser("request-merge [TARGET] [MERGE1...]") + return parser + +commands = { +'changes' : Changes, +'help' : Help, +'update': Update, +'apply-changes':ApplyChanges, +'cat-log': CatLog, +'commit': Commit, +'revision': Revision, +'revisions': Revisions, +'get': Get, +'revert': Revert, +'shell': Shell, +'add-id': AddID, +'merge': Merge, +'elog': ELog, +'mirror-archive': MirrorArchive, +'ninventory': Inventory, +'alias' : Alias, +'request-merge': RequestMerge, +} +suggestions = { +'apply-delta' : "Try \"apply-changes\".", +'delta' : "To compare two revisions, use \"changes\".", +'diff-rev' : "To compare two revisions, use \"changes\".", +'undo' : "To undo local changes, use \"revert\".", +'undelete' : "To undo only deletions, use \"revert --deletions\"", +'missing-from' : "Try \"revisions --missing-from\".", +'missing' : "Try \"revisions --missing\".", +'missing-merge' : "Try \"revisions --partner-missing\".", +'new-merges' : "Try \"revisions --new-merges\".", +'cachedrevs' : "Try \"revisions --cacherevs\". (no 'd')", +'logs' : "Try \"revisions --logs\"", +'tree-source' : "Use the \"^ttag\" alias (\"revision ^ttag\")", +'latest-revision' : "Use the \"^acur\" alias (\"revision ^acur\")", +'change-version' : "Try \"update REVISION\"", +'tree-revision' : "Use the \"^tcur\" alias (\"revision ^tcur\")", +'rev-depends' : "Use revisions --dependencies", +'auto-get' : "Plain get will do archive lookups", +'tagline' : "Use add-id. It uses taglines in tagline trees", +'emlog' : "Use elog. It automatically adds log-for-merge text, if any", +'library-revisions' : "Use revisions --library", +'file-revert' : "Use revert FILE" +} +# arch-tag: 19d5739d-3708-486c-93ba-deecc3027fc7 *** modified file 'bzrlib/branch.py' --- bzrlib/branch.py +++ bzrlib/branch.py @@ -31,6 +31,8 @@ from revision import Revision from errors import bailout, BzrError from textui import show_status +import patches +from bzrlib import progress BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? @@ -864,3 +866,36 @@ s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) + + +def iter_anno_data(branch, file_id): + later_revision = branch.revno() + q = range(branch.revno()) + q.reverse() + later_text_id = branch.basis_tree().inventory[file_id].text_id + i = 0 + for revno in q: + i += 1 + cur_tree = branch.revision_tree(branch.lookup_revision(revno)) + if file_id not in cur_tree.inventory: + text_id = None + else: + text_id = cur_tree.inventory[file_id].text_id + if text_id != later_text_id: + patch = get_patch(branch, revno, later_revision, file_id) + yield revno, patch.iter_inserted(), patch + later_revision = revno + later_text_id = text_id + yield progress.Progress("revisions", i) + +def get_patch(branch, old_revno, new_revno, file_id): + old_tree = branch.revision_tree(branch.lookup_revision(old_revno)) + new_tree = branch.revision_tree(branch.lookup_revision(new_revno)) + if file_id in old_tree.inventory: + old_file = old_tree.get_file(file_id).readlines() + else: + old_file = [] + ud = difflib.unified_diff(old_file, new_tree.get_file(file_id).readlines()) + return patches.parse_patch(ud) + + *** modified file 'bzrlib/commands.py' --- bzrlib/commands.py +++ bzrlib/commands.py @@ -27,6 +27,9 @@ from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date from bzrlib import merge +from bzrlib.branch import iter_anno_data +from bzrlib import patches +from bzrlib import progress def _squish_command_name(cmd): @@ -882,7 +885,15 @@ print '%3d FAILED!' % mf else: print - + result = bzrlib.patches.test() + resultFailed = len(result.errors) + len(result.failures) + print '%-40s %3d tests' % ('bzrlib.patches', result.testsRun), + if resultFailed: + print '%3d FAILED!' % resultFailed + else: + print + tests += result.testsRun + failures += resultFailed print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures @@ -897,6 +908,34 @@ """Show version of bzr""" def run(self): show_version() + +class cmd_annotate(Command): + """Show which revision added each line in a file""" + + takes_args = ['filename'] + def run(self, filename): + if not os.path.exists(filename): + raise BzrCommandError("The file %s does not exist." % filename) + branch = (Branch(filename)) + file_id = branch.working_tree().path2id(filename) + if file_id is None: + raise BzrCommandError("The file %s is not versioned." % filename) + lines = branch.basis_tree().get_file(file_id) + total = branch.revno() + anno_d_iter = iter_anno_data(branch, file_id) + progress_bar = progress.ProgressBar() + try: + for result in patches.iter_annotate_file(lines, anno_d_iter): + if isinstance(result, progress.Progress): + result.total = total + progress_bar(result) + else: + anno_lines = result + finally: + progress.clear_progress_bar() + for line in anno_lines: + sys.stdout.write("%4s:%s" % (str(line.log), line.text)) + def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ commit refs/heads/master mark :647 committer Martin Pool 1118385266 +1000 data 38 - split out updated progress indicator from :646 M 644 inline patches/annotate4.patch data 269289 *** added file 'bzrlib/patches.py' --- /dev/null +++ bzrlib/patches.py @@ -0,0 +1,497 @@ +# Copyright (C) 2004, 2005 Aaron Bentley +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +import sys +import progress +class PatchSyntax(Exception): + def __init__(self, msg): + Exception.__init__(self, msg) + + +class MalformedPatchHeader(PatchSyntax): + def __init__(self, desc, line): + self.desc = desc + self.line = line + msg = "Malformed patch header. %s\n%s" % (self.desc, self.line) + PatchSyntax.__init__(self, msg) + +class MalformedHunkHeader(PatchSyntax): + def __init__(self, desc, line): + self.desc = desc + self.line = line + msg = "Malformed hunk header. %s\n%s" % (self.desc, self.line) + PatchSyntax.__init__(self, msg) + +class MalformedLine(PatchSyntax): + def __init__(self, desc, line): + self.desc = desc + self.line = line + msg = "Malformed line. %s\n%s" % (self.desc, self.line) + PatchSyntax.__init__(self, msg) + +def get_patch_names(iter_lines): + try: + line = iter_lines.next() + if not line.startswith("--- "): + raise MalformedPatchHeader("No orig name", line) + else: + orig_name = line[4:].rstrip("\n") + except StopIteration: + raise MalformedPatchHeader("No orig line", "") + try: + line = iter_lines.next() + if not line.startswith("+++ "): + raise PatchSyntax("No mod name") + else: + mod_name = line[4:].rstrip("\n") + except StopIteration: + raise MalformedPatchHeader("No mod line", "") + return (orig_name, mod_name) + +def parse_range(textrange): + """Parse a patch range, handling the "1" special-case + + :param textrange: The text to parse + :type textrange: str + :return: the position and range, as a tuple + :rtype: (int, int) + """ + tmp = textrange.split(',') + if len(tmp) == 1: + pos = tmp[0] + range = "1" + else: + (pos, range) = tmp + pos = int(pos) + range = int(range) + return (pos, range) + + +def hunk_from_header(line): + if not line.startswith("@@") or not line.endswith("@@\n") \ + or not len(line) > 4: + raise MalformedHunkHeader("Does not start and end with @@.", line) + try: + (orig, mod) = line[3:-4].split(" ") + except Exception, e: + raise MalformedHunkHeader(str(e), line) + if not orig.startswith('-') or not mod.startswith('+'): + raise MalformedHunkHeader("Positions don't start with + or -.", line) + try: + (orig_pos, orig_range) = parse_range(orig[1:]) + (mod_pos, mod_range) = parse_range(mod[1:]) + except Exception, e: + raise MalformedHunkHeader(str(e), line) + if mod_range < 0 or orig_range < 0: + raise MalformedHunkHeader("Hunk range is negative", line) + return Hunk(orig_pos, orig_range, mod_pos, mod_range) + + +class HunkLine: + def __init__(self, contents): + self.contents = contents + + def get_str(self, leadchar): + if self.contents == "\n" and leadchar == " " and False: + return "\n" + return leadchar + self.contents + +class ContextLine(HunkLine): + def __init__(self, contents): + HunkLine.__init__(self, contents) + + def __str__(self): + return self.get_str(" ") + + +class InsertLine(HunkLine): + def __init__(self, contents): + HunkLine.__init__(self, contents) + + def __str__(self): + return self.get_str("+") + + +class RemoveLine(HunkLine): + def __init__(self, contents): + HunkLine.__init__(self, contents) + + def __str__(self): + return self.get_str("-") + +__pychecker__="no-returnvalues" +def parse_line(line): + if line.startswith("\n"): + return ContextLine(line) + elif line.startswith(" "): + return ContextLine(line[1:]) + elif line.startswith("+"): + return InsertLine(line[1:]) + elif line.startswith("-"): + return RemoveLine(line[1:]) + else: + raise MalformedLine("Unknown line type", line) +__pychecker__="" + + +class Hunk: + def __init__(self, orig_pos, orig_range, mod_pos, mod_range): + self.orig_pos = orig_pos + self.orig_range = orig_range + self.mod_pos = mod_pos + self.mod_range = mod_range + self.lines = [] + + def get_header(self): + return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos, + self.orig_range), + self.range_str(self.mod_pos, + self.mod_range)) + + def range_str(self, pos, range): + """Return a file range, special-casing for 1-line files. + + :param pos: The position in the file + :type pos: int + :range: The range in the file + :type range: int + :return: a string in the format 1,4 except when range == pos == 1 + """ + if range == 1: + return "%i" % pos + else: + return "%i,%i" % (pos, range) + + def __str__(self): + lines = [self.get_header()] + for line in self.lines: + lines.append(str(line)) + return "".join(lines) + + def shift_to_mod(self, pos): + if pos < self.orig_pos-1: + return 0 + elif pos > self.orig_pos+self.orig_range: + return self.mod_range - self.orig_range + else: + return self.shift_to_mod_lines(pos) + + def shift_to_mod_lines(self, pos): + assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range) + position = self.orig_pos-1 + shift = 0 + for line in self.lines: + if isinstance(line, InsertLine): + shift += 1 + elif isinstance(line, RemoveLine): + if position == pos: + return None + shift -= 1 + position += 1 + elif isinstance(line, ContextLine): + position += 1 + if position > pos: + break + return shift + +def iter_hunks(iter_lines): + hunk = None + for line in iter_lines: + if line.startswith("@@"): + if hunk is not None: + yield hunk + hunk = hunk_from_header(line) + else: + hunk.lines.append(parse_line(line)) + + if hunk is not None: + yield hunk + +class Patch: + def __init__(self, oldname, newname): + self.oldname = oldname + self.newname = newname + self.hunks = [] + + def __str__(self): + ret = "--- %s\n+++ %s\n" % (self.oldname, self.newname) + ret += "".join([str(h) for h in self.hunks]) + return ret + + def stats_str(self): + """Return a string of patch statistics""" + removes = 0 + inserts = 0 + for hunk in self.hunks: + for line in hunk.lines: + if isinstance(line, InsertLine): + inserts+=1; + elif isinstance(line, RemoveLine): + removes+=1; + return "%i inserts, %i removes in %i hunks" % \ + (inserts, removes, len(self.hunks)) + + def pos_in_mod(self, position): + newpos = position + for hunk in self.hunks: + shift = hunk.shift_to_mod(position) + if shift is None: + return None + newpos += shift + return newpos + + def iter_inserted(self): + """Iteraties through inserted lines + + :return: Pair of line number, line + :rtype: iterator of (int, InsertLine) + """ + for hunk in self.hunks: + pos = hunk.mod_pos - 1; + for line in hunk.lines: + if isinstance(line, InsertLine): + yield (pos, line) + pos += 1 + if isinstance(line, ContextLine): + pos += 1 + +def parse_patch(iter_lines): + (orig_name, mod_name) = get_patch_names(iter_lines) + patch = Patch(orig_name, mod_name) + for hunk in iter_hunks(iter_lines): + patch.hunks.append(hunk) + return patch + + +class AnnotateLine: + """A line associated with the log that produced it""" + def __init__(self, text, log=None): + self.text = text + self.log = log + +class CantGetRevisionData(Exception): + def __init__(self, revision): + Exception.__init__(self, "Can't get data for revision %s" % revision) + +def annotate_file2(file_lines, anno_iter): + for result in iter_annotate_file(file_lines, anno_iter): + pass + return result + + +def iter_annotate_file(file_lines, anno_iter): + lines = [AnnotateLine(f) for f in file_lines] + patches = [] + try: + for result in anno_iter: + if isinstance(result, progress.Progress): + yield result + continue + log, iter_inserted, patch = result + for (num, line) in iter_inserted: + old_num = num + + for cur_patch in patches: + num = cur_patch.pos_in_mod(num) + if num == None: + break + + if num >= len(lines): + continue + if num is not None and lines[num].log is None: + lines[num].log = log + patches=[patch]+patches + except CantGetRevisionData: + pass + yield lines + + +def difference_index(atext, btext): + """Find the indext of the first character that differs betweeen two texts + + :param atext: The first text + :type atext: str + :param btext: The second text + :type str: str + :return: The index, or None if there are no differences within the range + :rtype: int or NoneType + """ + length = len(atext) + if len(btext) < length: + length = len(btext) + for i in range(length): + if atext[i] != btext[i]: + return i; + return None + + +def test(): + import unittest + class PatchesTester(unittest.TestCase): + def testValidPatchHeader(self): + """Parse a valid patch header""" + lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n') + (orig, mod) = get_patch_names(lines.__iter__()) + assert(orig == "orig/commands.py") + assert(mod == "mod/dommands.py") + + def testInvalidPatchHeader(self): + """Parse an invalid patch header""" + lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n') + self.assertRaises(MalformedPatchHeader, get_patch_names, + lines.__iter__()) + + def testValidHunkHeader(self): + """Parse a valid hunk header""" + header = "@@ -34,11 +50,6 @@\n" + hunk = hunk_from_header(header); + assert (hunk.orig_pos == 34) + assert (hunk.orig_range == 11) + assert (hunk.mod_pos == 50) + assert (hunk.mod_range == 6) + assert (str(hunk) == header) + + def testValidHunkHeader2(self): + """Parse a tricky, valid hunk header""" + header = "@@ -1 +0,0 @@\n" + hunk = hunk_from_header(header); + assert (hunk.orig_pos == 1) + assert (hunk.orig_range == 1) + assert (hunk.mod_pos == 0) + assert (hunk.mod_range == 0) + assert (str(hunk) == header) + + def makeMalformed(self, header): + self.assertRaises(MalformedHunkHeader, hunk_from_header, header) + + def testInvalidHeader(self): + """Parse an invalid hunk header""" + self.makeMalformed(" -34,11 +50,6 \n") + self.makeMalformed("@@ +50,6 -34,11 @@\n") + self.makeMalformed("@@ -34,11 +50,6 @@") + self.makeMalformed("@@ -34.5,11 +50,6 @@\n") + self.makeMalformed("@@-34,11 +50,6@@\n") + self.makeMalformed("@@ 34,11 50,6 @@\n") + self.makeMalformed("@@ -34,11 @@\n") + self.makeMalformed("@@ -34,11 +50,6.5 @@\n") + self.makeMalformed("@@ -34,11 +50,-6 @@\n") + + def lineThing(self,text, type): + line = parse_line(text) + assert(isinstance(line, type)) + assert(str(line)==text) + + def makeMalformedLine(self, text): + self.assertRaises(MalformedLine, parse_line, text) + + def testValidLine(self): + """Parse a valid hunk line""" + self.lineThing(" hello\n", ContextLine) + self.lineThing("+hello\n", InsertLine) + self.lineThing("-hello\n", RemoveLine) + + def testMalformedLine(self): + """Parse invalid valid hunk lines""" + self.makeMalformedLine("hello\n") + + def compare_parsed(self, patchtext): + lines = patchtext.splitlines(True) + patch = parse_patch(lines.__iter__()) + pstr = str(patch) + i = difference_index(patchtext, pstr) + if i is not None: + print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i]) + assert (patchtext == str(patch)) + + def testAll(self): + """Test parsing a whole patch""" + patchtext = """--- orig/commands.py ++++ mod/commands.py +@@ -1337,7 +1337,8 @@ + + def set_title(self, command=None): + try: +- version = self.tree.tree_version.nonarch ++ version = pylon.alias_or_version(self.tree.tree_version, self.tree, ++ full=False) + except: + version = "[no version]" + if command is None: +@@ -1983,7 +1984,11 @@ + version) + if len(new_merges) > 0: + if cmdutil.prompt("Log for merge"): +- mergestuff = cmdutil.log_for_merge(tree, comp_version) ++ if cmdutil.prompt("changelog for merge"): ++ mergestuff = "Patches applied:\\n" ++ mergestuff += pylon.changelog_for_merge(new_merges) ++ else: ++ mergestuff = cmdutil.log_for_merge(tree, comp_version) + log.description += mergestuff + log.save() + try: +""" + self.compare_parsed(patchtext) + + def testInit(self): + """Handle patches missing half the position, range tuple""" + patchtext = \ +"""--- orig/__init__.py ++++ mod/__init__.py +@@ -1 +1,2 @@ + __docformat__ = "restructuredtext en" ++__doc__ = An alternate Arch commandline interface""" + self.compare_parsed(patchtext) + + + + def testLineLookup(self): + """Make sure we can accurately look up mod line from orig""" + patch = parse_patch(open("testdata/diff")) + orig = list(open("testdata/orig")) + mod = list(open("testdata/mod")) + removals = [] + for i in range(len(orig)): + mod_pos = patch.pos_in_mod(i) + if mod_pos is None: + removals.append(orig[i]) + continue + assert(mod[mod_pos]==orig[i]) + rem_iter = removals.__iter__() + for hunk in patch.hunks: + for line in hunk.lines: + if isinstance(line, RemoveLine): + next = rem_iter.next() + if line.contents != next: + sys.stdout.write(" orig:%spatch:%s" % (next, + line.contents)) + assert(line.contents == next) + self.assertRaises(StopIteration, rem_iter.next) + + def testFirstLineRenumber(self): + """Make sure we handle lines at the beginning of the hunk""" + patch = parse_patch(open("testdata/insert_top.patch")) + assert (patch.pos_in_mod(0)==1) + + + patchesTestSuite = unittest.makeSuite(PatchesTester,'test') + runner = unittest.TextTestRunner(verbosity=0) + return runner.run(patchesTestSuite) + + +if __name__ == "__main__": + test() +# arch-tag: d1541a25-eac5-4de9-a476-08a7cecd5683 *** added directory 'testdata' *** added file 'testdata/diff' --- /dev/null +++ testdata/diff @@ -0,0 +1,1154 @@ +--- orig/commands.py ++++ mod/commands.py +@@ -19,25 +19,31 @@ + import arch + import arch.util + import arch.arch ++ ++import pylon.errors ++from pylon.errors import * ++from pylon import errors ++from pylon import util ++from pylon import arch_core ++from pylon import arch_compound ++from pylon import ancillary ++from pylon import misc ++from pylon import paths ++ + import abacmds + import cmdutil + import shutil + import os + import options +-import paths + import time + import cmd + import readline + import re + import string +-import arch_core +-from errors import * +-import errors + import terminal +-import ancillary +-import misc + import email + import smtplib ++import textwrap + + __docformat__ = "restructuredtext" + __doc__ = "Implementation of user (sub) commands" +@@ -257,7 +263,7 @@ + + tree=arch.tree_root() + if len(args) == 0: +- a_spec = cmdutil.comp_revision(tree) ++ a_spec = ancillary.comp_revision(tree) + else: + a_spec = cmdutil.determine_revision_tree(tree, args[0]) + cmdutil.ensure_archive_registered(a_spec.archive) +@@ -284,7 +290,7 @@ + changeset=options.changeset + tmpdir = None + else: +- tmpdir=cmdutil.tmpdir() ++ tmpdir=util.tmpdir() + changeset=tmpdir+"/changeset" + try: + delta=arch.iter_delta(a_spec, b_spec, changeset) +@@ -304,14 +310,14 @@ + if status > 1: + return + if (options.perform_diff): +- chan = cmdutil.ChangesetMunger(changeset) ++ chan = arch_compound.ChangesetMunger(changeset) + chan.read_indices() +- if isinstance(b_spec, arch.Revision): +- b_dir = b_spec.library_find() +- else: +- b_dir = b_spec +- a_dir = a_spec.library_find() + if options.diffopts is not None: ++ if isinstance(b_spec, arch.Revision): ++ b_dir = b_spec.library_find() ++ else: ++ b_dir = b_spec ++ a_dir = a_spec.library_find() + diffopts = options.diffopts.split() + cmdutil.show_custom_diffs(chan, diffopts, a_dir, b_dir) + else: +@@ -517,7 +523,7 @@ + except arch.errors.TreeRootError, e: + print e + return +- from_revision=cmdutil.tree_latest(tree) ++ from_revision = arch_compound.tree_latest(tree) + if from_revision==to_revision: + print "Tree is already up to date with:\n"+str(to_revision)+"." + return +@@ -592,6 +598,9 @@ + + if len(args) == 0: + args = None ++ if options.version is None: ++ return options, tree.tree_version, args ++ + revision=cmdutil.determine_revision_arch(tree, options.version) + return options, revision.get_version(), args + +@@ -601,11 +610,16 @@ + """ + tree=arch.tree_root() + options, version, files = self.parse_commandline(cmdargs, tree) ++ ancestor = None + if options.__dict__.has_key("base") and options.base: + base = cmdutil.determine_revision_tree(tree, options.base) ++ ancestor = base + else: +- base = cmdutil.submit_revision(tree) +- ++ base = ancillary.submit_revision(tree) ++ ancestor = base ++ if ancestor is None: ++ ancestor = arch_compound.tree_latest(tree, version) ++ + writeversion=version + archive=version.archive + source=cmdutil.get_mirror_source(archive) +@@ -625,18 +639,26 @@ + try: + last_revision=tree.iter_logs(version, True).next().revision + except StopIteration, e: +- if cmdutil.prompt("Import from commit"): +- return do_import(version) +- else: +- raise NoVersionLogs(version) +- if last_revision!=version.iter_revisions(True).next(): ++ last_revision = None ++ if ancestor is None: ++ if cmdutil.prompt("Import from commit"): ++ return do_import(version) ++ else: ++ raise NoVersionLogs(version) ++ try: ++ arch_last_revision = version.iter_revisions(True).next() ++ except StopIteration, e: ++ arch_last_revision = None ++ ++ if last_revision != arch_last_revision: ++ print "Tree is not up to date with %s" % str(version) + if not cmdutil.prompt("Out of date"): + raise OutOfDate + else: + allow_old=True + + try: +- if not cmdutil.has_changed(version): ++ if not cmdutil.has_changed(ancestor): + if not cmdutil.prompt("Empty commit"): + raise EmptyCommit + except arch.util.ExecProblem, e: +@@ -645,15 +667,15 @@ + raise MissingID(e) + else: + raise +- log = tree.log_message(create=False) ++ log = tree.log_message(create=False, version=version) + if log is None: + try: + if cmdutil.prompt("Create log"): +- edit_log(tree) ++ edit_log(tree, version) + + except cmdutil.NoEditorSpecified, e: + raise CommandFailed(e) +- log = tree.log_message(create=False) ++ log = tree.log_message(create=False, version=version) + if log is None: + raise NoLogMessage + if log["Summary"] is None or len(log["Summary"].strip()) == 0: +@@ -837,23 +859,24 @@ + if spec is not None: + revision = cmdutil.determine_revision_tree(tree, spec) + else: +- revision = cmdutil.comp_revision(tree) ++ revision = ancillary.comp_revision(tree) + except cmdutil.CantDetermineRevision, e: + raise CommandFailedWrapper(e) + munger = None + + if options.file_contents or options.file_perms or options.deletions\ + or options.additions or options.renames or options.hunk_prompt: +- munger = cmdutil.MungeOpts() +- munger.hunk_prompt = options.hunk_prompt ++ munger = arch_compound.MungeOpts() ++ munger.set_hunk_prompt(cmdutil.colorize, cmdutil.user_hunk_confirm, ++ options.hunk_prompt) + + if len(args) > 0 or options.logs or options.pattern_files or \ + options.control: + if munger is None: +- munger = cmdutil.MungeOpts(True) ++ munger = cmdutil.arch_compound.MungeOpts(True) + munger.all_types(True) + if len(args) > 0: +- t_cwd = cmdutil.tree_cwd(tree) ++ t_cwd = arch_compound.tree_cwd(tree) + for name in args: + if len(t_cwd) > 0: + t_cwd += "/" +@@ -878,7 +901,7 @@ + if options.pattern_files: + munger.add_keep_pattern(options.pattern_files) + +- for line in cmdutil.revert(tree, revision, munger, ++ for line in arch_compound.revert(tree, revision, munger, + not options.no_output): + cmdutil.colorize(line) + +@@ -1042,18 +1065,13 @@ + help_tree_spec() + return + +-def require_version_exists(version, spec): +- if not version.exists(): +- raise cmdutil.CantDetermineVersion(spec, +- "The version %s does not exist." \ +- % version) +- + class Revisions(BaseCommand): + """ + Print a revision name based on a revision specifier + """ + def __init__(self): + self.description="Lists revisions" ++ self.cl_revisions = [] + + def do_command(self, cmdargs): + """ +@@ -1066,224 +1084,68 @@ + self.tree = arch.tree_root() + except arch.errors.TreeRootError: + self.tree = None ++ if options.type == "default": ++ options.type = "archive" + try: +- iter = self.get_iterator(options.type, args, options.reverse, +- options.modified) ++ iter = cmdutil.revision_iterator(self.tree, options.type, args, ++ options.reverse, options.modified, ++ options.shallow) + except cmdutil.CantDetermineRevision, e: + raise CommandFailedWrapper(e) +- ++ except cmdutil.CantDetermineVersion, e: ++ raise CommandFailedWrapper(e) + if options.skip is not None: + iter = cmdutil.iter_skip(iter, int(options.skip)) + +- for revision in iter: +- log = None +- if isinstance(revision, arch.Patchlog): +- log = revision +- revision=revision.revision +- print options.display(revision) +- if log is None and (options.summary or options.creator or +- options.date or options.merges): +- log = revision.patchlog +- if options.creator: +- print " %s" % log.creator +- if options.date: +- print " %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date) +- if options.summary: +- print " %s" % log.summary +- if options.merges: +- showed_title = False +- for revision in log.merged_patches: +- if not showed_title: +- print " Merged:" +- showed_title = True +- print " %s" % revision +- +- def get_iterator(self, type, args, reverse, modified): +- if len(args) > 0: +- spec = args[0] +- else: +- spec = None +- if modified is not None: +- iter = cmdutil.modified_iter(modified, self.tree) +- if reverse: +- return iter +- else: +- return cmdutil.iter_reverse(iter) +- elif type == "archive": +- if spec is None: +- if self.tree is None: +- raise cmdutil.CantDetermineRevision("", +- "Not in a project tree") +- version = cmdutil.determine_version_tree(spec, self.tree) +- else: +- version = cmdutil.determine_version_arch(spec, self.tree) +- cmdutil.ensure_archive_registered(version.archive) +- require_version_exists(version, spec) +- return version.iter_revisions(reverse) +- elif type == "cacherevs": +- if spec is None: +- if self.tree is None: +- raise cmdutil.CantDetermineRevision("", +- "Not in a project tree") +- version = cmdutil.determine_version_tree(spec, self.tree) +- else: +- version = cmdutil.determine_version_arch(spec, self.tree) +- cmdutil.ensure_archive_registered(version.archive) +- require_version_exists(version, spec) +- return cmdutil.iter_cacherevs(version, reverse) +- elif type == "library": +- if spec is None: +- if self.tree is None: +- raise cmdutil.CantDetermineRevision("", +- "Not in a project tree") +- version = cmdutil.determine_version_tree(spec, self.tree) +- else: +- version = cmdutil.determine_version_arch(spec, self.tree) +- return version.iter_library_revisions(reverse) +- elif type == "logs": +- if self.tree is None: +- raise cmdutil.CantDetermineRevision("", "Not in a project tree") +- return self.tree.iter_logs(cmdutil.determine_version_tree(spec, \ +- self.tree), reverse) +- elif type == "missing" or type == "skip-present": +- if self.tree is None: +- raise cmdutil.CantDetermineRevision("", "Not in a project tree") +- skip = (type == "skip-present") +- version = cmdutil.determine_version_tree(spec, self.tree) +- cmdutil.ensure_archive_registered(version.archive) +- require_version_exists(version, spec) +- return cmdutil.iter_missing(self.tree, version, reverse, +- skip_present=skip) +- +- elif type == "present": +- if self.tree is None: +- raise cmdutil.CantDetermineRevision("", "Not in a project tree") +- version = cmdutil.determine_version_tree(spec, self.tree) +- cmdutil.ensure_archive_registered(version.archive) +- require_version_exists(version, spec) +- return cmdutil.iter_present(self.tree, version, reverse) +- +- elif type == "new-merges" or type == "direct-merges": +- if self.tree is None: +- raise cmdutil.CantDetermineRevision("", "Not in a project tree") +- version = cmdutil.determine_version_tree(spec, self.tree) +- cmdutil.ensure_archive_registered(version.archive) +- require_version_exists(version, spec) +- iter = cmdutil.iter_new_merges(self.tree, version, reverse) +- if type == "new-merges": +- return iter +- elif type == "direct-merges": +- return cmdutil.direct_merges(iter) +- +- elif type == "missing-from": +- if self.tree is None: +- raise cmdutil.CantDetermineRevision("", "Not in a project tree") +- revision = cmdutil.determine_revision_tree(self.tree, spec) +- libtree = cmdutil.find_or_make_local_revision(revision) +- return cmdutil.iter_missing(libtree, self.tree.tree_version, +- reverse) +- +- elif type == "partner-missing": +- return cmdutil.iter_partner_missing(self.tree, reverse) +- +- elif type == "ancestry": +- revision = cmdutil.determine_revision_tree(self.tree, spec) +- iter = cmdutil._iter_ancestry(self.tree, revision) +- if reverse: +- return iter +- else: +- return cmdutil.iter_reverse(iter) +- +- elif type == "dependencies" or type == "non-dependencies": +- nondeps = (type == "non-dependencies") +- revision = cmdutil.determine_revision_tree(self.tree, spec) +- anc_iter = cmdutil._iter_ancestry(self.tree, revision) +- iter_depends = cmdutil.iter_depends(anc_iter, nondeps) +- if reverse: +- return iter_depends +- else: +- return cmdutil.iter_reverse(iter_depends) +- elif type == "micro": +- return cmdutil.iter_micro(self.tree) +- +- ++ try: ++ for revision in iter: ++ log = None ++ if isinstance(revision, arch.Patchlog): ++ log = revision ++ revision=revision.revision ++ out = options.display(revision) ++ if out is not None: ++ print out ++ if log is None and (options.summary or options.creator or ++ options.date or options.merges): ++ log = revision.patchlog ++ if options.creator: ++ print " %s" % log.creator ++ if options.date: ++ print " %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date) ++ if options.summary: ++ print " %s" % log.summary ++ if options.merges: ++ showed_title = False ++ for revision in log.merged_patches: ++ if not showed_title: ++ print " Merged:" ++ showed_title = True ++ print " %s" % revision ++ if len(self.cl_revisions) > 0: ++ print pylon.changelog_for_merge(self.cl_revisions) ++ except pylon.errors.TreeRootNone: ++ raise CommandFailedWrapper( ++ Exception("This option can only be used in a project tree.")) ++ ++ def changelog_append(self, revision): ++ if isinstance(revision, arch.Revision): ++ revision=arch.Patchlog(revision) ++ self.cl_revisions.append(revision) ++ + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ +- parser=cmdutil.CmdOptionParser("fai revisions [revision]") ++ parser=cmdutil.CmdOptionParser("fai revisions [version/revision]") + select = cmdutil.OptionGroup(parser, "Selection options", + "Control which revisions are listed. These options" + " are mutually exclusive. If more than one is" + " specified, the last is used.") +- select.add_option("", "--archive", action="store_const", +- const="archive", dest="type", default="archive", +- help="List all revisions in the archive") +- select.add_option("", "--cacherevs", action="store_const", +- const="cacherevs", dest="type", +- help="List all revisions stored in the archive as " +- "complete copies") +- select.add_option("", "--logs", action="store_const", +- const="logs", dest="type", +- help="List revisions that have a patchlog in the " +- "tree") +- select.add_option("", "--missing", action="store_const", +- const="missing", dest="type", +- help="List revisions from the specified version that" +- " have no patchlog in the tree") +- select.add_option("", "--skip-present", action="store_const", +- const="skip-present", dest="type", +- help="List revisions from the specified version that" +- " have no patchlogs at all in the tree") +- select.add_option("", "--present", action="store_const", +- const="present", dest="type", +- help="List revisions from the specified version that" +- " have no patchlog in the tree, but can't be merged") +- select.add_option("", "--missing-from", action="store_const", +- const="missing-from", dest="type", +- help="List revisions from the specified revision " +- "that have no patchlog for the tree version") +- select.add_option("", "--partner-missing", action="store_const", +- const="partner-missing", dest="type", +- help="List revisions in partner versions that are" +- " missing") +- select.add_option("", "--new-merges", action="store_const", +- const="new-merges", dest="type", +- help="List revisions that have had patchlogs added" +- " to the tree since the last commit") +- select.add_option("", "--direct-merges", action="store_const", +- const="direct-merges", dest="type", +- help="List revisions that have been directly added" +- " to tree since the last commit ") +- select.add_option("", "--library", action="store_const", +- const="library", dest="type", +- help="List revisions in the revision library") +- select.add_option("", "--ancestry", action="store_const", +- const="ancestry", dest="type", +- help="List revisions that are ancestors of the " +- "current tree version") +- +- select.add_option("", "--dependencies", action="store_const", +- const="dependencies", dest="type", +- help="List revisions that the given revision " +- "depends on") +- +- select.add_option("", "--non-dependencies", action="store_const", +- const="non-dependencies", dest="type", +- help="List revisions that the given revision " +- "does not depend on") +- +- select.add_option("--micro", action="store_const", +- const="micro", dest="type", +- help="List partner revisions aimed for this " +- "micro-branch") +- +- select.add_option("", "--modified", dest="modified", +- help="List tree ancestor revisions that modified a " +- "given file", metavar="FILE[:LINE]") + ++ cmdutil.add_revision_iter_options(select) + parser.add_option("", "--skip", dest="skip", + help="Skip revisions. Positive numbers skip from " + "beginning, negative skip from end.", +@@ -1312,6 +1174,9 @@ + format.add_option("--cacherev", action="store_const", + const=paths.determine_cacherev_path, dest="display", + help="Show location of cacherev file") ++ format.add_option("--changelog", action="store_const", ++ const=self.changelog_append, dest="display", ++ help="Show location of cacherev file") + parser.add_option_group(format) + display = cmdutil.OptionGroup(parser, "Display format options", + "These control the display of data") +@@ -1448,6 +1313,7 @@ + if os.access(self.history_file, os.R_OK) and \ + os.path.isfile(self.history_file): + readline.read_history_file(self.history_file) ++ self.cwd = os.getcwd() + + def write_history(self): + readline.write_history_file(self.history_file) +@@ -1470,16 +1336,21 @@ + def set_prompt(self): + if self.tree is not None: + try: +- version = " "+self.tree.tree_version.nonarch ++ prompt = pylon.alias_or_version(self.tree.tree_version, ++ self.tree, ++ full=False) ++ if prompt is not None: ++ prompt = " " + prompt + except: +- version = "" ++ prompt = "" + else: +- version = "" +- self.prompt = "Fai%s> " % version ++ prompt = "" ++ self.prompt = "Fai%s> " % prompt + + def set_title(self, command=None): + try: +- version = self.tree.tree_version.nonarch ++ version = pylon.alias_or_version(self.tree.tree_version, self.tree, ++ full=False) + except: + version = "[no version]" + if command is None: +@@ -1489,8 +1360,15 @@ + def do_cd(self, line): + if line == "": + line = "~" ++ line = os.path.expanduser(line) ++ if os.path.isabs(line): ++ newcwd = line ++ else: ++ newcwd = self.cwd+'/'+line ++ newcwd = os.path.normpath(newcwd) + try: +- os.chdir(os.path.expanduser(line)) ++ os.chdir(newcwd) ++ self.cwd = newcwd + except Exception, e: + print e + try: +@@ -1523,7 +1401,7 @@ + except cmdutil.CantDetermineRevision, e: + print e + except Exception, e: +- print "Unhandled error:\n%s" % cmdutil.exception_str(e) ++ print "Unhandled error:\n%s" % errors.exception_str(e) + + elif suggestions.has_key(args[0]): + print suggestions[args[0]] +@@ -1574,7 +1452,7 @@ + arg = line.split()[-1] + else: + arg = "" +- iter = iter_munged_completions(iter, arg, text) ++ iter = cmdutil.iter_munged_completions(iter, arg, text) + except Exception, e: + print e + return list(iter) +@@ -1604,10 +1482,11 @@ + else: + arg = "" + if arg.startswith("-"): +- return list(iter_munged_completions(iter, arg, text)) ++ return list(cmdutil.iter_munged_completions(iter, arg, ++ text)) + else: +- return list(iter_munged_completions( +- iter_file_completions(arg), arg, text)) ++ return list(cmdutil.iter_munged_completions( ++ cmdutil.iter_file_completions(arg), arg, text)) + + + elif cmd == "cd": +@@ -1615,13 +1494,13 @@ + arg = args.split()[-1] + else: + arg = "" +- iter = iter_dir_completions(arg) +- iter = iter_munged_completions(iter, arg, text) ++ iter = cmdutil.iter_dir_completions(arg) ++ iter = cmdutil.iter_munged_completions(iter, arg, text) + return list(iter) + elif len(args)>0: + arg = args.split()[-1] +- return list(iter_munged_completions(iter_file_completions(arg), +- arg, text)) ++ iter = cmdutil.iter_file_completions(arg) ++ return list(cmdutil.iter_munged_completions(iter, arg, text)) + else: + return self.completenames(text, line, begidx, endidx) + except Exception, e: +@@ -1636,44 +1515,8 @@ + yield entry + + +-def iter_file_completions(arg, only_dirs = False): +- """Generate an iterator that iterates through filename completions. +- +- :param arg: The filename fragment to match +- :type arg: str +- :param only_dirs: If true, match only directories +- :type only_dirs: bool +- """ +- cwd = os.getcwd() +- if cwd != "/": +- extras = [".", ".."] +- else: +- extras = [] +- (dir, file) = os.path.split(arg) +- if dir != "": +- listingdir = os.path.expanduser(dir) +- else: +- listingdir = cwd +- for file in cmdutil.iter_combine([os.listdir(listingdir), extras]): +- if dir != "": +- userfile = dir+'/'+file +- else: +- userfile = file +- if userfile.startswith(arg): +- if os.path.isdir(listingdir+'/'+file): +- userfile+='/' +- yield userfile +- elif not only_dirs: +- yield userfile +- +-def iter_munged_completions(iter, arg, text): +- for completion in iter: +- completion = str(completion) +- if completion.startswith(arg): +- yield completion[len(arg)-len(text):] +- + def iter_source_file_completions(tree, arg): +- treepath = cmdutil.tree_cwd(tree) ++ treepath = arch_compound.tree_cwd(tree) + if len(treepath) > 0: + dirs = [treepath] + else: +@@ -1701,7 +1544,7 @@ + :return: An iterator of all matching untagged files + :rtype: iterator of str + """ +- treepath = cmdutil.tree_cwd(tree) ++ treepath = arch_compound.tree_cwd(tree) + if len(treepath) > 0: + dirs = [treepath] + else: +@@ -1743,8 +1586,8 @@ + :param arg: The prefix to match + :type arg: str + """ +- treepath = cmdutil.tree_cwd(tree) +- tmpdir = cmdutil.tmpdir() ++ treepath = arch_compound.tree_cwd(tree) ++ tmpdir = util.tmpdir() + changeset = tmpdir+"/changeset" + completions = [] + revision = cmdutil.determine_revision_tree(tree) +@@ -1756,14 +1599,6 @@ + shutil.rmtree(tmpdir) + return completions + +-def iter_dir_completions(arg): +- """Generate an iterator that iterates through directory name completions. +- +- :param arg: The directory name fragment to match +- :type arg: str +- """ +- return iter_file_completions(arg, True) +- + class Shell(BaseCommand): + def __init__(self): + self.description = "Runs Fai as a shell" +@@ -1795,7 +1630,11 @@ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + +- tree = arch.tree_root() ++ try: ++ tree = arch.tree_root() ++ except arch.errors.TreeRootError, e: ++ raise pylon.errors.CommandFailedWrapper(e) ++ + + if (len(args) == 0) == (options.untagged == False): + raise cmdutil.GetHelp +@@ -1809,13 +1648,22 @@ + if options.id_type == "tagline": + if method != "tagline": + if not cmdutil.prompt("Tagline in other tree"): +- if method == "explicit": +- options.id_type == explicit ++ if method == "explicit" or method == "implicit": ++ options.id_type == method + else: + print "add-id not supported for \"%s\" tagging method"\ + % method + return + ++ elif options.id_type == "implicit": ++ if method != "implicit": ++ if not cmdutil.prompt("Implicit in other tree"): ++ if method == "explicit" or method == "tagline": ++ options.id_type == method ++ else: ++ print "add-id not supported for \"%s\" tagging method"\ ++ % method ++ return + elif options.id_type == "explicit": + if method != "tagline" and method != explicit: + if not prompt("Explicit in other tree"): +@@ -1824,7 +1672,8 @@ + return + + if options.id_type == "auto": +- if method != "tagline" and method != "explicit": ++ if method != "tagline" and method != "explicit" \ ++ and method !="implicit": + print "add-id not supported for \"%s\" tagging method" % method + return + else: +@@ -1852,10 +1701,12 @@ + previous_files.extend(files) + if id_type == "explicit": + cmdutil.add_id(files) +- elif id_type == "tagline": ++ elif id_type == "tagline" or id_type == "implicit": + for file in files: + try: +- cmdutil.add_tagline_or_explicit_id(file) ++ implicit = (id_type == "implicit") ++ cmdutil.add_tagline_or_explicit_id(file, False, ++ implicit) + except cmdutil.AlreadyTagged: + print "\"%s\" already has a tagline." % file + except cmdutil.NoCommentSyntax: +@@ -1888,6 +1739,9 @@ + parser.add_option("--tagline", action="store_const", + const="tagline", dest="id_type", + help="Use a tagline id") ++ parser.add_option("--implicit", action="store_const", ++ const="implicit", dest="id_type", ++ help="Use an implicit id (deprecated)") + parser.add_option("--untagged", action="store_true", + dest="untagged", default=False, + help="tag all untagged files") +@@ -1926,27 +1780,7 @@ + def get_completer(self, arg, index): + if self.tree is None: + raise arch.errors.TreeRootError +- completions = list(ancillary.iter_partners(self.tree, +- self.tree.tree_version)) +- if len(completions) == 0: +- completions = list(self.tree.iter_log_versions()) +- +- aliases = [] +- try: +- for completion in completions: +- alias = ancillary.compact_alias(str(completion), self.tree) +- if alias: +- aliases.extend(alias) +- +- for completion in completions: +- if completion.archive == self.tree.tree_version.archive: +- aliases.append(completion.nonarch) +- +- except Exception, e: +- print e +- +- completions.extend(aliases) +- return completions ++ return cmdutil.merge_completions(self.tree, arg, index) + + def do_command(self, cmdargs): + """ +@@ -1961,7 +1795,7 @@ + + if self.tree is None: + raise arch.errors.TreeRootError(os.getcwd()) +- if cmdutil.has_changed(self.tree.tree_version): ++ if cmdutil.has_changed(ancillary.comp_revision(self.tree)): + raise UncommittedChanges(self.tree) + + if len(args) > 0: +@@ -2027,14 +1861,14 @@ + :type other_revision: `arch.Revision` + :return: 0 if the merge was skipped, 1 if it was applied + """ +- other_tree = cmdutil.find_or_make_local_revision(other_revision) ++ other_tree = arch_compound.find_or_make_local_revision(other_revision) + try: + if action == "native-merge": +- ancestor = cmdutil.merge_ancestor2(self.tree, other_tree, +- other_revision) ++ ancestor = arch_compound.merge_ancestor2(self.tree, other_tree, ++ other_revision) + elif action == "update": +- ancestor = cmdutil.tree_latest(self.tree, +- other_revision.version) ++ ancestor = arch_compound.tree_latest(self.tree, ++ other_revision.version) + except CantDetermineRevision, e: + raise CommandFailedWrapper(e) + cmdutil.colorize(arch.Chatter("* Found common ancestor %s" % ancestor)) +@@ -2104,7 +1938,10 @@ + if self.tree is None: + raise arch.errors.TreeRootError + +- edit_log(self.tree) ++ try: ++ edit_log(self.tree, self.tree.tree_version) ++ except pylon.errors.NoEditorSpecified, e: ++ raise pylon.errors.CommandFailedWrapper(e) + + def get_parser(self): + """ +@@ -2132,7 +1969,7 @@ + """ + return + +-def edit_log(tree): ++def edit_log(tree, version): + """Makes and edits the log for a tree. Does all kinds of fancy things + like log templates and merge summaries and log-for-merge + +@@ -2141,28 +1978,29 @@ + """ + #ensure we have an editor before preparing the log + cmdutil.find_editor() +- log = tree.log_message(create=False) ++ log = tree.log_message(create=False, version=version) + log_is_new = False + if log is None or cmdutil.prompt("Overwrite log"): + if log is not None: + os.remove(log.name) +- log = tree.log_message(create=True) ++ log = tree.log_message(create=True, version=version) + log_is_new = True + tmplog = log.name +- template = tree+"/{arch}/=log-template" +- if not os.path.exists(template): +- template = os.path.expanduser("~/.arch-params/=log-template") +- if not os.path.exists(template): +- template = None ++ template = pylon.log_template_path(tree) + if template: + shutil.copyfile(template, tmplog) +- +- new_merges = list(cmdutil.iter_new_merges(tree, +- tree.tree_version)) +- log["Summary"] = merge_summary(new_merges, tree.tree_version) ++ comp_version = ancillary.comp_revision(tree).version ++ new_merges = cmdutil.iter_new_merges(tree, comp_version) ++ new_merges = cmdutil.direct_merges(new_merges) ++ log["Summary"] = pylon.merge_summary(new_merges, ++ version) + if len(new_merges) > 0: + if cmdutil.prompt("Log for merge"): +- mergestuff = cmdutil.log_for_merge(tree) ++ if cmdutil.prompt("changelog for merge"): ++ mergestuff = "Patches applied:\n" ++ mergestuff += pylon.changelog_for_merge(new_merges) ++ else: ++ mergestuff = cmdutil.log_for_merge(tree, comp_version) + log.description += mergestuff + log.save() + try: +@@ -2172,29 +2010,6 @@ + os.remove(log.name) + raise + +-def merge_summary(new_merges, tree_version): +- if len(new_merges) == 0: +- return "" +- if len(new_merges) == 1: +- summary = new_merges[0].summary +- else: +- summary = "Merge" +- +- credits = [] +- for merge in new_merges: +- if arch.my_id() != merge.creator: +- name = re.sub("<.*>", "", merge.creator).rstrip(" "); +- if not name in credits: +- credits.append(name) +- else: +- version = merge.revision.version +- if version.archive == tree_version.archive: +- if not version.nonarch in credits: +- credits.append(version.nonarch) +- elif not str(version) in credits: +- credits.append(str(version)) +- +- return ("%s (%s)") % (summary, ", ".join(credits)) + + class MirrorArchive(BaseCommand): + """ +@@ -2268,31 +2083,73 @@ + + Use "alias" to list available (user and automatic) aliases.""" + ++auto_alias = [ ++"acur", ++"The latest revision in the archive of the tree-version. You can specify \ ++a different version like so: acur:foo--bar--0 (aliases can be used)", ++"tcur", ++"""(tree current) The latest revision in the tree of the tree-version. \ ++You can specify a different version like so: tcur:foo--bar--0 (aliases can be \ ++used).""", ++"tprev" , ++"""(tree previous) The previous revision in the tree of the tree-version. To \ ++specify an older revision, use a number, e.g. "tprev:4" """, ++"tanc" , ++"""(tree ancestor) The ancestor revision of the tree To specify an older \ ++revision, use a number, e.g. "tanc:4".""", ++"tdate" , ++"""(tree date) The latest revision from a given date, e.g. "tdate:July 6".""", ++"tmod" , ++""" (tree modified) The latest revision to modify a given file, e.g. \ ++"tmod:engine.cpp" or "tmod:engine.cpp:16".""", ++"ttag" , ++"""(tree tag) The revision that was tagged into the current tree revision, \ ++according to the tree""", ++"tagcur", ++"""(tag current) The latest revision of the version that the current tree \ ++was tagged from.""", ++"mergeanc" , ++"""The common ancestor of the current tree and the specified revision. \ ++Defaults to the first partner-version's latest revision or to tagcur.""", ++] ++ ++ ++def is_auto_alias(name): ++ """Determine whether a name is an auto alias name ++ ++ :param name: the name to check ++ :type name: str ++ :return: True if the name is an auto alias, false if not ++ :rtype: bool ++ """ ++ return name in [f for (f, v) in pylon.util.iter_pairs(auto_alias)] ++ ++ ++def display_def(iter, wrap = 80): ++ """Display a list of definitions ++ ++ :param iter: iter of name, definition pairs ++ :type iter: iter of (str, str) ++ :param wrap: The width for text wrapping ++ :type wrap: int ++ """ ++ vals = list(iter) ++ maxlen = 0 ++ for (key, value) in vals: ++ if len(key) > maxlen: ++ maxlen = len(key) ++ for (key, value) in vals: ++ tw=textwrap.TextWrapper(width=wrap, ++ initial_indent=key.rjust(maxlen)+" : ", ++ subsequent_indent="".rjust(maxlen+3)) ++ print tw.fill(value) ++ ++ + def help_aliases(tree): +- print """Auto-generated aliases +- acur : The latest revision in the archive of the tree-version. You can specfy +- a different version like so: acur:foo--bar--0 (aliases can be used) +- tcur : (tree current) The latest revision in the tree of the tree-version. +- You can specify a different version like so: tcur:foo--bar--0 (aliases +- can be used). +-tprev : (tree previous) The previous revision in the tree of the tree-version. +- To specify an older revision, use a number, e.g. "tprev:4" +- tanc : (tree ancestor) The ancestor revision of the tree +- To specify an older revision, use a number, e.g. "tanc:4" +-tdate : (tree date) The latest revision from a given date (e.g. "tdate:July 6") +- tmod : (tree modified) The latest revision to modify a given file +- (e.g. "tmod:engine.cpp" or "tmod:engine.cpp:16") +- ttag : (tree tag) The revision that was tagged into the current tree revision, +- according to the tree. +-tagcur: (tag current) The latest revision of the version that the current tree +- was tagged from. +-mergeanc : The common ancestor of the current tree and the specified revision. +- Defaults to the first partner-version's latest revision or to tagcur. +- """ ++ print """Auto-generated aliases""" ++ display_def(pylon.util.iter_pairs(auto_alias)) + print "User aliases" +- for parts in ancillary.iter_all_alias(tree): +- print parts[0].rjust(10)+" : "+parts[1] +- ++ display_def(ancillary.iter_all_alias(tree)) + + class Inventory(BaseCommand): + """List the status of files in the tree""" +@@ -2428,6 +2285,11 @@ + except cmdutil.ForbiddenAliasSyntax, e: + raise CommandFailedWrapper(e) + ++ def no_prefix(self, alias): ++ if alias.startswith("^"): ++ alias = alias[1:] ++ return alias ++ + def arg_dispatch(self, args, options): + """Add, modify, or list aliases, depending on number of arguments + +@@ -2438,15 +2300,20 @@ + if len(args) == 0: + help_aliases(self.tree) + return +- elif len(args) == 1: +- self.print_alias(args[0]) +- elif (len(args)) == 2: +- self.add(args[0], args[1], options) + else: +- raise cmdutil.GetHelp ++ alias = self.no_prefix(args[0]) ++ if len(args) == 1: ++ self.print_alias(alias) ++ elif (len(args)) == 2: ++ self.add(alias, args[1], options) ++ else: ++ raise cmdutil.GetHelp + + def print_alias(self, alias): + answer = None ++ if is_auto_alias(alias): ++ raise pylon.errors.IsAutoAlias(alias, "\"%s\" is an auto alias." ++ " Use \"revision\" to expand auto aliases." % alias) + for pair in ancillary.iter_all_alias(self.tree): + if pair[0] == alias: + answer = pair[1] +@@ -2464,6 +2331,8 @@ + :type expansion: str + :param options: The commandline options + """ ++ if is_auto_alias(alias): ++ raise IsAutoAlias(alias) + newlist = "" + written = False + new_line = "%s=%s\n" % (alias, cmdutil.expand_alias(expansion, +@@ -2490,14 +2359,17 @@ + deleted = False + if len(args) != 1: + raise cmdutil.GetHelp ++ alias = self.no_prefix(args[0]) ++ if is_auto_alias(alias): ++ raise IsAutoAlias(alias) + newlist = "" + for pair in self.get_iterator(options): +- if pair[0] != args[0]: ++ if pair[0] != alias: + newlist+="%s=%s\n" % (pair[0], pair[1]) + else: + deleted = True + if not deleted: +- raise errors.NoSuchAlias(args[0]) ++ raise errors.NoSuchAlias(alias) + self.write_aliases(newlist, options) + + def get_alias_file(self, options): +@@ -2526,7 +2398,7 @@ + :param options: The commandline options + """ + filename = os.path.expanduser(self.get_alias_file(options)) +- file = cmdutil.NewFileVersion(filename) ++ file = util.NewFileVersion(filename) + file.write(newlist) + file.commit() + +@@ -2588,10 +2460,13 @@ + :param cmdargs: The commandline arguments + :type cmdargs: list of str + """ +- cmdutil.find_editor() + parser = self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: ++ cmdutil.find_editor() ++ except pylon.errors.NoEditorSpecified, e: ++ raise pylon.errors.CommandFailedWrapper(e) ++ try: + self.tree=arch.tree_root() + except: + self.tree=None +@@ -2655,7 +2530,7 @@ + target_revision = cmdutil.determine_revision_arch(self.tree, + args[0]) + else: +- target_revision = cmdutil.tree_latest(self.tree) ++ target_revision = arch_compound.tree_latest(self.tree) + if len(args) > 1: + merges = [ arch.Patchlog(cmdutil.determine_revision_arch( + self.tree, f)) for f in args[1:] ] +@@ -2711,7 +2586,7 @@ + + :param message: The message to send + :type message: `email.Message`""" +- server = smtplib.SMTP() ++ server = smtplib.SMTP("localhost") + server.sendmail(message['From'], message['To'], message.as_string()) + server.quit() + +@@ -2763,6 +2638,22 @@ + 'alias' : Alias, + 'request-merge': RequestMerge, + } ++ ++def my_import(mod_name): ++ module = __import__(mod_name) ++ components = mod_name.split('.') ++ for comp in components[1:]: ++ module = getattr(module, comp) ++ return module ++ ++def plugin(mod_name): ++ module = my_import(mod_name) ++ module.add_command(commands) ++ ++for file in os.listdir(sys.path[0]+"/command"): ++ if len(file) > 3 and file[-3:] == ".py" and file != "__init__.py": ++ plugin("command."+file[:-3]) ++ + suggestions = { + 'apply-delta' : "Try \"apply-changes\".", + 'delta' : "To compare two revisions, use \"changes\".", +@@ -2784,6 +2675,7 @@ + 'tagline' : "Use add-id. It uses taglines in tagline trees", + 'emlog' : "Use elog. It automatically adds log-for-merge text, if any", + 'library-revisions' : "Use revisions --library", +-'file-revert' : "Use revert FILE" ++'file-revert' : "Use revert FILE", ++'join-branch' : "Use replay --logs-only" + } + # arch-tag: 19d5739d-3708-486c-93ba-deecc3027fc7 *** added file 'testdata/insert_top.patch' --- /dev/null +++ testdata/insert_top.patch @@ -0,0 +1,7 @@ +--- orig/pylon/patches.py ++++ mod/pylon/patches.py +@@ -1,3 +1,4 @@ ++#test + import util + import sys + class PatchSyntax(Exception): *** added file 'testdata/mod' --- /dev/null +++ testdata/mod @@ -0,0 +1,2681 @@ +# Copyright (C) 2004 Aaron Bentley +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import sys +import arch +import arch.util +import arch.arch + +import pylon.errors +from pylon.errors import * +from pylon import errors +from pylon import util +from pylon import arch_core +from pylon import arch_compound +from pylon import ancillary +from pylon import misc +from pylon import paths + +import abacmds +import cmdutil +import shutil +import os +import options +import time +import cmd +import readline +import re +import string +import terminal +import email +import smtplib +import textwrap + +__docformat__ = "restructuredtext" +__doc__ = "Implementation of user (sub) commands" +commands = {} + +def find_command(cmd): + """ + Return an instance of a command type. Return None if the type isn't + registered. + + :param cmd: the name of the command to look for + :type cmd: the type of the command + """ + if commands.has_key(cmd): + return commands[cmd]() + else: + return None + +class BaseCommand: + def __call__(self, cmdline): + try: + self.do_command(cmdline.split()) + except cmdutil.GetHelp, e: + self.help() + except Exception, e: + print e + + def get_completer(index): + return None + + def complete(self, args, text): + """ + Returns a list of possible completions for the given text. + + :param args: The complete list of arguments + :type args: List of str + :param text: text to complete (may be shorter than args[-1]) + :type text: str + :rtype: list of str + """ + matches = [] + candidates = None + + if len(args) > 0: + realtext = args[-1] + else: + realtext = "" + + try: + parser=self.get_parser() + if realtext.startswith('-'): + candidates = parser.iter_options() + else: + (options, parsed_args) = parser.parse_args(args) + + if len (parsed_args) > 0: + candidates = self.get_completer(parsed_args[-1], len(parsed_args) -1) + else: + candidates = self.get_completer("", 0) + except: + pass + if candidates is None: + return + for candidate in candidates: + candidate = str(candidate) + if candidate.startswith(realtext): + matches.append(candidate[len(realtext)- len(text):]) + return matches + + +class Help(BaseCommand): + """ + Lists commands, prints help messages. + """ + def __init__(self): + self.description="Prints help mesages" + self.parser = None + + def do_command(self, cmdargs): + """ + Prints a help message. + """ + options, args = self.get_parser().parse_args(cmdargs) + if len(args) > 1: + raise cmdutil.GetHelp + + if options.native or options.suggestions or options.external: + native = options.native + suggestions = options.suggestions + external = options.external + else: + native = True + suggestions = False + external = True + + if len(args) == 0: + self.list_commands(native, suggestions, external) + return + elif len(args) == 1: + command_help(args[0]) + return + + def help(self): + self.get_parser().print_help() + print """ +If no command is specified, commands are listed. If a command is +specified, help for that command is listed. + """ + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + if self.parser is not None: + return self.parser + parser=cmdutil.CmdOptionParser("fai help [command]") + parser.add_option("-n", "--native", action="store_true", + dest="native", help="Show native commands") + parser.add_option("-e", "--external", action="store_true", + dest="external", help="Show external commands") + parser.add_option("-s", "--suggest", action="store_true", + dest="suggestions", help="Show suggestions") + self.parser = parser + return parser + + def list_commands(self, native=True, suggest=False, external=True): + """ + Lists supported commands. + + :param native: list native, python-based commands + :type native: bool + :param external: list external aba-style commands + :type external: bool + """ + if native: + print "Native Fai commands" + keys=commands.keys() + keys.sort() + for k in keys: + space="" + for i in range(28-len(k)): + space+=" " + print space+k+" : "+commands[k]().description + print + if suggest: + print "Unavailable commands and suggested alternatives" + key_list = suggestions.keys() + key_list.sort() + for key in key_list: + print "%28s : %s" % (key, suggestions[key]) + print + if external: + fake_aba = abacmds.AbaCmds() + if (fake_aba.abadir == ""): + return + print "External commands" + fake_aba.list_commands() + print + if not suggest: + print "Use help --suggest to list alternatives to tla and aba"\ + " commands." + if options.tla_fallthrough and (native or external): + print "Fai also supports tla commands." + +def command_help(cmd): + """ + Prints help for a command. + + :param cmd: The name of the command to print help for + :type cmd: str + """ + fake_aba = abacmds.AbaCmds() + cmdobj = find_command(cmd) + if cmdobj != None: + cmdobj.help() + elif suggestions.has_key(cmd): + print "Not available\n" + suggestions[cmd] + else: + abacmd = fake_aba.is_command(cmd) + if abacmd: + abacmd.help() + else: + print "No help is available for \""+cmd+"\". Maybe try \"tla "+cmd+" -H\"?" + + + +class Changes(BaseCommand): + """ + the "changes" command: lists differences between trees/revisions: + """ + + def __init__(self): + self.description="Lists what files have changed in the project tree" + + def get_completer(self, arg, index): + if index > 1: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def parse_commandline(self, cmdline): + """ + Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. + + :param cmdline: A list of arguments to parse + :rtype: (options, Revision, Revision/WorkingTree) + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdline) + if len(args) > 2: + raise cmdutil.GetHelp + + tree=arch.tree_root() + if len(args) == 0: + a_spec = ancillary.comp_revision(tree) + else: + a_spec = cmdutil.determine_revision_tree(tree, args[0]) + cmdutil.ensure_archive_registered(a_spec.archive) + if len(args) == 2: + b_spec = cmdutil.determine_revision_tree(tree, args[1]) + cmdutil.ensure_archive_registered(b_spec.archive) + else: + b_spec=tree + return options, a_spec, b_spec + + def do_command(self, cmdargs): + """ + Master function that perfoms the "changes" command. + """ + try: + options, a_spec, b_spec = self.parse_commandline(cmdargs); + except cmdutil.CantDetermineRevision, e: + print e + return + except arch.errors.TreeRootError, e: + print e + return + if options.changeset: + changeset=options.changeset + tmpdir = None + else: + tmpdir=util.tmpdir() + changeset=tmpdir+"/changeset" + try: + delta=arch.iter_delta(a_spec, b_spec, changeset) + try: + for line in delta: + if cmdutil.chattermatch(line, "changeset:"): + pass + else: + cmdutil.colorize(line, options.suppress_chatter) + except arch.util.ExecProblem, e: + if e.proc.error and e.proc.error.startswith( + "missing explicit id for file"): + raise MissingID(e) + else: + raise + status=delta.status + if status > 1: + return + if (options.perform_diff): + chan = arch_compound.ChangesetMunger(changeset) + chan.read_indices() + if options.diffopts is not None: + if isinstance(b_spec, arch.Revision): + b_dir = b_spec.library_find() + else: + b_dir = b_spec + a_dir = a_spec.library_find() + diffopts = options.diffopts.split() + cmdutil.show_custom_diffs(chan, diffopts, a_dir, b_dir) + else: + cmdutil.show_diffs(delta.changeset) + finally: + if tmpdir and (os.access(tmpdir, os.X_OK)): + shutil.rmtree(tmpdir) + + def get_parser(self): + """ + Returns the options parser to use for the "changes" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai changes [options] [revision]" + " [revision]") + parser.add_option("-d", "--diff", action="store_true", + dest="perform_diff", default=False, + help="Show diffs in summary") + parser.add_option("-c", "--changeset", dest="changeset", + help="Store a changeset in the given directory", + metavar="DIRECTORY") + parser.add_option("-s", "--silent", action="store_true", + dest="suppress_chatter", default=False, + help="Suppress chatter messages") + parser.add_option("--diffopts", dest="diffopts", + help="Use the specified diff options", + metavar="OPTIONS") + + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser is None: + parser=self.get_parser() + parser.print_help() + print """ +Performs source-tree comparisons + +If no revision is specified, the current project tree is compared to the +last-committed revision. If one revision is specified, the current project +tree is compared to that revision. If two revisions are specified, they are +compared to each other. + """ + help_tree_spec() + return + + +class ApplyChanges(BaseCommand): + """ + Apply differences between two revisions to a tree + """ + + def __init__(self): + self.description="Applies changes to a project tree" + + def get_completer(self, arg, index): + if index > 1: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def parse_commandline(self, cmdline, tree): + """ + Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. + + :param cmdline: A list of arguments to parse + :rtype: (options, Revision, Revision/WorkingTree) + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdline) + if len(args) != 2: + raise cmdutil.GetHelp + + a_spec = cmdutil.determine_revision_tree(tree, args[0]) + cmdutil.ensure_archive_registered(a_spec.archive) + b_spec = cmdutil.determine_revision_tree(tree, args[1]) + cmdutil.ensure_archive_registered(b_spec.archive) + return options, a_spec, b_spec + + def do_command(self, cmdargs): + """ + Master function that performs "apply-changes". + """ + try: + tree = arch.tree_root() + options, a_spec, b_spec = self.parse_commandline(cmdargs, tree); + except cmdutil.CantDetermineRevision, e: + print e + return + except arch.errors.TreeRootError, e: + print e + return + delta=cmdutil.apply_delta(a_spec, b_spec, tree) + for line in cmdutil.iter_apply_delta_filter(delta): + cmdutil.colorize(line, options.suppress_chatter) + + def get_parser(self): + """ + Returns the options parser to use for the "apply-changes" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai apply-changes [options] revision" + " revision") + parser.add_option("-d", "--diff", action="store_true", + dest="perform_diff", default=False, + help="Show diffs in summary") + parser.add_option("-c", "--changeset", dest="changeset", + help="Store a changeset in the given directory", + metavar="DIRECTORY") + parser.add_option("-s", "--silent", action="store_true", + dest="suppress_chatter", default=False, + help="Suppress chatter messages") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser is None: + parser=self.get_parser() + parser.print_help() + print """ +Applies changes to a project tree + +Compares two revisions and applies the difference between them to the current +tree. + """ + help_tree_spec() + return + +class Update(BaseCommand): + """ + Updates a project tree to a given revision, preserving un-committed hanges. + """ + + def __init__(self): + self.description="Apply the latest changes to the current directory" + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def parse_commandline(self, cmdline, tree): + """ + Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. + + :param cmdline: A list of arguments to parse + :rtype: (options, Revision, Revision/WorkingTree) + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdline) + if len(args) > 2: + raise cmdutil.GetHelp + + spec=None + if len(args)>0: + spec=args[0] + revision=cmdutil.determine_revision_arch(tree, spec) + cmdutil.ensure_archive_registered(revision.archive) + + mirror_source = cmdutil.get_mirror_source(revision.archive) + if mirror_source != None: + if cmdutil.prompt("Mirror update"): + cmd=cmdutil.mirror_archive(mirror_source, + revision.archive, arch.NameParser(revision).get_package_version()) + for line in arch.chatter_classifier(cmd): + cmdutil.colorize(line, options.suppress_chatter) + + revision=cmdutil.determine_revision_arch(tree, spec) + + return options, revision + + def do_command(self, cmdargs): + """ + Master function that perfoms the "update" command. + """ + tree=arch.tree_root() + try: + options, to_revision = self.parse_commandline(cmdargs, tree); + except cmdutil.CantDetermineRevision, e: + print e + return + except arch.errors.TreeRootError, e: + print e + return + from_revision = arch_compound.tree_latest(tree) + if from_revision==to_revision: + print "Tree is already up to date with:\n"+str(to_revision)+"." + return + cmdutil.ensure_archive_registered(from_revision.archive) + cmd=cmdutil.apply_delta(from_revision, to_revision, tree, + options.patch_forward) + for line in cmdutil.iter_apply_delta_filter(cmd): + cmdutil.colorize(line) + if to_revision.version != tree.tree_version: + if cmdutil.prompt("Update version"): + tree.tree_version = to_revision.version + + def get_parser(self): + """ + Returns the options parser to use for the "update" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai update [options]" + " [revision/version]") + parser.add_option("-f", "--forward", action="store_true", + dest="patch_forward", default=False, + help="pass the --forward option to 'patch'") + parser.add_option("-s", "--silent", action="store_true", + dest="suppress_chatter", default=False, + help="Suppress chatter messages") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser is None: + parser=self.get_parser() + parser.print_help() + print """ +Updates a working tree to the current archive revision + +If a revision or version is specified, that is used instead + """ + help_tree_spec() + return + + +class Commit(BaseCommand): + """ + Create a revision based on the changes in the current tree. + """ + + def __init__(self): + self.description="Write local changes to the archive" + + def get_completer(self, arg, index): + if arg is None: + arg = "" + return iter_modified_file_completions(arch.tree_root(), arg) +# return iter_source_file_completions(arch.tree_root(), arg) + + def parse_commandline(self, cmdline, tree): + """ + Parse commandline arguments. Raise cmtutil.GetHelp if help is needed. + + :param cmdline: A list of arguments to parse + :rtype: (options, Revision, Revision/WorkingTree) + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdline) + + if len(args) == 0: + args = None + if options.version is None: + return options, tree.tree_version, args + + revision=cmdutil.determine_revision_arch(tree, options.version) + return options, revision.get_version(), args + + def do_command(self, cmdargs): + """ + Master function that perfoms the "commit" command. + """ + tree=arch.tree_root() + options, version, files = self.parse_commandline(cmdargs, tree) + ancestor = None + if options.__dict__.has_key("base") and options.base: + base = cmdutil.determine_revision_tree(tree, options.base) + ancestor = base + else: + base = ancillary.submit_revision(tree) + ancestor = base + if ancestor is None: + ancestor = arch_compound.tree_latest(tree, version) + + writeversion=version + archive=version.archive + source=cmdutil.get_mirror_source(archive) + allow_old=False + writethrough="implicit" + + if source!=None: + if writethrough=="explicit" and \ + cmdutil.prompt("Writethrough"): + writeversion=arch.Version(str(source)+"/"+str(version.get_nonarch())) + elif writethrough=="none": + raise CommitToMirror(archive) + + elif archive.is_mirror: + raise CommitToMirror(archive) + + try: + last_revision=tree.iter_logs(version, True).next().revision + except StopIteration, e: + last_revision = None + if ancestor is None: + if cmdutil.prompt("Import from commit"): + return do_import(version) + else: + raise NoVersionLogs(version) + try: + arch_last_revision = version.iter_revisions(True).next() + except StopIteration, e: + arch_last_revision = None + + if last_revision != arch_last_revision: + print "Tree is not up to date with %s" % str(version) + if not cmdutil.prompt("Out of date"): + raise OutOfDate + else: + allow_old=True + + try: + if not cmdutil.has_changed(ancestor): + if not cmdutil.prompt("Empty commit"): + raise EmptyCommit + except arch.util.ExecProblem, e: + if e.proc.error and e.proc.error.startswith( + "missing explicit id for file"): + raise MissingID(e) + else: + raise + log = tree.log_message(create=False, version=version) + if log is None: + try: + if cmdutil.prompt("Create log"): + edit_log(tree, version) + + except cmdutil.NoEditorSpecified, e: + raise CommandFailed(e) + log = tree.log_message(create=False, version=version) + if log is None: + raise NoLogMessage + if log["Summary"] is None or len(log["Summary"].strip()) == 0: + if not cmdutil.prompt("Omit log summary"): + raise errors.NoLogSummary + try: + for line in tree.iter_commit(version, seal=options.seal_version, + base=base, out_of_date_ok=allow_old, file_list=files): + cmdutil.colorize(line, options.suppress_chatter) + + except arch.util.ExecProblem, e: + if e.proc.error and e.proc.error.startswith( + "These files violate naming conventions:"): + raise LintFailure(e.proc.error) + else: + raise + + def get_parser(self): + """ + Returns the options parser to use for the "commit" command. + + :rtype: cmdutil.CmdOptionParser + """ + + parser=cmdutil.CmdOptionParser("fai commit [options] [file1]" + " [file2...]") + parser.add_option("--seal", action="store_true", + dest="seal_version", default=False, + help="seal this version") + parser.add_option("-v", "--version", dest="version", + help="Use the specified version", + metavar="VERSION") + parser.add_option("-s", "--silent", action="store_true", + dest="suppress_chatter", default=False, + help="Suppress chatter messages") + if cmdutil.supports_switch("commit", "--base"): + parser.add_option("--base", dest="base", help="", + metavar="REVISION") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser is None: + parser=self.get_parser() + parser.print_help() + print """ +Updates a working tree to the current archive revision + +If a version is specified, that is used instead + """ +# help_tree_spec() + return + + + +class CatLog(BaseCommand): + """ + Print the log of a given file (from current tree) + """ + def __init__(self): + self.description="Prints the patch log for a revision" + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "cat-log" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: + tree = arch.tree_root() + except arch.errors.TreeRootError, e: + tree = None + spec=None + if len(args) > 0: + spec=args[0] + if len(args) > 1: + raise cmdutil.GetHelp() + try: + if tree: + revision = cmdutil.determine_revision_tree(tree, spec) + else: + revision = cmdutil.determine_revision_arch(tree, spec) + except cmdutil.CantDetermineRevision, e: + raise CommandFailedWrapper(e) + log = None + + use_tree = (options.source == "tree" or \ + (options.source == "any" and tree)) + use_arch = (options.source == "archive" or options.source == "any") + + log = None + if use_tree: + for log in tree.iter_logs(revision.get_version()): + if log.revision == revision: + break + else: + log = None + if log is None and use_arch: + cmdutil.ensure_revision_exists(revision) + log = arch.Patchlog(revision) + if log is not None: + for item in log.items(): + print "%s: %s" % item + print log.description + + def get_parser(self): + """ + Returns the options parser to use for the "cat-log" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai cat-log [revision]") + parser.add_option("--archive", action="store_const", dest="source", + const="archive", default="any", + help="Always get the log from the archive") + parser.add_option("--tree", action="store_const", dest="source", + const="tree", help="Always get the log from the tree") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Prints the log for the specified revision + """ + help_tree_spec() + return + +class Revert(BaseCommand): + """ Reverts a tree (or aspects of it) to a revision + """ + def __init__(self): + self.description="Reverts a tree (or aspects of it) to a revision " + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return iter_modified_file_completions(tree, arg) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revert" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: + tree = arch.tree_root() + except arch.errors.TreeRootError, e: + raise CommandFailed(e) + spec=None + if options.revision is not None: + spec=options.revision + try: + if spec is not None: + revision = cmdutil.determine_revision_tree(tree, spec) + else: + revision = ancillary.comp_revision(tree) + except cmdutil.CantDetermineRevision, e: + raise CommandFailedWrapper(e) + munger = None + + if options.file_contents or options.file_perms or options.deletions\ + or options.additions or options.renames or options.hunk_prompt: + munger = arch_compound.MungeOpts() + munger.set_hunk_prompt(cmdutil.colorize, cmdutil.user_hunk_confirm, + options.hunk_prompt) + + if len(args) > 0 or options.logs or options.pattern_files or \ + options.control: + if munger is None: + munger = cmdutil.arch_compound.MungeOpts(True) + munger.all_types(True) + if len(args) > 0: + t_cwd = arch_compound.tree_cwd(tree) + for name in args: + if len(t_cwd) > 0: + t_cwd += "/" + name = "./" + t_cwd + name + munger.add_keep_file(name); + + if options.file_perms: + munger.file_perms = True + if options.file_contents: + munger.file_contents = True + if options.deletions: + munger.deletions = True + if options.additions: + munger.additions = True + if options.renames: + munger.renames = True + if options.logs: + munger.add_keep_pattern('^\./\{arch\}/[^=].*') + if options.control: + munger.add_keep_pattern("/\.arch-ids|^\./\{arch\}|"\ + "/\.arch-inventory$") + if options.pattern_files: + munger.add_keep_pattern(options.pattern_files) + + for line in arch_compound.revert(tree, revision, munger, + not options.no_output): + cmdutil.colorize(line) + + + def get_parser(self): + """ + Returns the options parser to use for the "cat-log" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai revert [options] [FILE...]") + parser.add_option("", "--contents", action="store_true", + dest="file_contents", + help="Revert file content changes") + parser.add_option("", "--permissions", action="store_true", + dest="file_perms", + help="Revert file permissions changes") + parser.add_option("", "--deletions", action="store_true", + dest="deletions", + help="Restore deleted files") + parser.add_option("", "--additions", action="store_true", + dest="additions", + help="Remove added files") + parser.add_option("", "--renames", action="store_true", + dest="renames", + help="Revert file names") + parser.add_option("--hunks", action="store_true", + dest="hunk_prompt", default=False, + help="Prompt which hunks to revert") + parser.add_option("--pattern-files", dest="pattern_files", + help="Revert files that match this pattern", + metavar="REGEX") + parser.add_option("--logs", action="store_true", + dest="logs", default=False, + help="Revert only logs") + parser.add_option("--control-files", action="store_true", + dest="control", default=False, + help="Revert logs and other control files") + parser.add_option("-n", "--no-output", action="store_true", + dest="no_output", + help="Don't keep an undo changeset") + parser.add_option("--revision", dest="revision", + help="Revert to the specified revision", + metavar="REVISION") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Reverts changes in the current working tree. If no flags are specified, all +types of changes are reverted. Otherwise, only selected types of changes are +reverted. + +If a revision is specified on the commandline, differences between the current +tree and that revision are reverted. If a version is specified, the current +tree is used to determine the revision. + +If files are specified, only those files listed will have any changes applied. +To specify a renamed file, you can use either the old or new name. (or both!) + +Unless "-n" is specified, reversions can be undone with "redo". + """ + return + +class Revision(BaseCommand): + """ + Print a revision name based on a revision specifier + """ + def __init__(self): + self.description="Prints the name of a revision" + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + tree = None + + spec=None + if len(args) > 0: + spec=args[0] + if len(args) > 1: + raise cmdutil.GetHelp + try: + if tree: + revision = cmdutil.determine_revision_tree(tree, spec) + else: + revision = cmdutil.determine_revision_arch(tree, spec) + except cmdutil.CantDetermineRevision, e: + print str(e) + return + print options.display(revision) + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai revision [revision]") + parser.add_option("", "--location", action="store_const", + const=paths.determine_path, dest="display", + help="Show location instead of name", default=str) + parser.add_option("--import", action="store_const", + const=paths.determine_import_path, dest="display", + help="Show location of import file") + parser.add_option("--log", action="store_const", + const=paths.determine_log_path, dest="display", + help="Show location of log file") + parser.add_option("--patch", action="store_const", + dest="display", const=paths.determine_patch_path, + help="Show location of patchfile") + parser.add_option("--continuation", action="store_const", + const=paths.determine_continuation_path, + dest="display", + help="Show location of continuation file") + parser.add_option("--cacherev", action="store_const", + const=paths.determine_cacherev_path, dest="display", + help="Show location of cacherev file") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Expands aliases and prints the name of the specified revision. Instead of +the name, several options can be used to print locations. If more than one is +specified, the last one is used. + """ + help_tree_spec() + return + +class Revisions(BaseCommand): + """ + Print a revision name based on a revision specifier + """ + def __init__(self): + self.description="Lists revisions" + self.cl_revisions = [] + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + (options, args) = self.get_parser().parse_args(cmdargs) + if len(args) > 1: + raise cmdutil.GetHelp + try: + self.tree = arch.tree_root() + except arch.errors.TreeRootError: + self.tree = None + if options.type == "default": + options.type = "archive" + try: + iter = cmdutil.revision_iterator(self.tree, options.type, args, + options.reverse, options.modified, + options.shallow) + except cmdutil.CantDetermineRevision, e: + raise CommandFailedWrapper(e) + except cmdutil.CantDetermineVersion, e: + raise CommandFailedWrapper(e) + if options.skip is not None: + iter = cmdutil.iter_skip(iter, int(options.skip)) + + try: + for revision in iter: + log = None + if isinstance(revision, arch.Patchlog): + log = revision + revision=revision.revision + out = options.display(revision) + if out is not None: + print out + if log is None and (options.summary or options.creator or + options.date or options.merges): + log = revision.patchlog + if options.creator: + print " %s" % log.creator + if options.date: + print " %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date) + if options.summary: + print " %s" % log.summary + if options.merges: + showed_title = False + for revision in log.merged_patches: + if not showed_title: + print " Merged:" + showed_title = True + print " %s" % revision + if len(self.cl_revisions) > 0: + print pylon.changelog_for_merge(self.cl_revisions) + except pylon.errors.TreeRootNone: + raise CommandFailedWrapper( + Exception("This option can only be used in a project tree.")) + + def changelog_append(self, revision): + if isinstance(revision, arch.Revision): + revision=arch.Patchlog(revision) + self.cl_revisions.append(revision) + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai revisions [version/revision]") + select = cmdutil.OptionGroup(parser, "Selection options", + "Control which revisions are listed. These options" + " are mutually exclusive. If more than one is" + " specified, the last is used.") + + cmdutil.add_revision_iter_options(select) + parser.add_option("", "--skip", dest="skip", + help="Skip revisions. Positive numbers skip from " + "beginning, negative skip from end.", + metavar="NUMBER") + + parser.add_option_group(select) + + format = cmdutil.OptionGroup(parser, "Revision format options", + "These control the appearance of listed revisions") + format.add_option("", "--location", action="store_const", + const=paths.determine_path, dest="display", + help="Show location instead of name", default=str) + format.add_option("--import", action="store_const", + const=paths.determine_import_path, dest="display", + help="Show location of import file") + format.add_option("--log", action="store_const", + const=paths.determine_log_path, dest="display", + help="Show location of log file") + format.add_option("--patch", action="store_const", + dest="display", const=paths.determine_patch_path, + help="Show location of patchfile") + format.add_option("--continuation", action="store_const", + const=paths.determine_continuation_path, + dest="display", + help="Show location of continuation file") + format.add_option("--cacherev", action="store_const", + const=paths.determine_cacherev_path, dest="display", + help="Show location of cacherev file") + format.add_option("--changelog", action="store_const", + const=self.changelog_append, dest="display", + help="Show location of cacherev file") + parser.add_option_group(format) + display = cmdutil.OptionGroup(parser, "Display format options", + "These control the display of data") + display.add_option("-r", "--reverse", action="store_true", + dest="reverse", help="Sort from newest to oldest") + display.add_option("-s", "--summary", action="store_true", + dest="summary", help="Show patchlog summary") + display.add_option("-D", "--date", action="store_true", + dest="date", help="Show patchlog date") + display.add_option("-c", "--creator", action="store_true", + dest="creator", help="Show the id that committed the" + " revision") + display.add_option("-m", "--merges", action="store_true", + dest="merges", help="Show the revisions that were" + " merged") + parser.add_option_group(display) + return parser + def help(self, parser=None): + """Attempt to explain the revisions command + + :param parser: If supplied, used to determine options + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """List revisions. + """ + help_tree_spec() + + +class Get(BaseCommand): + """ + Retrieve a revision from the archive + """ + def __init__(self): + self.description="Retrieve a revision from the archive" + self.parser=self.get_parser() + + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + + def do_command(self, cmdargs): + """ + Master function that perfoms the "get" command. + """ + (options, args) = self.parser.parse_args(cmdargs) + if len(args) < 1: + return self.help() + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + tree = None + + arch_loc = None + try: + revision, arch_loc = paths.full_path_decode(args[0]) + except Exception, e: + revision = cmdutil.determine_revision_arch(tree, args[0], + check_existence=False, allow_package=True) + if len(args) > 1: + directory = args[1] + else: + directory = str(revision.nonarch) + if os.path.exists(directory): + raise DirectoryExists(directory) + cmdutil.ensure_archive_registered(revision.archive, arch_loc) + try: + cmdutil.ensure_revision_exists(revision) + except cmdutil.NoSuchRevision, e: + raise CommandFailedWrapper(e) + + link = cmdutil.prompt ("get link") + for line in cmdutil.iter_get(revision, directory, link, + options.no_pristine, + options.no_greedy_add): + cmdutil.colorize(line) + + def get_parser(self): + """ + Returns the options parser to use for the "get" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai get revision [dir]") + parser.add_option("--no-pristine", action="store_true", + dest="no_pristine", + help="Do not make pristine copy for reference") + parser.add_option("--no-greedy-add", action="store_true", + dest="no_greedy_add", + help="Never add to greedy libraries") + + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Expands aliases and constructs a project tree for a revision. If the optional +"dir" argument is provided, the project tree will be stored in this directory. + """ + help_tree_spec() + return + +class PromptCmd(cmd.Cmd): + def __init__(self): + cmd.Cmd.__init__(self) + self.prompt = "Fai> " + try: + self.tree = arch.tree_root() + except: + self.tree = None + self.set_title() + self.set_prompt() + self.fake_aba = abacmds.AbaCmds() + self.identchars += '-' + self.history_file = os.path.expanduser("~/.fai-history") + readline.set_completer_delims(string.whitespace) + if os.access(self.history_file, os.R_OK) and \ + os.path.isfile(self.history_file): + readline.read_history_file(self.history_file) + self.cwd = os.getcwd() + + def write_history(self): + readline.write_history_file(self.history_file) + + def do_quit(self, args): + self.write_history() + sys.exit(0) + + def do_exit(self, args): + self.do_quit(args) + + def do_EOF(self, args): + print + self.do_quit(args) + + def postcmd(self, line, bar): + self.set_title() + self.set_prompt() + + def set_prompt(self): + if self.tree is not None: + try: + prompt = pylon.alias_or_version(self.tree.tree_version, + self.tree, + full=False) + if prompt is not None: + prompt = " " + prompt + except: + prompt = "" + else: + prompt = "" + self.prompt = "Fai%s> " % prompt + + def set_title(self, command=None): + try: + version = pylon.alias_or_version(self.tree.tree_version, self.tree, + full=False) + except: + version = "[no version]" + if command is None: + command = "" + sys.stdout.write(terminal.term_title("Fai %s %s" % (command, version))) + + def do_cd(self, line): + if line == "": + line = "~" + line = os.path.expanduser(line) + if os.path.isabs(line): + newcwd = line + else: + newcwd = self.cwd+'/'+line + newcwd = os.path.normpath(newcwd) + try: + os.chdir(newcwd) + self.cwd = newcwd + except Exception, e: + print e + try: + self.tree = arch.tree_root() + except: + self.tree = None + + def do_help(self, line): + Help()(line) + + def default(self, line): + args = line.split() + if find_command(args[0]): + try: + find_command(args[0]).do_command(args[1:]) + except cmdutil.BadCommandOption, e: + print e + except cmdutil.GetHelp, e: + find_command(args[0]).help() + except CommandFailed, e: + print e + except arch.errors.ArchiveNotRegistered, e: + print e + except KeyboardInterrupt, e: + print "Interrupted" + except arch.util.ExecProblem, e: + print e.proc.error.rstrip('\n') + except cmdutil.CantDetermineVersion, e: + print e + except cmdutil.CantDetermineRevision, e: + print e + except Exception, e: + print "Unhandled error:\n%s" % errors.exception_str(e) + + elif suggestions.has_key(args[0]): + print suggestions[args[0]] + + elif self.fake_aba.is_command(args[0]): + tree = None + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + pass + cmd = self.fake_aba.is_command(args[0]) + try: + cmd.run(cmdutil.expand_prefix_alias(args[1:], tree)) + except KeyboardInterrupt, e: + print "Interrupted" + + elif options.tla_fallthrough and args[0] != "rm" and \ + cmdutil.is_tla_command(args[0]): + try: + tree = None + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + pass + args = cmdutil.expand_prefix_alias(args, tree) + arch.util.exec_safe('tla', args, stderr=sys.stderr, + expected=(0, 1)) + except arch.util.ExecProblem, e: + pass + except KeyboardInterrupt, e: + print "Interrupted" + else: + try: + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + tree = None + args=line.split() + os.system(" ".join(cmdutil.expand_prefix_alias(args, tree))) + except KeyboardInterrupt, e: + print "Interrupted" + + def completenames(self, text, line, begidx, endidx): + completions = [] + iter = iter_command_names(self.fake_aba) + try: + if len(line) > 0: + arg = line.split()[-1] + else: + arg = "" + iter = cmdutil.iter_munged_completions(iter, arg, text) + except Exception, e: + print e + return list(iter) + + def completedefault(self, text, line, begidx, endidx): + """Perform completion for native commands. + + :param text: The text to complete + :type text: str + :param line: The entire line to complete + :type line: str + :param begidx: The start of the text in the line + :type begidx: int + :param endidx: The end of the text in the line + :type endidx: int + """ + try: + (cmd, args, foo) = self.parseline(line) + command_obj=find_command(cmd) + if command_obj is not None: + return command_obj.complete(args.split(), text) + elif not self.fake_aba.is_command(cmd) and \ + cmdutil.is_tla_command(cmd): + iter = cmdutil.iter_supported_switches(cmd) + if len(args) > 0: + arg = args.split()[-1] + else: + arg = "" + if arg.startswith("-"): + return list(cmdutil.iter_munged_completions(iter, arg, + text)) + else: + return list(cmdutil.iter_munged_completions( + cmdutil.iter_file_completions(arg), arg, text)) + + + elif cmd == "cd": + if len(args) > 0: + arg = args.split()[-1] + else: + arg = "" + iter = cmdutil.iter_dir_completions(arg) + iter = cmdutil.iter_munged_completions(iter, arg, text) + return list(iter) + elif len(args)>0: + arg = args.split()[-1] + iter = cmdutil.iter_file_completions(arg) + return list(cmdutil.iter_munged_completions(iter, arg, text)) + else: + return self.completenames(text, line, begidx, endidx) + except Exception, e: + print e + + +def iter_command_names(fake_aba): + for entry in cmdutil.iter_combine([commands.iterkeys(), + fake_aba.get_commands(), + cmdutil.iter_tla_commands(False)]): + if not suggestions.has_key(str(entry)): + yield entry + + +def iter_source_file_completions(tree, arg): + treepath = arch_compound.tree_cwd(tree) + if len(treepath) > 0: + dirs = [treepath] + else: + dirs = None + for file in tree.iter_inventory(dirs, source=True, both=True): + file = file_completion_match(file, treepath, arg) + if file is not None: + yield file + + +def iter_untagged(tree, dirs): + for file in arch_core.iter_inventory_filter(tree, dirs, tagged=False, + categories=arch_core.non_root, + control_files=True): + yield file.name + + +def iter_untagged_completions(tree, arg): + """Generate an iterator for all visible untagged files that match arg. + + :param tree: The tree to look for untagged files in + :type tree: `arch.WorkingTree` + :param arg: The argument to match + :type arg: str + :return: An iterator of all matching untagged files + :rtype: iterator of str + """ + treepath = arch_compound.tree_cwd(tree) + if len(treepath) > 0: + dirs = [treepath] + else: + dirs = None + + for file in iter_untagged(tree, dirs): + file = file_completion_match(file, treepath, arg) + if file is not None: + yield file + + +def file_completion_match(file, treepath, arg): + """Determines whether a file within an arch tree matches the argument. + + :param file: The rooted filename + :type file: str + :param treepath: The path to the cwd within the tree + :type treepath: str + :param arg: The prefix to match + :return: The completion name, or None if not a match + :rtype: str + """ + if not file.startswith(treepath): + return None + if treepath != "": + file = file[len(treepath)+1:] + + if not file.startswith(arg): + return None + if os.path.isdir(file): + file += '/' + return file + +def iter_modified_file_completions(tree, arg): + """Returns a list of modified files that match the specified prefix. + + :param tree: The current tree + :type tree: `arch.WorkingTree` + :param arg: The prefix to match + :type arg: str + """ + treepath = arch_compound.tree_cwd(tree) + tmpdir = util.tmpdir() + changeset = tmpdir+"/changeset" + completions = [] + revision = cmdutil.determine_revision_tree(tree) + for line in arch.iter_delta(revision, tree, changeset): + if isinstance(line, arch.FileModification): + file = file_completion_match(line.name[1:], treepath, arg) + if file is not None: + completions.append(file) + shutil.rmtree(tmpdir) + return completions + +class Shell(BaseCommand): + def __init__(self): + self.description = "Runs Fai as a shell" + + def do_command(self, cmdargs): + if len(cmdargs)!=0: + raise cmdutil.GetHelp + prompt = PromptCmd() + try: + prompt.cmdloop() + finally: + prompt.write_history() + +class AddID(BaseCommand): + """ + Adds an inventory id for the given file + """ + def __init__(self): + self.description="Add an inventory id for a given file" + + def get_completer(self, arg, index): + tree = arch.tree_root() + return iter_untagged_completions(tree, arg) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + + try: + tree = arch.tree_root() + except arch.errors.TreeRootError, e: + raise pylon.errors.CommandFailedWrapper(e) + + + if (len(args) == 0) == (options.untagged == False): + raise cmdutil.GetHelp + + #if options.id and len(args) != 1: + # print "If --id is specified, only one file can be named." + # return + + method = tree.tagging_method + + if options.id_type == "tagline": + if method != "tagline": + if not cmdutil.prompt("Tagline in other tree"): + if method == "explicit" or method == "implicit": + options.id_type == method + else: + print "add-id not supported for \"%s\" tagging method"\ + % method + return + + elif options.id_type == "implicit": + if method != "implicit": + if not cmdutil.prompt("Implicit in other tree"): + if method == "explicit" or method == "tagline": + options.id_type == method + else: + print "add-id not supported for \"%s\" tagging method"\ + % method + return + elif options.id_type == "explicit": + if method != "tagline" and method != explicit: + if not prompt("Explicit in other tree"): + print "add-id not supported for \"%s\" tagging method" % \ + method + return + + if options.id_type == "auto": + if method != "tagline" and method != "explicit" \ + and method !="implicit": + print "add-id not supported for \"%s\" tagging method" % method + return + else: + options.id_type = method + if options.untagged: + args = None + self.add_ids(tree, options.id_type, args) + + def add_ids(self, tree, id_type, files=()): + """Add inventory ids to files. + + :param tree: the tree the files are in + :type tree: `arch.WorkingTree` + :param id_type: the type of id to add: "explicit" or "tagline" + :type id_type: str + :param files: The list of files to add. If None do all untagged. + :type files: tuple of str + """ + + untagged = (files is None) + if untagged: + files = list(iter_untagged(tree, None)) + previous_files = [] + while len(files) > 0: + previous_files.extend(files) + if id_type == "explicit": + cmdutil.add_id(files) + elif id_type == "tagline" or id_type == "implicit": + for file in files: + try: + implicit = (id_type == "implicit") + cmdutil.add_tagline_or_explicit_id(file, False, + implicit) + except cmdutil.AlreadyTagged: + print "\"%s\" already has a tagline." % file + except cmdutil.NoCommentSyntax: + pass + #do inventory after tagging until no untagged files are encountered + if untagged: + files = [] + for file in iter_untagged(tree, None): + if not file in previous_files: + files.append(file) + + else: + break + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai add-id file1 [file2] [file3]...") +# ddaa suggests removing this to promote GUIDs. Let's see who squalks. +# parser.add_option("-i", "--id", dest="id", +# help="Specify id for a single file", default=None) + parser.add_option("--tltl", action="store_true", + dest="lord_style", help="Use Tom Lord's style of id.") + parser.add_option("--explicit", action="store_const", + const="explicit", dest="id_type", + help="Use an explicit id", default="auto") + parser.add_option("--tagline", action="store_const", + const="tagline", dest="id_type", + help="Use a tagline id") + parser.add_option("--implicit", action="store_const", + const="implicit", dest="id_type", + help="Use an implicit id (deprecated)") + parser.add_option("--untagged", action="store_true", + dest="untagged", default=False, + help="tag all untagged files") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Adds an inventory to the specified file(s) and directories. If --untagged is +specified, adds inventory to all untagged files and directories. + """ + return + + +class Merge(BaseCommand): + """ + Merges changes from other versions into the current tree + """ + def __init__(self): + self.description="Merges changes from other versions" + try: + self.tree = arch.tree_root() + except: + self.tree = None + + + def get_completer(self, arg, index): + if self.tree is None: + raise arch.errors.TreeRootError + return cmdutil.merge_completions(self.tree, arg, index) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "merge" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + if options.diff3: + action="star-merge" + else: + action = options.action + + if self.tree is None: + raise arch.errors.TreeRootError(os.getcwd()) + if cmdutil.has_changed(ancillary.comp_revision(self.tree)): + raise UncommittedChanges(self.tree) + + if len(args) > 0: + revisions = [] + for arg in args: + revisions.append(cmdutil.determine_revision_arch(self.tree, + arg)) + source = "from commandline" + else: + revisions = ancillary.iter_partner_revisions(self.tree, + self.tree.tree_version) + source = "from partner version" + revisions = misc.rewind_iterator(revisions) + try: + revisions.next() + revisions.rewind() + except StopIteration, e: + revision = cmdutil.tag_cur(self.tree) + if revision is None: + raise CantDetermineRevision("", "No version specified, no " + "partner-versions, and no tag" + " source") + revisions = [revision] + source = "from tag source" + for revision in revisions: + cmdutil.ensure_archive_registered(revision.archive) + cmdutil.colorize(arch.Chatter("* Merging %s [%s]" % + (revision, source))) + if action=="native-merge" or action=="update": + if self.native_merge(revision, action) == 0: + continue + elif action=="star-merge": + try: + self.star_merge(revision, options.diff3) + except errors.MergeProblem, e: + break + if cmdutil.has_changed(self.tree.tree_version): + break + + def star_merge(self, revision, diff3): + """Perform a star-merge on the current tree. + + :param revision: The revision to use for the merge + :type revision: `arch.Revision` + :param diff3: If true, do a diff3 merge + :type diff3: bool + """ + try: + for line in self.tree.iter_star_merge(revision, diff3=diff3): + cmdutil.colorize(line) + except arch.util.ExecProblem, e: + if e.proc.status is not None and e.proc.status == 1: + if e.proc.error: + print e.proc.error + raise MergeProblem + else: + raise + + def native_merge(self, other_revision, action): + """Perform a native-merge on the current tree. + + :param other_revision: The revision to use for the merge + :type other_revision: `arch.Revision` + :return: 0 if the merge was skipped, 1 if it was applied + """ + other_tree = arch_compound.find_or_make_local_revision(other_revision) + try: + if action == "native-merge": + ancestor = arch_compound.merge_ancestor2(self.tree, other_tree, + other_revision) + elif action == "update": + ancestor = arch_compound.tree_latest(self.tree, + other_revision.version) + except CantDetermineRevision, e: + raise CommandFailedWrapper(e) + cmdutil.colorize(arch.Chatter("* Found common ancestor %s" % ancestor)) + if (ancestor == other_revision): + cmdutil.colorize(arch.Chatter("* Skipping redundant merge" + % ancestor)) + return 0 + delta = cmdutil.apply_delta(ancestor, other_tree, self.tree) + for line in cmdutil.iter_apply_delta_filter(delta): + cmdutil.colorize(line) + return 1 + + + + def get_parser(self): + """ + Returns the options parser to use for the "merge" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai merge [VERSION]") + parser.add_option("-s", "--star-merge", action="store_const", + dest="action", help="Use star-merge", + const="star-merge", default="native-merge") + parser.add_option("--update", action="store_const", + dest="action", help="Use update picker", + const="update") + parser.add_option("--diff3", action="store_true", + dest="diff3", + help="Use diff3 for merge (implies star-merge)") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Performs a merge operation using the specified version. + """ + return + +class ELog(BaseCommand): + """ + Produces a raw patchlog and invokes the user's editor + """ + def __init__(self): + self.description="Edit a patchlog to commit" + try: + self.tree = arch.tree_root() + except: + self.tree = None + + + def do_command(self, cmdargs): + """ + Master function that perfoms the "elog" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + if self.tree is None: + raise arch.errors.TreeRootError + + try: + edit_log(self.tree, self.tree.tree_version) + except pylon.errors.NoEditorSpecified, e: + raise pylon.errors.CommandFailedWrapper(e) + + def get_parser(self): + """ + Returns the options parser to use for the "merge" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai elog") + return parser + + + def help(self, parser=None): + """ + Invokes $EDITOR to produce a log for committing. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Invokes $EDITOR to produce a log for committing. + """ + return + +def edit_log(tree, version): + """Makes and edits the log for a tree. Does all kinds of fancy things + like log templates and merge summaries and log-for-merge + + :param tree: The tree to edit the log for + :type tree: `arch.WorkingTree` + """ + #ensure we have an editor before preparing the log + cmdutil.find_editor() + log = tree.log_message(create=False, version=version) + log_is_new = False + if log is None or cmdutil.prompt("Overwrite log"): + if log is not None: + os.remove(log.name) + log = tree.log_message(create=True, version=version) + log_is_new = True + tmplog = log.name + template = pylon.log_template_path(tree) + if template: + shutil.copyfile(template, tmplog) + comp_version = ancillary.comp_revision(tree).version + new_merges = cmdutil.iter_new_merges(tree, comp_version) + new_merges = cmdutil.direct_merges(new_merges) + log["Summary"] = pylon.merge_summary(new_merges, + version) + if len(new_merges) > 0: + if cmdutil.prompt("Log for merge"): + if cmdutil.prompt("changelog for merge"): + mergestuff = "Patches applied:\n" + mergestuff += pylon.changelog_for_merge(new_merges) + else: + mergestuff = cmdutil.log_for_merge(tree, comp_version) + log.description += mergestuff + log.save() + try: + cmdutil.invoke_editor(log.name) + except: + if log_is_new: + os.remove(log.name) + raise + + +class MirrorArchive(BaseCommand): + """ + Updates a mirror from an archive + """ + def __init__(self): + self.description="Update a mirror from an archive" + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + if len(args) > 1: + raise GetHelp + try: + tree = arch.tree_root() + except: + tree = None + + if len(args) == 0: + if tree is not None: + name = tree.tree_version() + else: + name = cmdutil.expand_alias(args[0], tree) + name = arch.NameParser(name) + + to_arch = name.get_archive() + from_arch = cmdutil.get_mirror_source(arch.Archive(to_arch)) + limit = name.get_nonarch() + + iter = arch_core.mirror_archive(from_arch,to_arch, limit) + for line in arch.chatter_classifier(iter): + cmdutil.colorize(line) + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai mirror-archive ARCHIVE") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Updates a mirror from an archive. If a branch, package, or version is +supplied, only changes under it are mirrored. + """ + return + +def help_tree_spec(): + print """Specifying revisions (default: tree) +Revisions may be specified by alias, revision, version or patchlevel. +Revisions or versions may be fully qualified. Unqualified revisions, versions, +or patchlevels use the archive of the current project tree. Versions will +use the latest patchlevel in the tree. Patchlevels will use the current tree- +version. + +Use "alias" to list available (user and automatic) aliases.""" + +auto_alias = [ +"acur", +"The latest revision in the archive of the tree-version. You can specify \ +a different version like so: acur:foo--bar--0 (aliases can be used)", +"tcur", +"""(tree current) The latest revision in the tree of the tree-version. \ +You can specify a different version like so: tcur:foo--bar--0 (aliases can be \ +used).""", +"tprev" , +"""(tree previous) The previous revision in the tree of the tree-version. To \ +specify an older revision, use a number, e.g. "tprev:4" """, +"tanc" , +"""(tree ancestor) The ancestor revision of the tree To specify an older \ +revision, use a number, e.g. "tanc:4".""", +"tdate" , +"""(tree date) The latest revision from a given date, e.g. "tdate:July 6".""", +"tmod" , +""" (tree modified) The latest revision to modify a given file, e.g. \ +"tmod:engine.cpp" or "tmod:engine.cpp:16".""", +"ttag" , +"""(tree tag) The revision that was tagged into the current tree revision, \ +according to the tree""", +"tagcur", +"""(tag current) The latest revision of the version that the current tree \ +was tagged from.""", +"mergeanc" , +"""The common ancestor of the current tree and the specified revision. \ +Defaults to the first partner-version's latest revision or to tagcur.""", +] + + +def is_auto_alias(name): + """Determine whether a name is an auto alias name + + :param name: the name to check + :type name: str + :return: True if the name is an auto alias, false if not + :rtype: bool + """ + return name in [f for (f, v) in pylon.util.iter_pairs(auto_alias)] + + +def display_def(iter, wrap = 80): + """Display a list of definitions + + :param iter: iter of name, definition pairs + :type iter: iter of (str, str) + :param wrap: The width for text wrapping + :type wrap: int + """ + vals = list(iter) + maxlen = 0 + for (key, value) in vals: + if len(key) > maxlen: + maxlen = len(key) + for (key, value) in vals: + tw=textwrap.TextWrapper(width=wrap, + initial_indent=key.rjust(maxlen)+" : ", + subsequent_indent="".rjust(maxlen+3)) + print tw.fill(value) + + +def help_aliases(tree): + print """Auto-generated aliases""" + display_def(pylon.util.iter_pairs(auto_alias)) + print "User aliases" + display_def(ancillary.iter_all_alias(tree)) + +class Inventory(BaseCommand): + """List the status of files in the tree""" + def __init__(self): + self.description=self.__doc__ + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + tree = arch.tree_root() + categories = [] + + if (options.source): + categories.append(arch_core.SourceFile) + if (options.precious): + categories.append(arch_core.PreciousFile) + if (options.backup): + categories.append(arch_core.BackupFile) + if (options.junk): + categories.append(arch_core.JunkFile) + + if len(categories) == 1: + show_leading = False + else: + show_leading = True + + if len(categories) == 0: + categories = None + + if options.untagged: + categories = arch_core.non_root + show_leading = False + tagged = False + else: + tagged = None + + for file in arch_core.iter_inventory_filter(tree, None, + control_files=options.control_files, + categories = categories, tagged=tagged): + print arch_core.file_line(file, + category = show_leading, + untagged = show_leading, + id = options.ids) + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai inventory [options]") + parser.add_option("--ids", action="store_true", dest="ids", + help="Show file ids") + parser.add_option("--control", action="store_true", + dest="control_files", help="include control files") + parser.add_option("--source", action="store_true", dest="source", + help="List source files") + parser.add_option("--backup", action="store_true", dest="backup", + help="List backup files") + parser.add_option("--precious", action="store_true", dest="precious", + help="List precious files") + parser.add_option("--junk", action="store_true", dest="junk", + help="List junk files") + parser.add_option("--unrecognized", action="store_true", + dest="unrecognized", help="List unrecognized files") + parser.add_option("--untagged", action="store_true", + dest="untagged", help="List only untagged files") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Lists the status of files in the archive: +S source +P precious +B backup +J junk +U unrecognized +T tree root +? untagged-source +Leading letter are not displayed if only one kind of file is shown + """ + return + + +class Alias(BaseCommand): + """List or adjust aliases""" + def __init__(self): + self.description=self.__doc__ + + def get_completer(self, arg, index): + if index > 2: + return () + try: + self.tree = arch.tree_root() + except: + self.tree = None + + if index == 0: + return [part[0]+" " for part in ancillary.iter_all_alias(self.tree)] + elif index == 1: + return cmdutil.iter_revision_completions(arg, self.tree) + + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: + self.tree = arch.tree_root() + except: + self.tree = None + + + try: + options.action(args, options) + except cmdutil.ForbiddenAliasSyntax, e: + raise CommandFailedWrapper(e) + + def no_prefix(self, alias): + if alias.startswith("^"): + alias = alias[1:] + return alias + + def arg_dispatch(self, args, options): + """Add, modify, or list aliases, depending on number of arguments + + :param args: The list of commandline arguments + :type args: list of str + :param options: The commandline options + """ + if len(args) == 0: + help_aliases(self.tree) + return + else: + alias = self.no_prefix(args[0]) + if len(args) == 1: + self.print_alias(alias) + elif (len(args)) == 2: + self.add(alias, args[1], options) + else: + raise cmdutil.GetHelp + + def print_alias(self, alias): + answer = None + if is_auto_alias(alias): + raise pylon.errors.IsAutoAlias(alias, "\"%s\" is an auto alias." + " Use \"revision\" to expand auto aliases." % alias) + for pair in ancillary.iter_all_alias(self.tree): + if pair[0] == alias: + answer = pair[1] + if answer is not None: + print answer + else: + print "The alias %s is not assigned." % alias + + def add(self, alias, expansion, options): + """Add or modify aliases + + :param alias: The alias name to create/modify + :type alias: str + :param expansion: The expansion to assign to the alias name + :type expansion: str + :param options: The commandline options + """ + if is_auto_alias(alias): + raise IsAutoAlias(alias) + newlist = "" + written = False + new_line = "%s=%s\n" % (alias, cmdutil.expand_alias(expansion, + self.tree)) + ancillary.check_alias(new_line.rstrip("\n"), [alias, expansion]) + + for pair in self.get_iterator(options): + if pair[0] != alias: + newlist+="%s=%s\n" % (pair[0], pair[1]) + elif not written: + newlist+=new_line + written = True + if not written: + newlist+=new_line + self.write_aliases(newlist, options) + + def delete(self, args, options): + """Delete the specified alias + + :param args: The list of arguments + :type args: list of str + :param options: The commandline options + """ + deleted = False + if len(args) != 1: + raise cmdutil.GetHelp + alias = self.no_prefix(args[0]) + if is_auto_alias(alias): + raise IsAutoAlias(alias) + newlist = "" + for pair in self.get_iterator(options): + if pair[0] != alias: + newlist+="%s=%s\n" % (pair[0], pair[1]) + else: + deleted = True + if not deleted: + raise errors.NoSuchAlias(alias) + self.write_aliases(newlist, options) + + def get_alias_file(self, options): + """Return the name of the alias file to use + + :param options: The commandline options + """ + if options.tree: + if self.tree is None: + self.tree == arch.tree_root() + return str(self.tree)+"/{arch}/+aliases" + else: + return "~/.aba/aliases" + + def get_iterator(self, options): + """Return the alias iterator to use + + :param options: The commandline options + """ + return ancillary.iter_alias(self.get_alias_file(options)) + + def write_aliases(self, newlist, options): + """Safely rewrite the alias file + :param newlist: The new list of aliases + :type newlist: str + :param options: The commandline options + """ + filename = os.path.expanduser(self.get_alias_file(options)) + file = util.NewFileVersion(filename) + file.write(newlist) + file.commit() + + + def get_parser(self): + """ + Returns the options parser to use for the "alias" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai alias [ALIAS] [NAME]") + parser.add_option("-d", "--delete", action="store_const", dest="action", + const=self.delete, default=self.arg_dispatch, + help="Delete an alias") + parser.add_option("--tree", action="store_true", dest="tree", + help="Create a per-tree alias", default=False) + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Lists current aliases or modifies the list of aliases. + +If no arguments are supplied, aliases will be listed. If two arguments are +supplied, the specified alias will be created or modified. If -d or --delete +is supplied, the specified alias will be deleted. + +You can create aliases that refer to any fully-qualified part of the +Arch namespace, e.g. +archive, +archive/category, +archive/category--branch, +archive/category--branch--version (my favourite) +archive/category--branch--version--patchlevel + +Aliases can be used automatically by native commands. To use them +with external or tla commands, prefix them with ^ (you can do this +with native commands, too). +""" + + +class RequestMerge(BaseCommand): + """Submit a merge request to Bug Goo""" + def __init__(self): + self.description=self.__doc__ + + def do_command(self, cmdargs): + """Submit a merge request + + :param cmdargs: The commandline arguments + :type cmdargs: list of str + """ + parser = self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: + cmdutil.find_editor() + except pylon.errors.NoEditorSpecified, e: + raise pylon.errors.CommandFailedWrapper(e) + try: + self.tree=arch.tree_root() + except: + self.tree=None + base, revisions = self.revision_specs(args) + message = self.make_headers(base, revisions) + message += self.make_summary(revisions) + path = self.edit_message(message) + message = self.tidy_message(path) + if cmdutil.prompt("Send merge"): + self.send_message(message) + print "Merge request sent" + + def make_headers(self, base, revisions): + """Produce email and Bug Goo header strings + + :param base: The base revision to apply merges to + :type base: `arch.Revision` + :param revisions: The revisions to replay into the base + :type revisions: list of `arch.Patchlog` + :return: The headers + :rtype: str + """ + headers = "To: gnu-arch-users@gnu.org\n" + headers += "From: %s\n" % options.fromaddr + if len(revisions) == 1: + headers += "Subject: [MERGE REQUEST] %s\n" % revisions[0].summary + else: + headers += "Subject: [MERGE REQUEST]\n" + headers += "\n" + headers += "Base-Revision: %s\n" % base + for revision in revisions: + headers += "Revision: %s\n" % revision.revision + headers += "Bug: \n\n" + return headers + + def make_summary(self, logs): + """Generate a summary of merges + + :param logs: the patchlogs that were directly added by the merges + :type logs: list of `arch.Patchlog` + :return: the summary + :rtype: str + """ + summary = "" + for log in logs: + summary+=str(log.revision)+"\n" + summary+=log.summary+"\n" + if log.description.strip(): + summary+=log.description.strip('\n')+"\n\n" + return summary + + def revision_specs(self, args): + """Determine the base and merge revisions from tree and arguments. + + :param args: The parsed arguments + :type args: list of str + :return: The base revision and merge revisions + :rtype: `arch.Revision`, list of `arch.Patchlog` + """ + if len(args) > 0: + target_revision = cmdutil.determine_revision_arch(self.tree, + args[0]) + else: + target_revision = arch_compound.tree_latest(self.tree) + if len(args) > 1: + merges = [ arch.Patchlog(cmdutil.determine_revision_arch( + self.tree, f)) for f in args[1:] ] + else: + if self.tree is None: + raise CantDetermineRevision("", "Not in a project tree") + merge_iter = cmdutil.iter_new_merges(self.tree, + target_revision.version, + False) + merges = [f for f in cmdutil.direct_merges(merge_iter)] + return (target_revision, merges) + + def edit_message(self, message): + """Edit an email message in the user's standard editor + + :param message: The message to edit + :type message: str + :return: the path of the edited message + :rtype: str + """ + if self.tree is None: + path = os.get_cwd() + else: + path = self.tree + path += "/,merge-request" + file = open(path, 'w') + file.write(message) + file.flush() + cmdutil.invoke_editor(path) + return path + + def tidy_message(self, path): + """Validate and clean up message. + + :param path: The path to the message to clean up + :type path: str + :return: The parsed message + :rtype: `email.Message` + """ + mail = email.message_from_file(open(path)) + if mail["Subject"].strip() == "[MERGE REQUEST]": + raise BlandSubject + + request = email.message_from_string(mail.get_payload()) + if request.has_key("Bug"): + if request["Bug"].strip()=="": + del request["Bug"] + mail.set_payload(request.as_string()) + return mail + + def send_message(self, message): + """Send a message, using its headers to address it. + + :param message: The message to send + :type message: `email.Message`""" + server = smtplib.SMTP("localhost") + server.sendmail(message['From'], message['To'], message.as_string()) + server.quit() + + def help(self, parser=None): + """Print a usage message + + :param parser: The options parser to use + :type parser: `cmdutil.CmdOptionParser` + """ + if parser is None: + parser = self.get_parser() + parser.print_help() + print """ +Sends a merge request formatted for Bug Goo. Intended use: get the tree +you'd like to merge into. Apply the merges you want. Invoke request-merge. +The merge request will open in your $EDITOR. + +When no TARGET is specified, it uses the current tree revision. When +no MERGE is specified, it uses the direct merges (as in "revisions +--direct-merges"). But you can specify just the TARGET, or all the MERGE +revisions. +""" + + def get_parser(self): + """Produce a commandline parser for this command. + + :rtype: `cmdutil.CmdOptionParser` + """ + parser=cmdutil.CmdOptionParser("request-merge [TARGET] [MERGE1...]") + return parser + +commands = { +'changes' : Changes, +'help' : Help, +'update': Update, +'apply-changes':ApplyChanges, +'cat-log': CatLog, +'commit': Commit, +'revision': Revision, +'revisions': Revisions, +'get': Get, +'revert': Revert, +'shell': Shell, +'add-id': AddID, +'merge': Merge, +'elog': ELog, +'mirror-archive': MirrorArchive, +'ninventory': Inventory, +'alias' : Alias, +'request-merge': RequestMerge, +} + +def my_import(mod_name): + module = __import__(mod_name) + components = mod_name.split('.') + for comp in components[1:]: + module = getattr(module, comp) + return module + +def plugin(mod_name): + module = my_import(mod_name) + module.add_command(commands) + +for file in os.listdir(sys.path[0]+"/command"): + if len(file) > 3 and file[-3:] == ".py" and file != "__init__.py": + plugin("command."+file[:-3]) + +suggestions = { +'apply-delta' : "Try \"apply-changes\".", +'delta' : "To compare two revisions, use \"changes\".", +'diff-rev' : "To compare two revisions, use \"changes\".", +'undo' : "To undo local changes, use \"revert\".", +'undelete' : "To undo only deletions, use \"revert --deletions\"", +'missing-from' : "Try \"revisions --missing-from\".", +'missing' : "Try \"revisions --missing\".", +'missing-merge' : "Try \"revisions --partner-missing\".", +'new-merges' : "Try \"revisions --new-merges\".", +'cachedrevs' : "Try \"revisions --cacherevs\". (no 'd')", +'logs' : "Try \"revisions --logs\"", +'tree-source' : "Use the \"^ttag\" alias (\"revision ^ttag\")", +'latest-revision' : "Use the \"^acur\" alias (\"revision ^acur\")", +'change-version' : "Try \"update REVISION\"", +'tree-revision' : "Use the \"^tcur\" alias (\"revision ^tcur\")", +'rev-depends' : "Use revisions --dependencies", +'auto-get' : "Plain get will do archive lookups", +'tagline' : "Use add-id. It uses taglines in tagline trees", +'emlog' : "Use elog. It automatically adds log-for-merge text, if any", +'library-revisions' : "Use revisions --library", +'file-revert' : "Use revert FILE", +'join-branch' : "Use replay --logs-only" +} +# arch-tag: 19d5739d-3708-486c-93ba-deecc3027fc7 *** added file 'testdata/orig' --- /dev/null +++ testdata/orig @@ -0,0 +1,2789 @@ +# Copyright (C) 2004 Aaron Bentley +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import sys +import arch +import arch.util +import arch.arch +import abacmds +import cmdutil +import shutil +import os +import options +import paths +import time +import cmd +import readline +import re +import string +import arch_core +from errors import * +import errors +import terminal +import ancillary +import misc +import email +import smtplib + +__docformat__ = "restructuredtext" +__doc__ = "Implementation of user (sub) commands" +commands = {} + +def find_command(cmd): + """ + Return an instance of a command type. Return None if the type isn't + registered. + + :param cmd: the name of the command to look for + :type cmd: the type of the command + """ + if commands.has_key(cmd): + return commands[cmd]() + else: + return None + +class BaseCommand: + def __call__(self, cmdline): + try: + self.do_command(cmdline.split()) + except cmdutil.GetHelp, e: + self.help() + except Exception, e: + print e + + def get_completer(index): + return None + + def complete(self, args, text): + """ + Returns a list of possible completions for the given text. + + :param args: The complete list of arguments + :type args: List of str + :param text: text to complete (may be shorter than args[-1]) + :type text: str + :rtype: list of str + """ + matches = [] + candidates = None + + if len(args) > 0: + realtext = args[-1] + else: + realtext = "" + + try: + parser=self.get_parser() + if realtext.startswith('-'): + candidates = parser.iter_options() + else: + (options, parsed_args) = parser.parse_args(args) + + if len (parsed_args) > 0: + candidates = self.get_completer(parsed_args[-1], len(parsed_args) -1) + else: + candidates = self.get_completer("", 0) + except: + pass + if candidates is None: + return + for candidate in candidates: + candidate = str(candidate) + if candidate.startswith(realtext): + matches.append(candidate[len(realtext)- len(text):]) + return matches + + +class Help(BaseCommand): + """ + Lists commands, prints help messages. + """ + def __init__(self): + self.description="Prints help mesages" + self.parser = None + + def do_command(self, cmdargs): + """ + Prints a help message. + """ + options, args = self.get_parser().parse_args(cmdargs) + if len(args) > 1: + raise cmdutil.GetHelp + + if options.native or options.suggestions or options.external: + native = options.native + suggestions = options.suggestions + external = options.external + else: + native = True + suggestions = False + external = True + + if len(args) == 0: + self.list_commands(native, suggestions, external) + return + elif len(args) == 1: + command_help(args[0]) + return + + def help(self): + self.get_parser().print_help() + print """ +If no command is specified, commands are listed. If a command is +specified, help for that command is listed. + """ + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + if self.parser is not None: + return self.parser + parser=cmdutil.CmdOptionParser("fai help [command]") + parser.add_option("-n", "--native", action="store_true", + dest="native", help="Show native commands") + parser.add_option("-e", "--external", action="store_true", + dest="external", help="Show external commands") + parser.add_option("-s", "--suggest", action="store_true", + dest="suggestions", help="Show suggestions") + self.parser = parser + return parser + + def list_commands(self, native=True, suggest=False, external=True): + """ + Lists supported commands. + + :param native: list native, python-based commands + :type native: bool + :param external: list external aba-style commands + :type external: bool + """ + if native: + print "Native Fai commands" + keys=commands.keys() + keys.sort() + for k in keys: + space="" + for i in range(28-len(k)): + space+=" " + print space+k+" : "+commands[k]().description + print + if suggest: + print "Unavailable commands and suggested alternatives" + key_list = suggestions.keys() + key_list.sort() + for key in key_list: + print "%28s : %s" % (key, suggestions[key]) + print + if external: + fake_aba = abacmds.AbaCmds() + if (fake_aba.abadir == ""): + return + print "External commands" + fake_aba.list_commands() + print + if not suggest: + print "Use help --suggest to list alternatives to tla and aba"\ + " commands." + if options.tla_fallthrough and (native or external): + print "Fai also supports tla commands." + +def command_help(cmd): + """ + Prints help for a command. + + :param cmd: The name of the command to print help for + :type cmd: str + """ + fake_aba = abacmds.AbaCmds() + cmdobj = find_command(cmd) + if cmdobj != None: + cmdobj.help() + elif suggestions.has_key(cmd): + print "Not available\n" + suggestions[cmd] + else: + abacmd = fake_aba.is_command(cmd) + if abacmd: + abacmd.help() + else: + print "No help is available for \""+cmd+"\". Maybe try \"tla "+cmd+" -H\"?" + + + +class Changes(BaseCommand): + """ + the "changes" command: lists differences between trees/revisions: + """ + + def __init__(self): + self.description="Lists what files have changed in the project tree" + + def get_completer(self, arg, index): + if index > 1: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def parse_commandline(self, cmdline): + """ + Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. + + :param cmdline: A list of arguments to parse + :rtype: (options, Revision, Revision/WorkingTree) + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdline) + if len(args) > 2: + raise cmdutil.GetHelp + + tree=arch.tree_root() + if len(args) == 0: + a_spec = cmdutil.comp_revision(tree) + else: + a_spec = cmdutil.determine_revision_tree(tree, args[0]) + cmdutil.ensure_archive_registered(a_spec.archive) + if len(args) == 2: + b_spec = cmdutil.determine_revision_tree(tree, args[1]) + cmdutil.ensure_archive_registered(b_spec.archive) + else: + b_spec=tree + return options, a_spec, b_spec + + def do_command(self, cmdargs): + """ + Master function that perfoms the "changes" command. + """ + try: + options, a_spec, b_spec = self.parse_commandline(cmdargs); + except cmdutil.CantDetermineRevision, e: + print e + return + except arch.errors.TreeRootError, e: + print e + return + if options.changeset: + changeset=options.changeset + tmpdir = None + else: + tmpdir=cmdutil.tmpdir() + changeset=tmpdir+"/changeset" + try: + delta=arch.iter_delta(a_spec, b_spec, changeset) + try: + for line in delta: + if cmdutil.chattermatch(line, "changeset:"): + pass + else: + cmdutil.colorize(line, options.suppress_chatter) + except arch.util.ExecProblem, e: + if e.proc.error and e.proc.error.startswith( + "missing explicit id for file"): + raise MissingID(e) + else: + raise + status=delta.status + if status > 1: + return + if (options.perform_diff): + chan = cmdutil.ChangesetMunger(changeset) + chan.read_indices() + if isinstance(b_spec, arch.Revision): + b_dir = b_spec.library_find() + else: + b_dir = b_spec + a_dir = a_spec.library_find() + if options.diffopts is not None: + diffopts = options.diffopts.split() + cmdutil.show_custom_diffs(chan, diffopts, a_dir, b_dir) + else: + cmdutil.show_diffs(delta.changeset) + finally: + if tmpdir and (os.access(tmpdir, os.X_OK)): + shutil.rmtree(tmpdir) + + def get_parser(self): + """ + Returns the options parser to use for the "changes" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai changes [options] [revision]" + " [revision]") + parser.add_option("-d", "--diff", action="store_true", + dest="perform_diff", default=False, + help="Show diffs in summary") + parser.add_option("-c", "--changeset", dest="changeset", + help="Store a changeset in the given directory", + metavar="DIRECTORY") + parser.add_option("-s", "--silent", action="store_true", + dest="suppress_chatter", default=False, + help="Suppress chatter messages") + parser.add_option("--diffopts", dest="diffopts", + help="Use the specified diff options", + metavar="OPTIONS") + + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser is None: + parser=self.get_parser() + parser.print_help() + print """ +Performs source-tree comparisons + +If no revision is specified, the current project tree is compared to the +last-committed revision. If one revision is specified, the current project +tree is compared to that revision. If two revisions are specified, they are +compared to each other. + """ + help_tree_spec() + return + + +class ApplyChanges(BaseCommand): + """ + Apply differences between two revisions to a tree + """ + + def __init__(self): + self.description="Applies changes to a project tree" + + def get_completer(self, arg, index): + if index > 1: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def parse_commandline(self, cmdline, tree): + """ + Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. + + :param cmdline: A list of arguments to parse + :rtype: (options, Revision, Revision/WorkingTree) + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdline) + if len(args) != 2: + raise cmdutil.GetHelp + + a_spec = cmdutil.determine_revision_tree(tree, args[0]) + cmdutil.ensure_archive_registered(a_spec.archive) + b_spec = cmdutil.determine_revision_tree(tree, args[1]) + cmdutil.ensure_archive_registered(b_spec.archive) + return options, a_spec, b_spec + + def do_command(self, cmdargs): + """ + Master function that performs "apply-changes". + """ + try: + tree = arch.tree_root() + options, a_spec, b_spec = self.parse_commandline(cmdargs, tree); + except cmdutil.CantDetermineRevision, e: + print e + return + except arch.errors.TreeRootError, e: + print e + return + delta=cmdutil.apply_delta(a_spec, b_spec, tree) + for line in cmdutil.iter_apply_delta_filter(delta): + cmdutil.colorize(line, options.suppress_chatter) + + def get_parser(self): + """ + Returns the options parser to use for the "apply-changes" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai apply-changes [options] revision" + " revision") + parser.add_option("-d", "--diff", action="store_true", + dest="perform_diff", default=False, + help="Show diffs in summary") + parser.add_option("-c", "--changeset", dest="changeset", + help="Store a changeset in the given directory", + metavar="DIRECTORY") + parser.add_option("-s", "--silent", action="store_true", + dest="suppress_chatter", default=False, + help="Suppress chatter messages") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser is None: + parser=self.get_parser() + parser.print_help() + print """ +Applies changes to a project tree + +Compares two revisions and applies the difference between them to the current +tree. + """ + help_tree_spec() + return + +class Update(BaseCommand): + """ + Updates a project tree to a given revision, preserving un-committed hanges. + """ + + def __init__(self): + self.description="Apply the latest changes to the current directory" + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def parse_commandline(self, cmdline, tree): + """ + Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. + + :param cmdline: A list of arguments to parse + :rtype: (options, Revision, Revision/WorkingTree) + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdline) + if len(args) > 2: + raise cmdutil.GetHelp + + spec=None + if len(args)>0: + spec=args[0] + revision=cmdutil.determine_revision_arch(tree, spec) + cmdutil.ensure_archive_registered(revision.archive) + + mirror_source = cmdutil.get_mirror_source(revision.archive) + if mirror_source != None: + if cmdutil.prompt("Mirror update"): + cmd=cmdutil.mirror_archive(mirror_source, + revision.archive, arch.NameParser(revision).get_package_version()) + for line in arch.chatter_classifier(cmd): + cmdutil.colorize(line, options.suppress_chatter) + + revision=cmdutil.determine_revision_arch(tree, spec) + + return options, revision + + def do_command(self, cmdargs): + """ + Master function that perfoms the "update" command. + """ + tree=arch.tree_root() + try: + options, to_revision = self.parse_commandline(cmdargs, tree); + except cmdutil.CantDetermineRevision, e: + print e + return + except arch.errors.TreeRootError, e: + print e + return + from_revision=cmdutil.tree_latest(tree) + if from_revision==to_revision: + print "Tree is already up to date with:\n"+str(to_revision)+"." + return + cmdutil.ensure_archive_registered(from_revision.archive) + cmd=cmdutil.apply_delta(from_revision, to_revision, tree, + options.patch_forward) + for line in cmdutil.iter_apply_delta_filter(cmd): + cmdutil.colorize(line) + if to_revision.version != tree.tree_version: + if cmdutil.prompt("Update version"): + tree.tree_version = to_revision.version + + def get_parser(self): + """ + Returns the options parser to use for the "update" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai update [options]" + " [revision/version]") + parser.add_option("-f", "--forward", action="store_true", + dest="patch_forward", default=False, + help="pass the --forward option to 'patch'") + parser.add_option("-s", "--silent", action="store_true", + dest="suppress_chatter", default=False, + help="Suppress chatter messages") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser is None: + parser=self.get_parser() + parser.print_help() + print """ +Updates a working tree to the current archive revision + +If a revision or version is specified, that is used instead + """ + help_tree_spec() + return + + +class Commit(BaseCommand): + """ + Create a revision based on the changes in the current tree. + """ + + def __init__(self): + self.description="Write local changes to the archive" + + def get_completer(self, arg, index): + if arg is None: + arg = "" + return iter_modified_file_completions(arch.tree_root(), arg) +# return iter_source_file_completions(arch.tree_root(), arg) + + def parse_commandline(self, cmdline, tree): + """ + Parse commandline arguments. Raise cmtutil.GetHelp if help is needed. + + :param cmdline: A list of arguments to parse + :rtype: (options, Revision, Revision/WorkingTree) + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdline) + + if len(args) == 0: + args = None + revision=cmdutil.determine_revision_arch(tree, options.version) + return options, revision.get_version(), args + + def do_command(self, cmdargs): + """ + Master function that perfoms the "commit" command. + """ + tree=arch.tree_root() + options, version, files = self.parse_commandline(cmdargs, tree) + if options.__dict__.has_key("base") and options.base: + base = cmdutil.determine_revision_tree(tree, options.base) + else: + base = cmdutil.submit_revision(tree) + + writeversion=version + archive=version.archive + source=cmdutil.get_mirror_source(archive) + allow_old=False + writethrough="implicit" + + if source!=None: + if writethrough=="explicit" and \ + cmdutil.prompt("Writethrough"): + writeversion=arch.Version(str(source)+"/"+str(version.get_nonarch())) + elif writethrough=="none": + raise CommitToMirror(archive) + + elif archive.is_mirror: + raise CommitToMirror(archive) + + try: + last_revision=tree.iter_logs(version, True).next().revision + except StopIteration, e: + if cmdutil.prompt("Import from commit"): + return do_import(version) + else: + raise NoVersionLogs(version) + if last_revision!=version.iter_revisions(True).next(): + if not cmdutil.prompt("Out of date"): + raise OutOfDate + else: + allow_old=True + + try: + if not cmdutil.has_changed(version): + if not cmdutil.prompt("Empty commit"): + raise EmptyCommit + except arch.util.ExecProblem, e: + if e.proc.error and e.proc.error.startswith( + "missing explicit id for file"): + raise MissingID(e) + else: + raise + log = tree.log_message(create=False) + if log is None: + try: + if cmdutil.prompt("Create log"): + edit_log(tree) + + except cmdutil.NoEditorSpecified, e: + raise CommandFailed(e) + log = tree.log_message(create=False) + if log is None: + raise NoLogMessage + if log["Summary"] is None or len(log["Summary"].strip()) == 0: + if not cmdutil.prompt("Omit log summary"): + raise errors.NoLogSummary + try: + for line in tree.iter_commit(version, seal=options.seal_version, + base=base, out_of_date_ok=allow_old, file_list=files): + cmdutil.colorize(line, options.suppress_chatter) + + except arch.util.ExecProblem, e: + if e.proc.error and e.proc.error.startswith( + "These files violate naming conventions:"): + raise LintFailure(e.proc.error) + else: + raise + + def get_parser(self): + """ + Returns the options parser to use for the "commit" command. + + :rtype: cmdutil.CmdOptionParser + """ + + parser=cmdutil.CmdOptionParser("fai commit [options] [file1]" + " [file2...]") + parser.add_option("--seal", action="store_true", + dest="seal_version", default=False, + help="seal this version") + parser.add_option("-v", "--version", dest="version", + help="Use the specified version", + metavar="VERSION") + parser.add_option("-s", "--silent", action="store_true", + dest="suppress_chatter", default=False, + help="Suppress chatter messages") + if cmdutil.supports_switch("commit", "--base"): + parser.add_option("--base", dest="base", help="", + metavar="REVISION") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser is None: + parser=self.get_parser() + parser.print_help() + print """ +Updates a working tree to the current archive revision + +If a version is specified, that is used instead + """ +# help_tree_spec() + return + + + +class CatLog(BaseCommand): + """ + Print the log of a given file (from current tree) + """ + def __init__(self): + self.description="Prints the patch log for a revision" + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "cat-log" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: + tree = arch.tree_root() + except arch.errors.TreeRootError, e: + tree = None + spec=None + if len(args) > 0: + spec=args[0] + if len(args) > 1: + raise cmdutil.GetHelp() + try: + if tree: + revision = cmdutil.determine_revision_tree(tree, spec) + else: + revision = cmdutil.determine_revision_arch(tree, spec) + except cmdutil.CantDetermineRevision, e: + raise CommandFailedWrapper(e) + log = None + + use_tree = (options.source == "tree" or \ + (options.source == "any" and tree)) + use_arch = (options.source == "archive" or options.source == "any") + + log = None + if use_tree: + for log in tree.iter_logs(revision.get_version()): + if log.revision == revision: + break + else: + log = None + if log is None and use_arch: + cmdutil.ensure_revision_exists(revision) + log = arch.Patchlog(revision) + if log is not None: + for item in log.items(): + print "%s: %s" % item + print log.description + + def get_parser(self): + """ + Returns the options parser to use for the "cat-log" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai cat-log [revision]") + parser.add_option("--archive", action="store_const", dest="source", + const="archive", default="any", + help="Always get the log from the archive") + parser.add_option("--tree", action="store_const", dest="source", + const="tree", help="Always get the log from the tree") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Prints the log for the specified revision + """ + help_tree_spec() + return + +class Revert(BaseCommand): + """ Reverts a tree (or aspects of it) to a revision + """ + def __init__(self): + self.description="Reverts a tree (or aspects of it) to a revision " + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return iter_modified_file_completions(tree, arg) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revert" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: + tree = arch.tree_root() + except arch.errors.TreeRootError, e: + raise CommandFailed(e) + spec=None + if options.revision is not None: + spec=options.revision + try: + if spec is not None: + revision = cmdutil.determine_revision_tree(tree, spec) + else: + revision = cmdutil.comp_revision(tree) + except cmdutil.CantDetermineRevision, e: + raise CommandFailedWrapper(e) + munger = None + + if options.file_contents or options.file_perms or options.deletions\ + or options.additions or options.renames or options.hunk_prompt: + munger = cmdutil.MungeOpts() + munger.hunk_prompt = options.hunk_prompt + + if len(args) > 0 or options.logs or options.pattern_files or \ + options.control: + if munger is None: + munger = cmdutil.MungeOpts(True) + munger.all_types(True) + if len(args) > 0: + t_cwd = cmdutil.tree_cwd(tree) + for name in args: + if len(t_cwd) > 0: + t_cwd += "/" + name = "./" + t_cwd + name + munger.add_keep_file(name); + + if options.file_perms: + munger.file_perms = True + if options.file_contents: + munger.file_contents = True + if options.deletions: + munger.deletions = True + if options.additions: + munger.additions = True + if options.renames: + munger.renames = True + if options.logs: + munger.add_keep_pattern('^\./\{arch\}/[^=].*') + if options.control: + munger.add_keep_pattern("/\.arch-ids|^\./\{arch\}|"\ + "/\.arch-inventory$") + if options.pattern_files: + munger.add_keep_pattern(options.pattern_files) + + for line in cmdutil.revert(tree, revision, munger, + not options.no_output): + cmdutil.colorize(line) + + + def get_parser(self): + """ + Returns the options parser to use for the "cat-log" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai revert [options] [FILE...]") + parser.add_option("", "--contents", action="store_true", + dest="file_contents", + help="Revert file content changes") + parser.add_option("", "--permissions", action="store_true", + dest="file_perms", + help="Revert file permissions changes") + parser.add_option("", "--deletions", action="store_true", + dest="deletions", + help="Restore deleted files") + parser.add_option("", "--additions", action="store_true", + dest="additions", + help="Remove added files") + parser.add_option("", "--renames", action="store_true", + dest="renames", + help="Revert file names") + parser.add_option("--hunks", action="store_true", + dest="hunk_prompt", default=False, + help="Prompt which hunks to revert") + parser.add_option("--pattern-files", dest="pattern_files", + help="Revert files that match this pattern", + metavar="REGEX") + parser.add_option("--logs", action="store_true", + dest="logs", default=False, + help="Revert only logs") + parser.add_option("--control-files", action="store_true", + dest="control", default=False, + help="Revert logs and other control files") + parser.add_option("-n", "--no-output", action="store_true", + dest="no_output", + help="Don't keep an undo changeset") + parser.add_option("--revision", dest="revision", + help="Revert to the specified revision", + metavar="REVISION") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Reverts changes in the current working tree. If no flags are specified, all +types of changes are reverted. Otherwise, only selected types of changes are +reverted. + +If a revision is specified on the commandline, differences between the current +tree and that revision are reverted. If a version is specified, the current +tree is used to determine the revision. + +If files are specified, only those files listed will have any changes applied. +To specify a renamed file, you can use either the old or new name. (or both!) + +Unless "-n" is specified, reversions can be undone with "redo". + """ + return + +class Revision(BaseCommand): + """ + Print a revision name based on a revision specifier + """ + def __init__(self): + self.description="Prints the name of a revision" + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + tree = None + + spec=None + if len(args) > 0: + spec=args[0] + if len(args) > 1: + raise cmdutil.GetHelp + try: + if tree: + revision = cmdutil.determine_revision_tree(tree, spec) + else: + revision = cmdutil.determine_revision_arch(tree, spec) + except cmdutil.CantDetermineRevision, e: + print str(e) + return + print options.display(revision) + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai revision [revision]") + parser.add_option("", "--location", action="store_const", + const=paths.determine_path, dest="display", + help="Show location instead of name", default=str) + parser.add_option("--import", action="store_const", + const=paths.determine_import_path, dest="display", + help="Show location of import file") + parser.add_option("--log", action="store_const", + const=paths.determine_log_path, dest="display", + help="Show location of log file") + parser.add_option("--patch", action="store_const", + dest="display", const=paths.determine_patch_path, + help="Show location of patchfile") + parser.add_option("--continuation", action="store_const", + const=paths.determine_continuation_path, + dest="display", + help="Show location of continuation file") + parser.add_option("--cacherev", action="store_const", + const=paths.determine_cacherev_path, dest="display", + help="Show location of cacherev file") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Expands aliases and prints the name of the specified revision. Instead of +the name, several options can be used to print locations. If more than one is +specified, the last one is used. + """ + help_tree_spec() + return + +def require_version_exists(version, spec): + if not version.exists(): + raise cmdutil.CantDetermineVersion(spec, + "The version %s does not exist." \ + % version) + +class Revisions(BaseCommand): + """ + Print a revision name based on a revision specifier + """ + def __init__(self): + self.description="Lists revisions" + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + (options, args) = self.get_parser().parse_args(cmdargs) + if len(args) > 1: + raise cmdutil.GetHelp + try: + self.tree = arch.tree_root() + except arch.errors.TreeRootError: + self.tree = None + try: + iter = self.get_iterator(options.type, args, options.reverse, + options.modified) + except cmdutil.CantDetermineRevision, e: + raise CommandFailedWrapper(e) + + if options.skip is not None: + iter = cmdutil.iter_skip(iter, int(options.skip)) + + for revision in iter: + log = None + if isinstance(revision, arch.Patchlog): + log = revision + revision=revision.revision + print options.display(revision) + if log is None and (options.summary or options.creator or + options.date or options.merges): + log = revision.patchlog + if options.creator: + print " %s" % log.creator + if options.date: + print " %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date) + if options.summary: + print " %s" % log.summary + if options.merges: + showed_title = False + for revision in log.merged_patches: + if not showed_title: + print " Merged:" + showed_title = True + print " %s" % revision + + def get_iterator(self, type, args, reverse, modified): + if len(args) > 0: + spec = args[0] + else: + spec = None + if modified is not None: + iter = cmdutil.modified_iter(modified, self.tree) + if reverse: + return iter + else: + return cmdutil.iter_reverse(iter) + elif type == "archive": + if spec is None: + if self.tree is None: + raise cmdutil.CantDetermineRevision("", + "Not in a project tree") + version = cmdutil.determine_version_tree(spec, self.tree) + else: + version = cmdutil.determine_version_arch(spec, self.tree) + cmdutil.ensure_archive_registered(version.archive) + require_version_exists(version, spec) + return version.iter_revisions(reverse) + elif type == "cacherevs": + if spec is None: + if self.tree is None: + raise cmdutil.CantDetermineRevision("", + "Not in a project tree") + version = cmdutil.determine_version_tree(spec, self.tree) + else: + version = cmdutil.determine_version_arch(spec, self.tree) + cmdutil.ensure_archive_registered(version.archive) + require_version_exists(version, spec) + return cmdutil.iter_cacherevs(version, reverse) + elif type == "library": + if spec is None: + if self.tree is None: + raise cmdutil.CantDetermineRevision("", + "Not in a project tree") + version = cmdutil.determine_version_tree(spec, self.tree) + else: + version = cmdutil.determine_version_arch(spec, self.tree) + return version.iter_library_revisions(reverse) + elif type == "logs": + if self.tree is None: + raise cmdutil.CantDetermineRevision("", "Not in a project tree") + return self.tree.iter_logs(cmdutil.determine_version_tree(spec, \ + self.tree), reverse) + elif type == "missing" or type == "skip-present": + if self.tree is None: + raise cmdutil.CantDetermineRevision("", "Not in a project tree") + skip = (type == "skip-present") + version = cmdutil.determine_version_tree(spec, self.tree) + cmdutil.ensure_archive_registered(version.archive) + require_version_exists(version, spec) + return cmdutil.iter_missing(self.tree, version, reverse, + skip_present=skip) + + elif type == "present": + if self.tree is None: + raise cmdutil.CantDetermineRevision("", "Not in a project tree") + version = cmdutil.determine_version_tree(spec, self.tree) + cmdutil.ensure_archive_registered(version.archive) + require_version_exists(version, spec) + return cmdutil.iter_present(self.tree, version, reverse) + + elif type == "new-merges" or type == "direct-merges": + if self.tree is None: + raise cmdutil.CantDetermineRevision("", "Not in a project tree") + version = cmdutil.determine_version_tree(spec, self.tree) + cmdutil.ensure_archive_registered(version.archive) + require_version_exists(version, spec) + iter = cmdutil.iter_new_merges(self.tree, version, reverse) + if type == "new-merges": + return iter + elif type == "direct-merges": + return cmdutil.direct_merges(iter) + + elif type == "missing-from": + if self.tree is None: + raise cmdutil.CantDetermineRevision("", "Not in a project tree") + revision = cmdutil.determine_revision_tree(self.tree, spec) + libtree = cmdutil.find_or_make_local_revision(revision) + return cmdutil.iter_missing(libtree, self.tree.tree_version, + reverse) + + elif type == "partner-missing": + return cmdutil.iter_partner_missing(self.tree, reverse) + + elif type == "ancestry": + revision = cmdutil.determine_revision_tree(self.tree, spec) + iter = cmdutil._iter_ancestry(self.tree, revision) + if reverse: + return iter + else: + return cmdutil.iter_reverse(iter) + + elif type == "dependencies" or type == "non-dependencies": + nondeps = (type == "non-dependencies") + revision = cmdutil.determine_revision_tree(self.tree, spec) + anc_iter = cmdutil._iter_ancestry(self.tree, revision) + iter_depends = cmdutil.iter_depends(anc_iter, nondeps) + if reverse: + return iter_depends + else: + return cmdutil.iter_reverse(iter_depends) + elif type == "micro": + return cmdutil.iter_micro(self.tree) + + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai revisions [revision]") + select = cmdutil.OptionGroup(parser, "Selection options", + "Control which revisions are listed. These options" + " are mutually exclusive. If more than one is" + " specified, the last is used.") + select.add_option("", "--archive", action="store_const", + const="archive", dest="type", default="archive", + help="List all revisions in the archive") + select.add_option("", "--cacherevs", action="store_const", + const="cacherevs", dest="type", + help="List all revisions stored in the archive as " + "complete copies") + select.add_option("", "--logs", action="store_const", + const="logs", dest="type", + help="List revisions that have a patchlog in the " + "tree") + select.add_option("", "--missing", action="store_const", + const="missing", dest="type", + help="List revisions from the specified version that" + " have no patchlog in the tree") + select.add_option("", "--skip-present", action="store_const", + const="skip-present", dest="type", + help="List revisions from the specified version that" + " have no patchlogs at all in the tree") + select.add_option("", "--present", action="store_const", + const="present", dest="type", + help="List revisions from the specified version that" + " have no patchlog in the tree, but can't be merged") + select.add_option("", "--missing-from", action="store_const", + const="missing-from", dest="type", + help="List revisions from the specified revision " + "that have no patchlog for the tree version") + select.add_option("", "--partner-missing", action="store_const", + const="partner-missing", dest="type", + help="List revisions in partner versions that are" + " missing") + select.add_option("", "--new-merges", action="store_const", + const="new-merges", dest="type", + help="List revisions that have had patchlogs added" + " to the tree since the last commit") + select.add_option("", "--direct-merges", action="store_const", + const="direct-merges", dest="type", + help="List revisions that have been directly added" + " to tree since the last commit ") + select.add_option("", "--library", action="store_const", + const="library", dest="type", + help="List revisions in the revision library") + select.add_option("", "--ancestry", action="store_const", + const="ancestry", dest="type", + help="List revisions that are ancestors of the " + "current tree version") + + select.add_option("", "--dependencies", action="store_const", + const="dependencies", dest="type", + help="List revisions that the given revision " + "depends on") + + select.add_option("", "--non-dependencies", action="store_const", + const="non-dependencies", dest="type", + help="List revisions that the given revision " + "does not depend on") + + select.add_option("--micro", action="store_const", + const="micro", dest="type", + help="List partner revisions aimed for this " + "micro-branch") + + select.add_option("", "--modified", dest="modified", + help="List tree ancestor revisions that modified a " + "given file", metavar="FILE[:LINE]") + + parser.add_option("", "--skip", dest="skip", + help="Skip revisions. Positive numbers skip from " + "beginning, negative skip from end.", + metavar="NUMBER") + + parser.add_option_group(select) + + format = cmdutil.OptionGroup(parser, "Revision format options", + "These control the appearance of listed revisions") + format.add_option("", "--location", action="store_const", + const=paths.determine_path, dest="display", + help="Show location instead of name", default=str) + format.add_option("--import", action="store_const", + const=paths.determine_import_path, dest="display", + help="Show location of import file") + format.add_option("--log", action="store_const", + const=paths.determine_log_path, dest="display", + help="Show location of log file") + format.add_option("--patch", action="store_const", + dest="display", const=paths.determine_patch_path, + help="Show location of patchfile") + format.add_option("--continuation", action="store_const", + const=paths.determine_continuation_path, + dest="display", + help="Show location of continuation file") + format.add_option("--cacherev", action="store_const", + const=paths.determine_cacherev_path, dest="display", + help="Show location of cacherev file") + parser.add_option_group(format) + display = cmdutil.OptionGroup(parser, "Display format options", + "These control the display of data") + display.add_option("-r", "--reverse", action="store_true", + dest="reverse", help="Sort from newest to oldest") + display.add_option("-s", "--summary", action="store_true", + dest="summary", help="Show patchlog summary") + display.add_option("-D", "--date", action="store_true", + dest="date", help="Show patchlog date") + display.add_option("-c", "--creator", action="store_true", + dest="creator", help="Show the id that committed the" + " revision") + display.add_option("-m", "--merges", action="store_true", + dest="merges", help="Show the revisions that were" + " merged") + parser.add_option_group(display) + return parser + def help(self, parser=None): + """Attempt to explain the revisions command + + :param parser: If supplied, used to determine options + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """List revisions. + """ + help_tree_spec() + + +class Get(BaseCommand): + """ + Retrieve a revision from the archive + """ + def __init__(self): + self.description="Retrieve a revision from the archive" + self.parser=self.get_parser() + + + def get_completer(self, arg, index): + if index > 0: + return None + try: + tree = arch.tree_root() + except: + tree = None + return cmdutil.iter_revision_completions(arg, tree) + + + def do_command(self, cmdargs): + """ + Master function that perfoms the "get" command. + """ + (options, args) = self.parser.parse_args(cmdargs) + if len(args) < 1: + return self.help() + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + tree = None + + arch_loc = None + try: + revision, arch_loc = paths.full_path_decode(args[0]) + except Exception, e: + revision = cmdutil.determine_revision_arch(tree, args[0], + check_existence=False, allow_package=True) + if len(args) > 1: + directory = args[1] + else: + directory = str(revision.nonarch) + if os.path.exists(directory): + raise DirectoryExists(directory) + cmdutil.ensure_archive_registered(revision.archive, arch_loc) + try: + cmdutil.ensure_revision_exists(revision) + except cmdutil.NoSuchRevision, e: + raise CommandFailedWrapper(e) + + link = cmdutil.prompt ("get link") + for line in cmdutil.iter_get(revision, directory, link, + options.no_pristine, + options.no_greedy_add): + cmdutil.colorize(line) + + def get_parser(self): + """ + Returns the options parser to use for the "get" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai get revision [dir]") + parser.add_option("--no-pristine", action="store_true", + dest="no_pristine", + help="Do not make pristine copy for reference") + parser.add_option("--no-greedy-add", action="store_true", + dest="no_greedy_add", + help="Never add to greedy libraries") + + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Expands aliases and constructs a project tree for a revision. If the optional +"dir" argument is provided, the project tree will be stored in this directory. + """ + help_tree_spec() + return + +class PromptCmd(cmd.Cmd): + def __init__(self): + cmd.Cmd.__init__(self) + self.prompt = "Fai> " + try: + self.tree = arch.tree_root() + except: + self.tree = None + self.set_title() + self.set_prompt() + self.fake_aba = abacmds.AbaCmds() + self.identchars += '-' + self.history_file = os.path.expanduser("~/.fai-history") + readline.set_completer_delims(string.whitespace) + if os.access(self.history_file, os.R_OK) and \ + os.path.isfile(self.history_file): + readline.read_history_file(self.history_file) + + def write_history(self): + readline.write_history_file(self.history_file) + + def do_quit(self, args): + self.write_history() + sys.exit(0) + + def do_exit(self, args): + self.do_quit(args) + + def do_EOF(self, args): + print + self.do_quit(args) + + def postcmd(self, line, bar): + self.set_title() + self.set_prompt() + + def set_prompt(self): + if self.tree is not None: + try: + version = " "+self.tree.tree_version.nonarch + except: + version = "" + else: + version = "" + self.prompt = "Fai%s> " % version + + def set_title(self, command=None): + try: + version = self.tree.tree_version.nonarch + except: + version = "[no version]" + if command is None: + command = "" + sys.stdout.write(terminal.term_title("Fai %s %s" % (command, version))) + + def do_cd(self, line): + if line == "": + line = "~" + try: + os.chdir(os.path.expanduser(line)) + except Exception, e: + print e + try: + self.tree = arch.tree_root() + except: + self.tree = None + + def do_help(self, line): + Help()(line) + + def default(self, line): + args = line.split() + if find_command(args[0]): + try: + find_command(args[0]).do_command(args[1:]) + except cmdutil.BadCommandOption, e: + print e + except cmdutil.GetHelp, e: + find_command(args[0]).help() + except CommandFailed, e: + print e + except arch.errors.ArchiveNotRegistered, e: + print e + except KeyboardInterrupt, e: + print "Interrupted" + except arch.util.ExecProblem, e: + print e.proc.error.rstrip('\n') + except cmdutil.CantDetermineVersion, e: + print e + except cmdutil.CantDetermineRevision, e: + print e + except Exception, e: + print "Unhandled error:\n%s" % cmdutil.exception_str(e) + + elif suggestions.has_key(args[0]): + print suggestions[args[0]] + + elif self.fake_aba.is_command(args[0]): + tree = None + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + pass + cmd = self.fake_aba.is_command(args[0]) + try: + cmd.run(cmdutil.expand_prefix_alias(args[1:], tree)) + except KeyboardInterrupt, e: + print "Interrupted" + + elif options.tla_fallthrough and args[0] != "rm" and \ + cmdutil.is_tla_command(args[0]): + try: + tree = None + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + pass + args = cmdutil.expand_prefix_alias(args, tree) + arch.util.exec_safe('tla', args, stderr=sys.stderr, + expected=(0, 1)) + except arch.util.ExecProblem, e: + pass + except KeyboardInterrupt, e: + print "Interrupted" + else: + try: + try: + tree = arch.tree_root() + except arch.errors.TreeRootError: + tree = None + args=line.split() + os.system(" ".join(cmdutil.expand_prefix_alias(args, tree))) + except KeyboardInterrupt, e: + print "Interrupted" + + def completenames(self, text, line, begidx, endidx): + completions = [] + iter = iter_command_names(self.fake_aba) + try: + if len(line) > 0: + arg = line.split()[-1] + else: + arg = "" + iter = iter_munged_completions(iter, arg, text) + except Exception, e: + print e + return list(iter) + + def completedefault(self, text, line, begidx, endidx): + """Perform completion for native commands. + + :param text: The text to complete + :type text: str + :param line: The entire line to complete + :type line: str + :param begidx: The start of the text in the line + :type begidx: int + :param endidx: The end of the text in the line + :type endidx: int + """ + try: + (cmd, args, foo) = self.parseline(line) + command_obj=find_command(cmd) + if command_obj is not None: + return command_obj.complete(args.split(), text) + elif not self.fake_aba.is_command(cmd) and \ + cmdutil.is_tla_command(cmd): + iter = cmdutil.iter_supported_switches(cmd) + if len(args) > 0: + arg = args.split()[-1] + else: + arg = "" + if arg.startswith("-"): + return list(iter_munged_completions(iter, arg, text)) + else: + return list(iter_munged_completions( + iter_file_completions(arg), arg, text)) + + + elif cmd == "cd": + if len(args) > 0: + arg = args.split()[-1] + else: + arg = "" + iter = iter_dir_completions(arg) + iter = iter_munged_completions(iter, arg, text) + return list(iter) + elif len(args)>0: + arg = args.split()[-1] + return list(iter_munged_completions(iter_file_completions(arg), + arg, text)) + else: + return self.completenames(text, line, begidx, endidx) + except Exception, e: + print e + + +def iter_command_names(fake_aba): + for entry in cmdutil.iter_combine([commands.iterkeys(), + fake_aba.get_commands(), + cmdutil.iter_tla_commands(False)]): + if not suggestions.has_key(str(entry)): + yield entry + + +def iter_file_completions(arg, only_dirs = False): + """Generate an iterator that iterates through filename completions. + + :param arg: The filename fragment to match + :type arg: str + :param only_dirs: If true, match only directories + :type only_dirs: bool + """ + cwd = os.getcwd() + if cwd != "/": + extras = [".", ".."] + else: + extras = [] + (dir, file) = os.path.split(arg) + if dir != "": + listingdir = os.path.expanduser(dir) + else: + listingdir = cwd + for file in cmdutil.iter_combine([os.listdir(listingdir), extras]): + if dir != "": + userfile = dir+'/'+file + else: + userfile = file + if userfile.startswith(arg): + if os.path.isdir(listingdir+'/'+file): + userfile+='/' + yield userfile + elif not only_dirs: + yield userfile + +def iter_munged_completions(iter, arg, text): + for completion in iter: + completion = str(completion) + if completion.startswith(arg): + yield completion[len(arg)-len(text):] + +def iter_source_file_completions(tree, arg): + treepath = cmdutil.tree_cwd(tree) + if len(treepath) > 0: + dirs = [treepath] + else: + dirs = None + for file in tree.iter_inventory(dirs, source=True, both=True): + file = file_completion_match(file, treepath, arg) + if file is not None: + yield file + + +def iter_untagged(tree, dirs): + for file in arch_core.iter_inventory_filter(tree, dirs, tagged=False, + categories=arch_core.non_root, + control_files=True): + yield file.name + + +def iter_untagged_completions(tree, arg): + """Generate an iterator for all visible untagged files that match arg. + + :param tree: The tree to look for untagged files in + :type tree: `arch.WorkingTree` + :param arg: The argument to match + :type arg: str + :return: An iterator of all matching untagged files + :rtype: iterator of str + """ + treepath = cmdutil.tree_cwd(tree) + if len(treepath) > 0: + dirs = [treepath] + else: + dirs = None + + for file in iter_untagged(tree, dirs): + file = file_completion_match(file, treepath, arg) + if file is not None: + yield file + + +def file_completion_match(file, treepath, arg): + """Determines whether a file within an arch tree matches the argument. + + :param file: The rooted filename + :type file: str + :param treepath: The path to the cwd within the tree + :type treepath: str + :param arg: The prefix to match + :return: The completion name, or None if not a match + :rtype: str + """ + if not file.startswith(treepath): + return None + if treepath != "": + file = file[len(treepath)+1:] + + if not file.startswith(arg): + return None + if os.path.isdir(file): + file += '/' + return file + +def iter_modified_file_completions(tree, arg): + """Returns a list of modified files that match the specified prefix. + + :param tree: The current tree + :type tree: `arch.WorkingTree` + :param arg: The prefix to match + :type arg: str + """ + treepath = cmdutil.tree_cwd(tree) + tmpdir = cmdutil.tmpdir() + changeset = tmpdir+"/changeset" + completions = [] + revision = cmdutil.determine_revision_tree(tree) + for line in arch.iter_delta(revision, tree, changeset): + if isinstance(line, arch.FileModification): + file = file_completion_match(line.name[1:], treepath, arg) + if file is not None: + completions.append(file) + shutil.rmtree(tmpdir) + return completions + +def iter_dir_completions(arg): + """Generate an iterator that iterates through directory name completions. + + :param arg: The directory name fragment to match + :type arg: str + """ + return iter_file_completions(arg, True) + +class Shell(BaseCommand): + def __init__(self): + self.description = "Runs Fai as a shell" + + def do_command(self, cmdargs): + if len(cmdargs)!=0: + raise cmdutil.GetHelp + prompt = PromptCmd() + try: + prompt.cmdloop() + finally: + prompt.write_history() + +class AddID(BaseCommand): + """ + Adds an inventory id for the given file + """ + def __init__(self): + self.description="Add an inventory id for a given file" + + def get_completer(self, arg, index): + tree = arch.tree_root() + return iter_untagged_completions(tree, arg) + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + + tree = arch.tree_root() + + if (len(args) == 0) == (options.untagged == False): + raise cmdutil.GetHelp + + #if options.id and len(args) != 1: + # print "If --id is specified, only one file can be named." + # return + + method = tree.tagging_method + + if options.id_type == "tagline": + if method != "tagline": + if not cmdutil.prompt("Tagline in other tree"): + if method == "explicit": + options.id_type == explicit + else: + print "add-id not supported for \"%s\" tagging method"\ + % method + return + + elif options.id_type == "explicit": + if method != "tagline" and method != explicit: + if not prompt("Explicit in other tree"): + print "add-id not supported for \"%s\" tagging method" % \ + method + return + + if options.id_type == "auto": + if method != "tagline" and method != "explicit": + print "add-id not supported for \"%s\" tagging method" % method + return + else: + options.id_type = method + if options.untagged: + args = None + self.add_ids(tree, options.id_type, args) + + def add_ids(self, tree, id_type, files=()): + """Add inventory ids to files. + + :param tree: the tree the files are in + :type tree: `arch.WorkingTree` + :param id_type: the type of id to add: "explicit" or "tagline" + :type id_type: str + :param files: The list of files to add. If None do all untagged. + :type files: tuple of str + """ + + untagged = (files is None) + if untagged: + files = list(iter_untagged(tree, None)) + previous_files = [] + while len(files) > 0: + previous_files.extend(files) + if id_type == "explicit": + cmdutil.add_id(files) + elif id_type == "tagline": + for file in files: + try: + cmdutil.add_tagline_or_explicit_id(file) + except cmdutil.AlreadyTagged: + print "\"%s\" already has a tagline." % file + except cmdutil.NoCommentSyntax: + pass + #do inventory after tagging until no untagged files are encountered + if untagged: + files = [] + for file in iter_untagged(tree, None): + if not file in previous_files: + files.append(file) + + else: + break + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai add-id file1 [file2] [file3]...") +# ddaa suggests removing this to promote GUIDs. Let's see who squalks. +# parser.add_option("-i", "--id", dest="id", +# help="Specify id for a single file", default=None) + parser.add_option("--tltl", action="store_true", + dest="lord_style", help="Use Tom Lord's style of id.") + parser.add_option("--explicit", action="store_const", + const="explicit", dest="id_type", + help="Use an explicit id", default="auto") + parser.add_option("--tagline", action="store_const", + const="tagline", dest="id_type", + help="Use a tagline id") + parser.add_option("--untagged", action="store_true", + dest="untagged", default=False, + help="tag all untagged files") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Adds an inventory to the specified file(s) and directories. If --untagged is +specified, adds inventory to all untagged files and directories. + """ + return + + +class Merge(BaseCommand): + """ + Merges changes from other versions into the current tree + """ + def __init__(self): + self.description="Merges changes from other versions" + try: + self.tree = arch.tree_root() + except: + self.tree = None + + + def get_completer(self, arg, index): + if self.tree is None: + raise arch.errors.TreeRootError + completions = list(ancillary.iter_partners(self.tree, + self.tree.tree_version)) + if len(completions) == 0: + completions = list(self.tree.iter_log_versions()) + + aliases = [] + try: + for completion in completions: + alias = ancillary.compact_alias(str(completion), self.tree) + if alias: + aliases.extend(alias) + + for completion in completions: + if completion.archive == self.tree.tree_version.archive: + aliases.append(completion.nonarch) + + except Exception, e: + print e + + completions.extend(aliases) + return completions + + def do_command(self, cmdargs): + """ + Master function that perfoms the "merge" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + if options.diff3: + action="star-merge" + else: + action = options.action + + if self.tree is None: + raise arch.errors.TreeRootError(os.getcwd()) + if cmdutil.has_changed(self.tree.tree_version): + raise UncommittedChanges(self.tree) + + if len(args) > 0: + revisions = [] + for arg in args: + revisions.append(cmdutil.determine_revision_arch(self.tree, + arg)) + source = "from commandline" + else: + revisions = ancillary.iter_partner_revisions(self.tree, + self.tree.tree_version) + source = "from partner version" + revisions = misc.rewind_iterator(revisions) + try: + revisions.next() + revisions.rewind() + except StopIteration, e: + revision = cmdutil.tag_cur(self.tree) + if revision is None: + raise CantDetermineRevision("", "No version specified, no " + "partner-versions, and no tag" + " source") + revisions = [revision] + source = "from tag source" + for revision in revisions: + cmdutil.ensure_archive_registered(revision.archive) + cmdutil.colorize(arch.Chatter("* Merging %s [%s]" % + (revision, source))) + if action=="native-merge" or action=="update": + if self.native_merge(revision, action) == 0: + continue + elif action=="star-merge": + try: + self.star_merge(revision, options.diff3) + except errors.MergeProblem, e: + break + if cmdutil.has_changed(self.tree.tree_version): + break + + def star_merge(self, revision, diff3): + """Perform a star-merge on the current tree. + + :param revision: The revision to use for the merge + :type revision: `arch.Revision` + :param diff3: If true, do a diff3 merge + :type diff3: bool + """ + try: + for line in self.tree.iter_star_merge(revision, diff3=diff3): + cmdutil.colorize(line) + except arch.util.ExecProblem, e: + if e.proc.status is not None and e.proc.status == 1: + if e.proc.error: + print e.proc.error + raise MergeProblem + else: + raise + + def native_merge(self, other_revision, action): + """Perform a native-merge on the current tree. + + :param other_revision: The revision to use for the merge + :type other_revision: `arch.Revision` + :return: 0 if the merge was skipped, 1 if it was applied + """ + other_tree = cmdutil.find_or_make_local_revision(other_revision) + try: + if action == "native-merge": + ancestor = cmdutil.merge_ancestor2(self.tree, other_tree, + other_revision) + elif action == "update": + ancestor = cmdutil.tree_latest(self.tree, + other_revision.version) + except CantDetermineRevision, e: + raise CommandFailedWrapper(e) + cmdutil.colorize(arch.Chatter("* Found common ancestor %s" % ancestor)) + if (ancestor == other_revision): + cmdutil.colorize(arch.Chatter("* Skipping redundant merge" + % ancestor)) + return 0 + delta = cmdutil.apply_delta(ancestor, other_tree, self.tree) + for line in cmdutil.iter_apply_delta_filter(delta): + cmdutil.colorize(line) + return 1 + + + + def get_parser(self): + """ + Returns the options parser to use for the "merge" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai merge [VERSION]") + parser.add_option("-s", "--star-merge", action="store_const", + dest="action", help="Use star-merge", + const="star-merge", default="native-merge") + parser.add_option("--update", action="store_const", + dest="action", help="Use update picker", + const="update") + parser.add_option("--diff3", action="store_true", + dest="diff3", + help="Use diff3 for merge (implies star-merge)") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Performs a merge operation using the specified version. + """ + return + +class ELog(BaseCommand): + """ + Produces a raw patchlog and invokes the user's editor + """ + def __init__(self): + self.description="Edit a patchlog to commit" + try: + self.tree = arch.tree_root() + except: + self.tree = None + + + def do_command(self, cmdargs): + """ + Master function that perfoms the "elog" command. + """ + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + if self.tree is None: + raise arch.errors.TreeRootError + + edit_log(self.tree) + + def get_parser(self): + """ + Returns the options parser to use for the "merge" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai elog") + return parser + + + def help(self, parser=None): + """ + Invokes $EDITOR to produce a log for committing. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Invokes $EDITOR to produce a log for committing. + """ + return + +def edit_log(tree): + """Makes and edits the log for a tree. Does all kinds of fancy things + like log templates and merge summaries and log-for-merge + + :param tree: The tree to edit the log for + :type tree: `arch.WorkingTree` + """ + #ensure we have an editor before preparing the log + cmdutil.find_editor() + log = tree.log_message(create=False) + log_is_new = False + if log is None or cmdutil.prompt("Overwrite log"): + if log is not None: + os.remove(log.name) + log = tree.log_message(create=True) + log_is_new = True + tmplog = log.name + template = tree+"/{arch}/=log-template" + if not os.path.exists(template): + template = os.path.expanduser("~/.arch-params/=log-template") + if not os.path.exists(template): + template = None + if template: + shutil.copyfile(template, tmplog) + + new_merges = list(cmdutil.iter_new_merges(tree, + tree.tree_version)) + log["Summary"] = merge_summary(new_merges, tree.tree_version) + if len(new_merges) > 0: + if cmdutil.prompt("Log for merge"): + mergestuff = cmdutil.log_for_merge(tree) + log.description += mergestuff + log.save() + try: + cmdutil.invoke_editor(log.name) + except: + if log_is_new: + os.remove(log.name) + raise + +def merge_summary(new_merges, tree_version): + if len(new_merges) == 0: + return "" + if len(new_merges) == 1: + summary = new_merges[0].summary + else: + summary = "Merge" + + credits = [] + for merge in new_merges: + if arch.my_id() != merge.creator: + name = re.sub("<.*>", "", merge.creator).rstrip(" "); + if not name in credits: + credits.append(name) + else: + version = merge.revision.version + if version.archive == tree_version.archive: + if not version.nonarch in credits: + credits.append(version.nonarch) + elif not str(version) in credits: + credits.append(str(version)) + + return ("%s (%s)") % (summary, ", ".join(credits)) + +class MirrorArchive(BaseCommand): + """ + Updates a mirror from an archive + """ + def __init__(self): + self.description="Update a mirror from an archive" + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + if len(args) > 1: + raise GetHelp + try: + tree = arch.tree_root() + except: + tree = None + + if len(args) == 0: + if tree is not None: + name = tree.tree_version() + else: + name = cmdutil.expand_alias(args[0], tree) + name = arch.NameParser(name) + + to_arch = name.get_archive() + from_arch = cmdutil.get_mirror_source(arch.Archive(to_arch)) + limit = name.get_nonarch() + + iter = arch_core.mirror_archive(from_arch,to_arch, limit) + for line in arch.chatter_classifier(iter): + cmdutil.colorize(line) + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai mirror-archive ARCHIVE") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Updates a mirror from an archive. If a branch, package, or version is +supplied, only changes under it are mirrored. + """ + return + +def help_tree_spec(): + print """Specifying revisions (default: tree) +Revisions may be specified by alias, revision, version or patchlevel. +Revisions or versions may be fully qualified. Unqualified revisions, versions, +or patchlevels use the archive of the current project tree. Versions will +use the latest patchlevel in the tree. Patchlevels will use the current tree- +version. + +Use "alias" to list available (user and automatic) aliases.""" + +def help_aliases(tree): + print """Auto-generated aliases + acur : The latest revision in the archive of the tree-version. You can specfy + a different version like so: acur:foo--bar--0 (aliases can be used) + tcur : (tree current) The latest revision in the tree of the tree-version. + You can specify a different version like so: tcur:foo--bar--0 (aliases + can be used). +tprev : (tree previous) The previous revision in the tree of the tree-version. + To specify an older revision, use a number, e.g. "tprev:4" + tanc : (tree ancestor) The ancestor revision of the tree + To specify an older revision, use a number, e.g. "tanc:4" +tdate : (tree date) The latest revision from a given date (e.g. "tdate:July 6") + tmod : (tree modified) The latest revision to modify a given file + (e.g. "tmod:engine.cpp" or "tmod:engine.cpp:16") + ttag : (tree tag) The revision that was tagged into the current tree revision, + according to the tree. +tagcur: (tag current) The latest revision of the version that the current tree + was tagged from. +mergeanc : The common ancestor of the current tree and the specified revision. + Defaults to the first partner-version's latest revision or to tagcur. + """ + print "User aliases" + for parts in ancillary.iter_all_alias(tree): + print parts[0].rjust(10)+" : "+parts[1] + + +class Inventory(BaseCommand): + """List the status of files in the tree""" + def __init__(self): + self.description=self.__doc__ + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + tree = arch.tree_root() + categories = [] + + if (options.source): + categories.append(arch_core.SourceFile) + if (options.precious): + categories.append(arch_core.PreciousFile) + if (options.backup): + categories.append(arch_core.BackupFile) + if (options.junk): + categories.append(arch_core.JunkFile) + + if len(categories) == 1: + show_leading = False + else: + show_leading = True + + if len(categories) == 0: + categories = None + + if options.untagged: + categories = arch_core.non_root + show_leading = False + tagged = False + else: + tagged = None + + for file in arch_core.iter_inventory_filter(tree, None, + control_files=options.control_files, + categories = categories, tagged=tagged): + print arch_core.file_line(file, + category = show_leading, + untagged = show_leading, + id = options.ids) + + def get_parser(self): + """ + Returns the options parser to use for the "revision" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai inventory [options]") + parser.add_option("--ids", action="store_true", dest="ids", + help="Show file ids") + parser.add_option("--control", action="store_true", + dest="control_files", help="include control files") + parser.add_option("--source", action="store_true", dest="source", + help="List source files") + parser.add_option("--backup", action="store_true", dest="backup", + help="List backup files") + parser.add_option("--precious", action="store_true", dest="precious", + help="List precious files") + parser.add_option("--junk", action="store_true", dest="junk", + help="List junk files") + parser.add_option("--unrecognized", action="store_true", + dest="unrecognized", help="List unrecognized files") + parser.add_option("--untagged", action="store_true", + dest="untagged", help="List only untagged files") + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Lists the status of files in the archive: +S source +P precious +B backup +J junk +U unrecognized +T tree root +? untagged-source +Leading letter are not displayed if only one kind of file is shown + """ + return + + +class Alias(BaseCommand): + """List or adjust aliases""" + def __init__(self): + self.description=self.__doc__ + + def get_completer(self, arg, index): + if index > 2: + return () + try: + self.tree = arch.tree_root() + except: + self.tree = None + + if index == 0: + return [part[0]+" " for part in ancillary.iter_all_alias(self.tree)] + elif index == 1: + return cmdutil.iter_revision_completions(arg, self.tree) + + + def do_command(self, cmdargs): + """ + Master function that perfoms the "revision" command. + """ + + parser=self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: + self.tree = arch.tree_root() + except: + self.tree = None + + + try: + options.action(args, options) + except cmdutil.ForbiddenAliasSyntax, e: + raise CommandFailedWrapper(e) + + def arg_dispatch(self, args, options): + """Add, modify, or list aliases, depending on number of arguments + + :param args: The list of commandline arguments + :type args: list of str + :param options: The commandline options + """ + if len(args) == 0: + help_aliases(self.tree) + return + elif len(args) == 1: + self.print_alias(args[0]) + elif (len(args)) == 2: + self.add(args[0], args[1], options) + else: + raise cmdutil.GetHelp + + def print_alias(self, alias): + answer = None + for pair in ancillary.iter_all_alias(self.tree): + if pair[0] == alias: + answer = pair[1] + if answer is not None: + print answer + else: + print "The alias %s is not assigned." % alias + + def add(self, alias, expansion, options): + """Add or modify aliases + + :param alias: The alias name to create/modify + :type alias: str + :param expansion: The expansion to assign to the alias name + :type expansion: str + :param options: The commandline options + """ + newlist = "" + written = False + new_line = "%s=%s\n" % (alias, cmdutil.expand_alias(expansion, + self.tree)) + ancillary.check_alias(new_line.rstrip("\n"), [alias, expansion]) + + for pair in self.get_iterator(options): + if pair[0] != alias: + newlist+="%s=%s\n" % (pair[0], pair[1]) + elif not written: + newlist+=new_line + written = True + if not written: + newlist+=new_line + self.write_aliases(newlist, options) + + def delete(self, args, options): + """Delete the specified alias + + :param args: The list of arguments + :type args: list of str + :param options: The commandline options + """ + deleted = False + if len(args) != 1: + raise cmdutil.GetHelp + newlist = "" + for pair in self.get_iterator(options): + if pair[0] != args[0]: + newlist+="%s=%s\n" % (pair[0], pair[1]) + else: + deleted = True + if not deleted: + raise errors.NoSuchAlias(args[0]) + self.write_aliases(newlist, options) + + def get_alias_file(self, options): + """Return the name of the alias file to use + + :param options: The commandline options + """ + if options.tree: + if self.tree is None: + self.tree == arch.tree_root() + return str(self.tree)+"/{arch}/+aliases" + else: + return "~/.aba/aliases" + + def get_iterator(self, options): + """Return the alias iterator to use + + :param options: The commandline options + """ + return ancillary.iter_alias(self.get_alias_file(options)) + + def write_aliases(self, newlist, options): + """Safely rewrite the alias file + :param newlist: The new list of aliases + :type newlist: str + :param options: The commandline options + """ + filename = os.path.expanduser(self.get_alias_file(options)) + file = cmdutil.NewFileVersion(filename) + file.write(newlist) + file.commit() + + + def get_parser(self): + """ + Returns the options parser to use for the "alias" command. + + :rtype: cmdutil.CmdOptionParser + """ + parser=cmdutil.CmdOptionParser("fai alias [ALIAS] [NAME]") + parser.add_option("-d", "--delete", action="store_const", dest="action", + const=self.delete, default=self.arg_dispatch, + help="Delete an alias") + parser.add_option("--tree", action="store_true", dest="tree", + help="Create a per-tree alias", default=False) + return parser + + def help(self, parser=None): + """ + Prints a help message. + + :param parser: If supplied, the parser to use for generating help. If \ + not supplied, it is retrieved. + :type parser: cmdutil.CmdOptionParser + """ + if parser==None: + parser=self.get_parser() + parser.print_help() + print """ +Lists current aliases or modifies the list of aliases. + +If no arguments are supplied, aliases will be listed. If two arguments are +supplied, the specified alias will be created or modified. If -d or --delete +is supplied, the specified alias will be deleted. + +You can create aliases that refer to any fully-qualified part of the +Arch namespace, e.g. +archive, +archive/category, +archive/category--branch, +archive/category--branch--version (my favourite) +archive/category--branch--version--patchlevel + +Aliases can be used automatically by native commands. To use them +with external or tla commands, prefix them with ^ (you can do this +with native commands, too). +""" + + +class RequestMerge(BaseCommand): + """Submit a merge request to Bug Goo""" + def __init__(self): + self.description=self.__doc__ + + def do_command(self, cmdargs): + """Submit a merge request + + :param cmdargs: The commandline arguments + :type cmdargs: list of str + """ + cmdutil.find_editor() + parser = self.get_parser() + (options, args) = parser.parse_args(cmdargs) + try: + self.tree=arch.tree_root() + except: + self.tree=None + base, revisions = self.revision_specs(args) + message = self.make_headers(base, revisions) + message += self.make_summary(revisions) + path = self.edit_message(message) + message = self.tidy_message(path) + if cmdutil.prompt("Send merge"): + self.send_message(message) + print "Merge request sent" + + def make_headers(self, base, revisions): + """Produce email and Bug Goo header strings + + :param base: The base revision to apply merges to + :type base: `arch.Revision` + :param revisions: The revisions to replay into the base + :type revisions: list of `arch.Patchlog` + :return: The headers + :rtype: str + """ + headers = "To: gnu-arch-users@gnu.org\n" + headers += "From: %s\n" % options.fromaddr + if len(revisions) == 1: + headers += "Subject: [MERGE REQUEST] %s\n" % revisions[0].summary + else: + headers += "Subject: [MERGE REQUEST]\n" + headers += "\n" + headers += "Base-Revision: %s\n" % base + for revision in revisions: + headers += "Revision: %s\n" % revision.revision + headers += "Bug: \n\n" + return headers + + def make_summary(self, logs): + """Generate a summary of merges + + :param logs: the patchlogs that were directly added by the merges + :type logs: list of `arch.Patchlog` + :return: the summary + :rtype: str + """ + summary = "" + for log in logs: + summary+=str(log.revision)+"\n" + summary+=log.summary+"\n" + if log.description.strip(): + summary+=log.description.strip('\n')+"\n\n" + return summary + + def revision_specs(self, args): + """Determine the base and merge revisions from tree and arguments. + + :param args: The parsed arguments + :type args: list of str + :return: The base revision and merge revisions + :rtype: `arch.Revision`, list of `arch.Patchlog` + """ + if len(args) > 0: + target_revision = cmdutil.determine_revision_arch(self.tree, + args[0]) + else: + target_revision = cmdutil.tree_latest(self.tree) + if len(args) > 1: + merges = [ arch.Patchlog(cmdutil.determine_revision_arch( + self.tree, f)) for f in args[1:] ] + else: + if self.tree is None: + raise CantDetermineRevision("", "Not in a project tree") + merge_iter = cmdutil.iter_new_merges(self.tree, + target_revision.version, + False) + merges = [f for f in cmdutil.direct_merges(merge_iter)] + return (target_revision, merges) + + def edit_message(self, message): + """Edit an email message in the user's standard editor + + :param message: The message to edit + :type message: str + :return: the path of the edited message + :rtype: str + """ + if self.tree is None: + path = os.get_cwd() + else: + path = self.tree + path += "/,merge-request" + file = open(path, 'w') + file.write(message) + file.flush() + cmdutil.invoke_editor(path) + return path + + def tidy_message(self, path): + """Validate and clean up message. + + :param path: The path to the message to clean up + :type path: str + :return: The parsed message + :rtype: `email.Message` + """ + mail = email.message_from_file(open(path)) + if mail["Subject"].strip() == "[MERGE REQUEST]": + raise BlandSubject + + request = email.message_from_string(mail.get_payload()) + if request.has_key("Bug"): + if request["Bug"].strip()=="": + del request["Bug"] + mail.set_payload(request.as_string()) + return mail + + def send_message(self, message): + """Send a message, using its headers to address it. + + :param message: The message to send + :type message: `email.Message`""" + server = smtplib.SMTP() + server.sendmail(message['From'], message['To'], message.as_string()) + server.quit() + + def help(self, parser=None): + """Print a usage message + + :param parser: The options parser to use + :type parser: `cmdutil.CmdOptionParser` + """ + if parser is None: + parser = self.get_parser() + parser.print_help() + print """ +Sends a merge request formatted for Bug Goo. Intended use: get the tree +you'd like to merge into. Apply the merges you want. Invoke request-merge. +The merge request will open in your $EDITOR. + +When no TARGET is specified, it uses the current tree revision. When +no MERGE is specified, it uses the direct merges (as in "revisions +--direct-merges"). But you can specify just the TARGET, or all the MERGE +revisions. +""" + + def get_parser(self): + """Produce a commandline parser for this command. + + :rtype: `cmdutil.CmdOptionParser` + """ + parser=cmdutil.CmdOptionParser("request-merge [TARGET] [MERGE1...]") + return parser + +commands = { +'changes' : Changes, +'help' : Help, +'update': Update, +'apply-changes':ApplyChanges, +'cat-log': CatLog, +'commit': Commit, +'revision': Revision, +'revisions': Revisions, +'get': Get, +'revert': Revert, +'shell': Shell, +'add-id': AddID, +'merge': Merge, +'elog': ELog, +'mirror-archive': MirrorArchive, +'ninventory': Inventory, +'alias' : Alias, +'request-merge': RequestMerge, +} +suggestions = { +'apply-delta' : "Try \"apply-changes\".", +'delta' : "To compare two revisions, use \"changes\".", +'diff-rev' : "To compare two revisions, use \"changes\".", +'undo' : "To undo local changes, use \"revert\".", +'undelete' : "To undo only deletions, use \"revert --deletions\"", +'missing-from' : "Try \"revisions --missing-from\".", +'missing' : "Try \"revisions --missing\".", +'missing-merge' : "Try \"revisions --partner-missing\".", +'new-merges' : "Try \"revisions --new-merges\".", +'cachedrevs' : "Try \"revisions --cacherevs\". (no 'd')", +'logs' : "Try \"revisions --logs\"", +'tree-source' : "Use the \"^ttag\" alias (\"revision ^ttag\")", +'latest-revision' : "Use the \"^acur\" alias (\"revision ^acur\")", +'change-version' : "Try \"update REVISION\"", +'tree-revision' : "Use the \"^tcur\" alias (\"revision ^tcur\")", +'rev-depends' : "Use revisions --dependencies", +'auto-get' : "Plain get will do archive lookups", +'tagline' : "Use add-id. It uses taglines in tagline trees", +'emlog' : "Use elog. It automatically adds log-for-merge text, if any", +'library-revisions' : "Use revisions --library", +'file-revert' : "Use revert FILE" +} +# arch-tag: 19d5739d-3708-486c-93ba-deecc3027fc7 *** modified file 'bzrlib/branch.py' --- bzrlib/branch.py +++ bzrlib/branch.py @@ -31,6 +31,8 @@ from revision import Revision from errors import bailout, BzrError from textui import show_status +import patches +from bzrlib import progress BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? @@ -864,3 +866,36 @@ s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) + + +def iter_anno_data(branch, file_id): + later_revision = branch.revno() + q = range(branch.revno()) + q.reverse() + later_text_id = branch.basis_tree().inventory[file_id].text_id + i = 0 + for revno in q: + i += 1 + cur_tree = branch.revision_tree(branch.lookup_revision(revno)) + if file_id not in cur_tree.inventory: + text_id = None + else: + text_id = cur_tree.inventory[file_id].text_id + if text_id != later_text_id: + patch = get_patch(branch, revno, later_revision, file_id) + yield revno, patch.iter_inserted(), patch + later_revision = revno + later_text_id = text_id + yield progress.Progress("revisions", i) + +def get_patch(branch, old_revno, new_revno, file_id): + old_tree = branch.revision_tree(branch.lookup_revision(old_revno)) + new_tree = branch.revision_tree(branch.lookup_revision(new_revno)) + if file_id in old_tree.inventory: + old_file = old_tree.get_file(file_id).readlines() + else: + old_file = [] + ud = difflib.unified_diff(old_file, new_tree.get_file(file_id).readlines()) + return patches.parse_patch(ud) + + *** modified file 'bzrlib/commands.py' --- bzrlib/commands.py +++ bzrlib/commands.py @@ -27,6 +27,9 @@ from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \ format_date from bzrlib import merge +from bzrlib.branch import iter_anno_data +from bzrlib import patches +from bzrlib import progress def _squish_command_name(cmd): @@ -882,7 +885,15 @@ print '%3d FAILED!' % mf else: print - + result = bzrlib.patches.test() + resultFailed = len(result.errors) + len(result.failures) + print '%-40s %3d tests' % ('bzrlib.patches', result.testsRun), + if resultFailed: + print '%3d FAILED!' % resultFailed + else: + print + tests += result.testsRun + failures += resultFailed print '%-40s %3d tests' % ('total', tests), if failures: print '%3d FAILED!' % failures @@ -897,6 +908,34 @@ """Show version of bzr""" def run(self): show_version() + +class cmd_annotate(Command): + """Show which revision added each line in a file""" + + takes_args = ['filename'] + def run(self, filename): + if not os.path.exists(filename): + raise BzrCommandError("The file %s does not exist." % filename) + branch = (Branch(filename)) + file_id = branch.working_tree().path2id(filename) + if file_id is None: + raise BzrCommandError("The file %s is not versioned." % filename) + lines = branch.basis_tree().get_file(file_id) + total = branch.revno() + anno_d_iter = iter_anno_data(branch, file_id) + progress_bar = progress.ProgressBar() + try: + for result in patches.iter_annotate_file(lines, anno_d_iter): + if isinstance(result, progress.Progress): + result.total = total + progress_bar(result) + else: + anno_lines = result + finally: + progress.clear_progress_bar() + for line in anno_lines: + sys.stdout.write("%4s:%s" % (str(line.log), line.text)) + def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ M 644 inline patches/progress.diff data 4914 *** added file 'bzrlib/progress.py' --- /dev/null +++ bzrlib/progress.py @@ -0,0 +1,138 @@ +# Copyright (C) 2005 Aaron Bentley +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import sys +import datetime + +class Progress(object): + def __init__(self, units, current, total=None): + self.units = units + self.current = current + self.total = total + + def _get_percent(self): + if self.total is not None and self.current is not None: + return 100.0 * self.current / self.total + + percent = property(_get_percent) + + def __str__(self): + if self.total is not None: + return "%i of %i %s %.1f%%" % (self.current, self.total, self.units, + self.percent) + else: + return "%i %s" (self.current, self.units) + +class ProgressBar(object): + def __init__(self): + self.start = None + object.__init__(self) + + def __call__(self, progress): + if self.start is None: + self.start = datetime.datetime.now() + progress_bar(progress, start_time=self.start) + +def divide_timedelta(delt, divisor): + """Divides a timedelta object""" + return datetime.timedelta(float(delt.days)/divisor, + float(delt.seconds)/divisor, + float(delt.microseconds)/divisor) + +def str_tdelta(delt): + if delt is None: + return "-:--:--" + return str(datetime.timedelta(delt.days, delt.seconds)) + +def get_eta(start_time, progress, enough_samples=20): + if start_time is None or progress.current == 0: + return None + elif progress.current < enough_samples: + return None + elapsed = datetime.datetime.now() - start_time + total_duration = divide_timedelta((elapsed) * long(progress.total), + progress.current) + if elapsed < total_duration: + eta = total_duration - elapsed + else: + eta = total_duration - total_duration + return eta + +def progress_bar(progress, start_time=None): + eta = get_eta(start_time, progress) + if start_time is not None: + eta_str = " "+str_tdelta(eta) + else: + eta_str = "" + + fmt = " %i of %i %s (%.1f%%)" + f = fmt % (progress.total, progress.total, progress.units, 100.0) + max = len(f) + cols = 77 - max + if start_time is not None: + cols -= len(eta_str) + markers = int (float(cols) * progress.current / progress.total) + txt = fmt % (progress.current, progress.total, progress.units, + progress.percent) + sys.stderr.write("\r[%s%s]%s%s" % ('='*markers, ' '*(cols-markers), txt, + eta_str)) + +def clear_progress_bar(): + sys.stderr.write('\r%s\r' % (' '*79)) + +def spinner_str(progress, show_text=False): + """ + Produces the string for a textual "spinner" progress indicator + :param progress: an object represinting current progress + :param show_text: If true, show progress text as well + :return: The spinner string + + >>> spinner_str(Progress("baloons", 0)) + '|' + >>> spinner_str(Progress("baloons", 5)) + '/' + >>> spinner_str(Progress("baloons", 6), show_text=True) + '- 6 baloons' + """ + positions = ('|', '/', '-', '\\') + text = positions[progress.current % 4] + if show_text: + text+=" %i %s" % (progress.current, progress.units) + return text + +def spinner(progress, show_text=False, output=sys.stderr): + """ + Update a spinner progress indicator on an output + :param progress: The progress to display + :param show_text: If true, show text as well as spinner + :param output: The output to write to + + >>> spinner(Progress("baloons", 6), show_text=True, output=sys.stdout) + \r- 6 baloons + """ + output.write('\r%s' % spinner_str(progress, show_text)) + +def run_tests(): + import doctest + result = doctest.testmod() + if result[1] > 0: + if result[0] == 0: + print "All tests passed" + else: + print "No tests to run" +if __name__ == "__main__": + run_tests() commit refs/heads/master mark :648 committer Martin Pool 1118386935 +1000 data 40 - import aaron's progress-indicator code from :647 M 644 inline bzrlib/progress.py data 4682 # Copyright (C) 2005 Aaron Bentley # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys import datetime class Progress(object): def __init__(self, units, current, total=None): self.units = units self.current = current self.total = total def _get_percent(self): if self.total is not None and self.current is not None: return 100.0 * self.current / self.total percent = property(_get_percent) def __str__(self): if self.total is not None: return "%i of %i %s %.1f%%" % (self.current, self.total, self.units, self.percent) else: return "%i %s" (self.current, self.units) class ProgressBar(object): def __init__(self): self.start = None object.__init__(self) def __call__(self, progress): if self.start is None: self.start = datetime.datetime.now() progress_bar(progress, start_time=self.start) def divide_timedelta(delt, divisor): """Divides a timedelta object""" return datetime.timedelta(float(delt.days)/divisor, float(delt.seconds)/divisor, float(delt.microseconds)/divisor) def str_tdelta(delt): if delt is None: return "-:--:--" return str(datetime.timedelta(delt.days, delt.seconds)) def get_eta(start_time, progress, enough_samples=20): if start_time is None or progress.current == 0: return None elif progress.current < enough_samples: return None elapsed = datetime.datetime.now() - start_time total_duration = divide_timedelta((elapsed) * long(progress.total), progress.current) if elapsed < total_duration: eta = total_duration - elapsed else: eta = total_duration - total_duration return eta def progress_bar(progress, start_time=None): eta = get_eta(start_time, progress) if start_time is not None: eta_str = " "+str_tdelta(eta) else: eta_str = "" fmt = " %i of %i %s (%.1f%%)" f = fmt % (progress.total, progress.total, progress.units, 100.0) max = len(f) cols = 77 - max if start_time is not None: cols -= len(eta_str) markers = int (float(cols) * progress.current / progress.total) txt = fmt % (progress.current, progress.total, progress.units, progress.percent) sys.stderr.write("\r[%s%s]%s%s" % ('='*markers, ' '*(cols-markers), txt, eta_str)) def clear_progress_bar(): sys.stderr.write('\r%s\r' % (' '*79)) def spinner_str(progress, show_text=False): """ Produces the string for a textual "spinner" progress indicator :param progress: an object represinting current progress :param show_text: If true, show progress text as well :return: The spinner string >>> spinner_str(Progress("baloons", 0)) '|' >>> spinner_str(Progress("baloons", 5)) '/' >>> spinner_str(Progress("baloons", 6), show_text=True) '- 6 baloons' """ positions = ('|', '/', '-', '\\') text = positions[progress.current % 4] if show_text: text+=" %i %s" % (progress.current, progress.units) return text def spinner(progress, show_text=False, output=sys.stderr): """ Update a spinner progress indicator on an output :param progress: The progress to display :param show_text: If true, show text as well as spinner :param output: The output to write to >>> spinner(Progress("baloons", 6), show_text=True, output=sys.stdout) \r- 6 baloons """ output.write('\r%s' % spinner_str(progress, show_text)) def run_tests(): import doctest result = doctest.testmod() if result[1] > 0: if result[0] == 0: print "All tests passed" else: print "No tests to run" if __name__ == "__main__": run_tests() commit refs/heads/master mark :649 committer Martin Pool 1118387425 +1000 data 42 - some cleanups for the progressbar method from :648 M 644 inline bzrlib/progress.py data 6067 # Copyright (C) 2005 Aaron Bentley # Copyright (C) 2005 Canonical # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ Simple text-mode progress indicator. Everyone loves ascii art! To display an indicator, create a ProgressBar object. Call it, passing Progress objects indicating the current state. When done, call clear(). Progress is suppressed when output is not sent to a terminal, so as not to clutter log files. """ # TODO: remove functions in favour of keeping everything in one class import sys import datetime def _width(): """Return estimated terminal width. TODO: Do something smart on Windows? TODO: Is there anything that gets a better update when the window is resized while the program is running? """ import os try: return int(os.environ['COLUMNS']) except (IndexError, KeyError, ValueError): return 80 def _supports_progress(f): return hasattr(f, 'isatty') and f.isatty() class Progress(object): def __init__(self, units, current, total=None): self.units = units self.current = current self.total = total def _get_percent(self): if self.total is not None and self.current is not None: return 100.0 * self.current / self.total percent = property(_get_percent) def __str__(self): if self.total is not None: return "%i of %i %s %.1f%%" % (self.current, self.total, self.units, self.percent) else: return "%i %s" (self.current, self.units) class ProgressBar(object): def __init__(self, to_file=sys.stderr): object.__init__(self) self.start = None self.to_file = to_file self.suppressed = not _supports_progress(self.to_file) def __call__(self, progress): if self.start is None: self.start = datetime.datetime.now() if not self.suppressed: draw_progress_bar(progress, start_time=self.start, to_file=self.to_file) def clear(self): if not self.suppressed: clear_progress_bar(self.to_file) def divide_timedelta(delt, divisor): """Divides a timedelta object""" return datetime.timedelta(float(delt.days)/divisor, float(delt.seconds)/divisor, float(delt.microseconds)/divisor) def str_tdelta(delt): if delt is None: return "-:--:--" return str(datetime.timedelta(delt.days, delt.seconds)) def get_eta(start_time, progress, enough_samples=20): if start_time is None or progress.current == 0: return None elif progress.current < enough_samples: return None elapsed = datetime.datetime.now() - start_time total_duration = divide_timedelta((elapsed) * long(progress.total), progress.current) if elapsed < total_duration: eta = total_duration - elapsed else: eta = total_duration - total_duration return eta def draw_progress_bar(progress, start_time=None, to_file=sys.stderr): eta = get_eta(start_time, progress) if start_time is not None: eta_str = " "+str_tdelta(eta) else: eta_str = "" fmt = " %i of %i %s (%.1f%%)" f = fmt % (progress.total, progress.total, progress.units, 100.0) cols = _width() - 3 - len(f) if start_time is not None: cols -= len(eta_str) markers = int (float(cols) * progress.current / progress.total) txt = fmt % (progress.current, progress.total, progress.units, progress.percent) to_file.write("\r[%s%s]%s%s" % ('='*markers, ' '*(cols-markers), txt, eta_str)) def clear_progress_bar(to_file=sys.stderr): to_file.write('\r%s\r' % (' '*79)) def spinner_str(progress, show_text=False): """ Produces the string for a textual "spinner" progress indicator :param progress: an object represinting current progress :param show_text: If true, show progress text as well :return: The spinner string >>> spinner_str(Progress("baloons", 0)) '|' >>> spinner_str(Progress("baloons", 5)) '/' >>> spinner_str(Progress("baloons", 6), show_text=True) '- 6 baloons' """ positions = ('|', '/', '-', '\\') text = positions[progress.current % 4] if show_text: text+=" %i %s" % (progress.current, progress.units) return text def spinner(progress, show_text=False, output=sys.stderr): """ Update a spinner progress indicator on an output :param progress: The progress to display :param show_text: If true, show text as well as spinner :param output: The output to write to >>> spinner(Progress("baloons", 6), show_text=True, output=sys.stdout) \r- 6 baloons """ output.write('\r%s' % spinner_str(progress, show_text)) def run_tests(): import doctest result = doctest.testmod() if result[1] > 0: if result[0] == 0: print "All tests passed" else: print "No tests to run" def demo(): from time import sleep pb = ProgressBar() for i in range(100): pb(Progress('Elephanten', i, 100)) sleep(0.3) print 'done!' if __name__ == "__main__": demo() commit refs/heads/master mark :650 committer Martin Pool 1118387487 +1000 data 43 - remove calls to bailout() from check code from :649 M 644 inline bzrlib/check.py data 4409 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ###################################################################### # consistency checks import sys from trace import mutter from bzrlib.errors import BzrCheckError import osutils def check(branch, progress=True): out = sys.stdout # TODO: factor out if not (hasattr(out, 'isatty') and out.isatty()): progress=False if progress: def p(m): mutter('checking ' + m) out.write('\rchecking: %-50.50s' % m) out.flush() else: def p(m): mutter('checking ' + m) p('history of %r' % branch.base) last_ptr = None checked_revs = {} history = branch.revision_history() revno = 0 revcount = len(history) checked_texts = {} for rid in history: revno += 1 p('revision %d/%d' % (revno, revcount)) mutter(' revision {%s}' % rid) rev = branch.get_revision(rid) if rev.revision_id != rid: raise BzrCheckError('wrong internal revision id in revision {%s}' % rid) if rev.precursor != last_ptr: raise BzrCheckError('mismatched precursor in revision {%s}' % rid) last_ptr = rid if rid in checked_revs: raise BzrCheckError('repeated revision {%s}' % rid) checked_revs[rid] = True ## TODO: Check all the required fields are present on the revision. inv = branch.get_inventory(rev.inventory_id) seen_ids = {} seen_names = {} p('revision %d/%d file ids' % (revno, revcount)) for file_id in inv: if file_id in seen_ids: raise BzrCheckError('duplicated file_id {%s} in inventory for revision {%s}' % (file_id, rid)) seen_ids[file_id] = True i = 0 len_inv = len(inv) for file_id in inv: i += 1 if (i % 100) == 0: p('revision %d/%d file text %d/%d' % (revno, revcount, i, len_inv)) ie = inv[file_id] if ie.parent_id != None: if ie.parent_id not in seen_ids: raise BzrCheckError('missing parent {%s} in inventory for revision {%s}' % (ie.parent_id, rid)) if ie.kind == 'file': if ie.text_id in checked_texts: fp = checked_texts[ie.text_id] else: if not ie.text_id in branch.text_store: raise BzrCheckError('text {%s} not in text_store' % ie.text_id) tf = branch.text_store[ie.text_id] fp = osutils.fingerprint_file(tf) checked_texts[ie.text_id] = fp if ie.text_size != fp['size']: raise BzrCheckError('text {%s} wrong size' % ie.text_id) if ie.text_sha1 != fp['sha1']: raise BzrCheckError('text {%s} wrong sha1' % ie.text_id) elif ie.kind == 'directory': if ie.text_sha1 != None or ie.text_size != None or ie.text_id != None: raise BzrCheckError('directory {%s} has text in revision {%s}' % (file_id, rid)) p('revision %d/%d file paths' % (revno, revcount)) for path, ie in inv.iter_entries(): if path in seen_names: raise BzrCheckError('duplicated path %r in inventory for revision {%s}' % (path, revid)) seen_names[path] = True p('done') if progress: print print 'checked %d revisions, %d file texts' % (revcount, len(checked_texts)) commit refs/heads/master mark :651 committer Martin Pool 1118387567 +1000 data 18 - clean up imports from :650 M 644 inline bzrlib/check.py data 4457 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ###################################################################### # consistency checks def check(branch, progress=True): import sys from bzrlib.trace import mutter from bzrlib.errors import BzrCheckError from bzrlib.osutils import fingerprint_file out = sys.stdout # TODO: factor out if not (hasattr(out, 'isatty') and out.isatty()): progress=False if progress: def p(m): mutter('checking ' + m) out.write('\rchecking: %-50.50s' % m) out.flush() else: def p(m): mutter('checking ' + m) p('history of %r' % branch.base) last_ptr = None checked_revs = {} history = branch.revision_history() revno = 0 revcount = len(history) checked_texts = {} for rid in history: revno += 1 p('revision %d/%d' % (revno, revcount)) mutter(' revision {%s}' % rid) rev = branch.get_revision(rid) if rev.revision_id != rid: raise BzrCheckError('wrong internal revision id in revision {%s}' % rid) if rev.precursor != last_ptr: raise BzrCheckError('mismatched precursor in revision {%s}' % rid) last_ptr = rid if rid in checked_revs: raise BzrCheckError('repeated revision {%s}' % rid) checked_revs[rid] = True ## TODO: Check all the required fields are present on the revision. inv = branch.get_inventory(rev.inventory_id) seen_ids = {} seen_names = {} p('revision %d/%d file ids' % (revno, revcount)) for file_id in inv: if file_id in seen_ids: raise BzrCheckError('duplicated file_id {%s} in inventory for revision {%s}' % (file_id, rid)) seen_ids[file_id] = True i = 0 len_inv = len(inv) for file_id in inv: i += 1 if (i % 100) == 0: p('revision %d/%d file text %d/%d' % (revno, revcount, i, len_inv)) ie = inv[file_id] if ie.parent_id != None: if ie.parent_id not in seen_ids: raise BzrCheckError('missing parent {%s} in inventory for revision {%s}' % (ie.parent_id, rid)) if ie.kind == 'file': if ie.text_id in checked_texts: fp = checked_texts[ie.text_id] else: if not ie.text_id in branch.text_store: raise BzrCheckError('text {%s} not in text_store' % ie.text_id) tf = branch.text_store[ie.text_id] fp = fingerprint_file(tf) checked_texts[ie.text_id] = fp if ie.text_size != fp['size']: raise BzrCheckError('text {%s} wrong size' % ie.text_id) if ie.text_sha1 != fp['sha1']: raise BzrCheckError('text {%s} wrong sha1' % ie.text_id) elif ie.kind == 'directory': if ie.text_sha1 != None or ie.text_size != None or ie.text_id != None: raise BzrCheckError('directory {%s} has text in revision {%s}' % (file_id, rid)) p('revision %d/%d file paths' % (revno, revcount)) for path, ie in inv.iter_entries(): if path in seen_names: raise BzrCheckError('duplicated path %r in inventory for revision {%s}' % (path, revid)) seen_names[path] = True p('done') if progress: print print 'checked %d revisions, %d file texts' % (revcount, len(checked_texts)) commit refs/heads/master mark :652 committer Martin Pool 1118387613 +1000 data 3 doc from :651 M 644 inline bzrlib/progress.py data 6230 # Copyright (C) 2005 Aaron Bentley # Copyright (C) 2005 Canonical # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ Simple text-mode progress indicator. Everyone loves ascii art! To display an indicator, create a ProgressBar object. Call it, passing Progress objects indicating the current state. When done, call clear(). Progress is suppressed when output is not sent to a terminal, so as not to clutter log files. """ # TODO: remove functions in favour of keeping everything in one class # TODO: should be a global option e.g. --silent that disables progress # indicators, preferably without needing to adjust all code that # potentially calls them. import sys import datetime def _width(): """Return estimated terminal width. TODO: Do something smart on Windows? TODO: Is there anything that gets a better update when the window is resized while the program is running? """ import os try: return int(os.environ['COLUMNS']) except (IndexError, KeyError, ValueError): return 80 def _supports_progress(f): return hasattr(f, 'isatty') and f.isatty() class Progress(object): def __init__(self, units, current, total=None): self.units = units self.current = current self.total = total def _get_percent(self): if self.total is not None and self.current is not None: return 100.0 * self.current / self.total percent = property(_get_percent) def __str__(self): if self.total is not None: return "%i of %i %s %.1f%%" % (self.current, self.total, self.units, self.percent) else: return "%i %s" (self.current, self.units) class ProgressBar(object): def __init__(self, to_file=sys.stderr): object.__init__(self) self.start = None self.to_file = to_file self.suppressed = not _supports_progress(self.to_file) def __call__(self, progress): if self.start is None: self.start = datetime.datetime.now() if not self.suppressed: draw_progress_bar(progress, start_time=self.start, to_file=self.to_file) def clear(self): if not self.suppressed: clear_progress_bar(self.to_file) def divide_timedelta(delt, divisor): """Divides a timedelta object""" return datetime.timedelta(float(delt.days)/divisor, float(delt.seconds)/divisor, float(delt.microseconds)/divisor) def str_tdelta(delt): if delt is None: return "-:--:--" return str(datetime.timedelta(delt.days, delt.seconds)) def get_eta(start_time, progress, enough_samples=20): if start_time is None or progress.current == 0: return None elif progress.current < enough_samples: return None elapsed = datetime.datetime.now() - start_time total_duration = divide_timedelta((elapsed) * long(progress.total), progress.current) if elapsed < total_duration: eta = total_duration - elapsed else: eta = total_duration - total_duration return eta def draw_progress_bar(progress, start_time=None, to_file=sys.stderr): eta = get_eta(start_time, progress) if start_time is not None: eta_str = " "+str_tdelta(eta) else: eta_str = "" fmt = " %i of %i %s (%.1f%%)" f = fmt % (progress.total, progress.total, progress.units, 100.0) cols = _width() - 3 - len(f) if start_time is not None: cols -= len(eta_str) markers = int (float(cols) * progress.current / progress.total) txt = fmt % (progress.current, progress.total, progress.units, progress.percent) to_file.write("\r[%s%s]%s%s" % ('='*markers, ' '*(cols-markers), txt, eta_str)) def clear_progress_bar(to_file=sys.stderr): to_file.write('\r%s\r' % (' '*79)) def spinner_str(progress, show_text=False): """ Produces the string for a textual "spinner" progress indicator :param progress: an object represinting current progress :param show_text: If true, show progress text as well :return: The spinner string >>> spinner_str(Progress("baloons", 0)) '|' >>> spinner_str(Progress("baloons", 5)) '/' >>> spinner_str(Progress("baloons", 6), show_text=True) '- 6 baloons' """ positions = ('|', '/', '-', '\\') text = positions[progress.current % 4] if show_text: text+=" %i %s" % (progress.current, progress.units) return text def spinner(progress, show_text=False, output=sys.stderr): """ Update a spinner progress indicator on an output :param progress: The progress to display :param show_text: If true, show text as well as spinner :param output: The output to write to >>> spinner(Progress("baloons", 6), show_text=True, output=sys.stdout) \r- 6 baloons """ output.write('\r%s' % spinner_str(progress, show_text)) def run_tests(): import doctest result = doctest.testmod() if result[1] > 0: if result[0] == 0: print "All tests passed" else: print "No tests to run" def demo(): from time import sleep pb = ProgressBar() for i in range(100): pb(Progress('Elephanten', i, 100)) sleep(0.3) print 'done!' if __name__ == "__main__": demo() commit refs/heads/master mark :653 committer Martin Pool 1118387824 +1000 data 3 doc from :652 M 644 inline bzrlib/progress.py data 6560 # Copyright (C) 2005 Aaron Bentley # Copyright (C) 2005 Canonical # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ Simple text-mode progress indicator. Everyone loves ascii art! To display an indicator, create a ProgressBar object. Call it, passing Progress objects indicating the current state. When done, call clear(). Progress is suppressed when output is not sent to a terminal, so as not to clutter log files. """ # TODO: remove functions in favour of keeping everything in one class # TODO: should be a global option e.g. --silent that disables progress # indicators, preferably without needing to adjust all code that # potentially calls them. import sys import datetime def _width(): """Return estimated terminal width. TODO: Do something smart on Windows? TODO: Is there anything that gets a better update when the window is resized while the program is running? """ import os try: return int(os.environ['COLUMNS']) except (IndexError, KeyError, ValueError): return 80 def _supports_progress(f): return hasattr(f, 'isatty') and f.isatty() class Progress(object): """Description of progress through a task. Basically just a fancy tuple holding: units noun string describing what is being traversed, e.g. "balloons", "kB" current how many objects have been processed so far total total number of objects to process, if known. """ def __init__(self, units, current, total=None): self.units = units self.current = current self.total = total def _get_percent(self): if self.total is not None and self.current is not None: return 100.0 * self.current / self.total percent = property(_get_percent) def __str__(self): if self.total is not None: return "%i of %i %s %.1f%%" % (self.current, self.total, self.units, self.percent) else: return "%i %s" (self.current, self.units) class ProgressBar(object): def __init__(self, to_file=sys.stderr): object.__init__(self) self.start = None self.to_file = to_file self.suppressed = not _supports_progress(self.to_file) def __call__(self, progress): if self.start is None: self.start = datetime.datetime.now() if not self.suppressed: draw_progress_bar(progress, start_time=self.start, to_file=self.to_file) def clear(self): if not self.suppressed: clear_progress_bar(self.to_file) def divide_timedelta(delt, divisor): """Divides a timedelta object""" return datetime.timedelta(float(delt.days)/divisor, float(delt.seconds)/divisor, float(delt.microseconds)/divisor) def str_tdelta(delt): if delt is None: return "-:--:--" return str(datetime.timedelta(delt.days, delt.seconds)) def get_eta(start_time, progress, enough_samples=20): if start_time is None or progress.current == 0: return None elif progress.current < enough_samples: return None elapsed = datetime.datetime.now() - start_time total_duration = divide_timedelta((elapsed) * long(progress.total), progress.current) if elapsed < total_duration: eta = total_duration - elapsed else: eta = total_duration - total_duration return eta def draw_progress_bar(progress, start_time=None, to_file=sys.stderr): eta = get_eta(start_time, progress) if start_time is not None: eta_str = " "+str_tdelta(eta) else: eta_str = "" fmt = " %i of %i %s (%.1f%%)" f = fmt % (progress.total, progress.total, progress.units, 100.0) cols = _width() - 3 - len(f) if start_time is not None: cols -= len(eta_str) markers = int (float(cols) * progress.current / progress.total) txt = fmt % (progress.current, progress.total, progress.units, progress.percent) to_file.write("\r[%s%s]%s%s" % ('='*markers, ' '*(cols-markers), txt, eta_str)) def clear_progress_bar(to_file=sys.stderr): to_file.write('\r%s\r' % (' '*79)) def spinner_str(progress, show_text=False): """ Produces the string for a textual "spinner" progress indicator :param progress: an object represinting current progress :param show_text: If true, show progress text as well :return: The spinner string >>> spinner_str(Progress("baloons", 0)) '|' >>> spinner_str(Progress("baloons", 5)) '/' >>> spinner_str(Progress("baloons", 6), show_text=True) '- 6 baloons' """ positions = ('|', '/', '-', '\\') text = positions[progress.current % 4] if show_text: text+=" %i %s" % (progress.current, progress.units) return text def spinner(progress, show_text=False, output=sys.stderr): """ Update a spinner progress indicator on an output :param progress: The progress to display :param show_text: If true, show text as well as spinner :param output: The output to write to >>> spinner(Progress("baloons", 6), show_text=True, output=sys.stdout) \r- 6 baloons """ output.write('\r%s' % spinner_str(progress, show_text)) def run_tests(): import doctest result = doctest.testmod() if result[1] > 0: if result[0] == 0: print "All tests passed" else: print "No tests to run" def demo(): from time import sleep pb = ProgressBar() for i in range(100): pb(Progress('Elephanten', i, 100)) sleep(0.3) print 'done!' if __name__ == "__main__": demo() commit refs/heads/master mark :654 committer Martin Pool 1118387952 +1000 data 96 - update check command to use aaron's progress code rather than hand-hacked progress indicator from :653 M 644 inline bzrlib/check.py data 4234 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def check(branch): """Run consistency checks on a branch. """ import sys from bzrlib.trace import mutter from bzrlib.errors import BzrCheckError from bzrlib.osutils import fingerprint_file from bzrlib.progress import ProgressBar, Progress out = sys.stdout pb = ProgressBar() last_ptr = None checked_revs = {} history = branch.revision_history() revno = 0 revcount = len(history) checked_texts = {} for rid in history: revno += 1 pb(Progress('revision', revno, revcount)) mutter(' revision {%s}' % rid) rev = branch.get_revision(rid) if rev.revision_id != rid: raise BzrCheckError('wrong internal revision id in revision {%s}' % rid) if rev.precursor != last_ptr: raise BzrCheckError('mismatched precursor in revision {%s}' % rid) last_ptr = rid if rid in checked_revs: raise BzrCheckError('repeated revision {%s}' % rid) checked_revs[rid] = True ## TODO: Check all the required fields are present on the revision. inv = branch.get_inventory(rev.inventory_id) seen_ids = {} seen_names = {} ## p('revision %d/%d file ids' % (revno, revcount)) for file_id in inv: if file_id in seen_ids: raise BzrCheckError('duplicated file_id {%s} ' 'in inventory for revision {%s}' % (file_id, rid)) seen_ids[file_id] = True i = 0 len_inv = len(inv) for file_id in inv: i += 1 #if (i % 100) == 0: # p('revision %d/%d file text %d/%d' % (revno, revcount, i, len_inv)) ie = inv[file_id] if ie.parent_id != None: if ie.parent_id not in seen_ids: raise BzrCheckError('missing parent {%s} in inventory for revision {%s}' % (ie.parent_id, rid)) if ie.kind == 'file': if ie.text_id in checked_texts: fp = checked_texts[ie.text_id] else: if not ie.text_id in branch.text_store: raise BzrCheckError('text {%s} not in text_store' % ie.text_id) tf = branch.text_store[ie.text_id] fp = fingerprint_file(tf) checked_texts[ie.text_id] = fp if ie.text_size != fp['size']: raise BzrCheckError('text {%s} wrong size' % ie.text_id) if ie.text_sha1 != fp['sha1']: raise BzrCheckError('text {%s} wrong sha1' % ie.text_id) elif ie.kind == 'directory': if ie.text_sha1 != None or ie.text_size != None or ie.text_id != None: raise BzrCheckError('directory {%s} has text in revision {%s}' % (file_id, rid)) # p('revision %d/%d file paths' % (revno, revcount)) for path, ie in inv.iter_entries(): if path in seen_names: raise BzrCheckError('duplicated path %r ' 'in inventory for revision {%s}' % (path, revid)) seen_names[path] = True pb.clear() print 'checked %d revisions, %d file texts' % (revcount, len(checked_texts)) commit refs/heads/master mark :655 committer Martin Pool 1118388139 +1000 data 45 - better calculation of progress bar position from :654 M 644 inline bzrlib/progress.py data 6647 # Copyright (C) 2005 Aaron Bentley # Copyright (C) 2005 Canonical # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ Simple text-mode progress indicator. Everyone loves ascii art! To display an indicator, create a ProgressBar object. Call it, passing Progress objects indicating the current state. When done, call clear(). Progress is suppressed when output is not sent to a terminal, so as not to clutter log files. """ # TODO: remove functions in favour of keeping everything in one class # TODO: should be a global option e.g. --silent that disables progress # indicators, preferably without needing to adjust all code that # potentially calls them. # TODO: Perhaps don't write updates faster than a certain rate, say # 5/second. import sys import datetime def _width(): """Return estimated terminal width. TODO: Do something smart on Windows? TODO: Is there anything that gets a better update when the window is resized while the program is running? """ import os try: return int(os.environ['COLUMNS']) except (IndexError, KeyError, ValueError): return 80 def _supports_progress(f): return hasattr(f, 'isatty') and f.isatty() class Progress(object): """Description of progress through a task. Basically just a fancy tuple holding: units noun string describing what is being traversed, e.g. "balloons", "kB" current how many objects have been processed so far total total number of objects to process, if known. """ def __init__(self, units, current, total=None): self.units = units self.current = current self.total = total def _get_percent(self): if self.total is not None and self.current is not None: return 100.0 * self.current / self.total percent = property(_get_percent) def __str__(self): if self.total is not None: return "%i of %i %s %.1f%%" % (self.current, self.total, self.units, self.percent) else: return "%i %s" (self.current, self.units) class ProgressBar(object): def __init__(self, to_file=sys.stderr): object.__init__(self) self.start = None self.to_file = to_file self.suppressed = not _supports_progress(self.to_file) def __call__(self, progress): if self.start is None: self.start = datetime.datetime.now() if not self.suppressed: draw_progress_bar(progress, start_time=self.start, to_file=self.to_file) def clear(self): if not self.suppressed: clear_progress_bar(self.to_file) def divide_timedelta(delt, divisor): """Divides a timedelta object""" return datetime.timedelta(float(delt.days)/divisor, float(delt.seconds)/divisor, float(delt.microseconds)/divisor) def str_tdelta(delt): if delt is None: return "-:--:--" return str(datetime.timedelta(delt.days, delt.seconds)) def get_eta(start_time, progress, enough_samples=20): if start_time is None or progress.current == 0: return None elif progress.current < enough_samples: return None elapsed = datetime.datetime.now() - start_time total_duration = divide_timedelta((elapsed) * long(progress.total), progress.current) if elapsed < total_duration: eta = total_duration - elapsed else: eta = total_duration - total_duration return eta def draw_progress_bar(progress, start_time=None, to_file=sys.stderr): eta = get_eta(start_time, progress) if start_time is not None: eta_str = " "+str_tdelta(eta) else: eta_str = "" fmt = " %i of %i %s (%.1f%%)" f = fmt % (progress.total, progress.total, progress.units, 100.0) cols = _width() - 3 - len(f) if start_time is not None: cols -= len(eta_str) markers = int(round(float(cols) * progress.current / progress.total)) txt = fmt % (progress.current, progress.total, progress.units, progress.percent) to_file.write("\r[%s%s]%s%s" % ('='*markers, ' '*(cols-markers), txt, eta_str)) def clear_progress_bar(to_file=sys.stderr): to_file.write('\r%s\r' % (' '*79)) def spinner_str(progress, show_text=False): """ Produces the string for a textual "spinner" progress indicator :param progress: an object represinting current progress :param show_text: If true, show progress text as well :return: The spinner string >>> spinner_str(Progress("baloons", 0)) '|' >>> spinner_str(Progress("baloons", 5)) '/' >>> spinner_str(Progress("baloons", 6), show_text=True) '- 6 baloons' """ positions = ('|', '/', '-', '\\') text = positions[progress.current % 4] if show_text: text+=" %i %s" % (progress.current, progress.units) return text def spinner(progress, show_text=False, output=sys.stderr): """ Update a spinner progress indicator on an output :param progress: The progress to display :param show_text: If true, show text as well as spinner :param output: The output to write to >>> spinner(Progress("baloons", 6), show_text=True, output=sys.stdout) \r- 6 baloons """ output.write('\r%s' % spinner_str(progress, show_text)) def run_tests(): import doctest result = doctest.testmod() if result[1] > 0: if result[0] == 0: print "All tests passed" else: print "No tests to run" def demo(): from time import sleep pb = ProgressBar() for i in range(100): pb(Progress('Elephanten', i, 100)) sleep(0.3) print 'done!' if __name__ == "__main__": demo() commit refs/heads/master mark :656 committer Martin Pool 1118388756 +1000 data 46 - create branch lock files if they don't exist from :655 M 644 inline bzrlib/lock.py data 7839 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Locking wrappers. This only does local locking using OS locks for now. This module causes two methods, lock() and unlock() to be defined in any way that works on the current platform. It is not specified whether these locks are reentrant (i.e. can be taken repeatedly by a single process) or whether they exclude different threads in a single process. Eventually we may need to use some kind of lock representation that will work on a dumb filesystem without actual locking primitives. This defines two classes: ReadLock and WriteLock, which can be implemented in different ways on different platforms. Both have an unlock() method. """ import sys import os from bzrlib.trace import mutter, note, warning from bzrlib.errors import LockError class _base_Lock(object): def _open(self, filename, filemode): import errno try: self.f = open(filename, filemode) return self.f except IOError, e: if e.errno != errno.ENOENT: raise # maybe this is an old branch (before may 2005) mutter("trying to create missing branch lock %r" % filename) self.f = open(filename, 'wb') return self.f def __del__(self): if self.f: from warnings import warn warn("lock on %r not released" % self.f) self.unlock() def unlock(self): raise NotImplementedError() ############################################################ # msvcrt locks try: import fcntl class _fcntl_FileLock(_base_Lock): f = None def unlock(self): fcntl.flock(self.f, fcntl.LOCK_UN) self.f.close() del self.f class _fcntl_WriteLock(_fcntl_FileLock): def __init__(self, filename): try: fcntl.flock(self._open(filename, 'wb'), fcntl.LOCK_EX) except Exception, e: raise LockError(e) class _fcntl_ReadLock(_fcntl_FileLock): def __init__(self, filename): try: fcntl.flock(self._open(filename, 'rb'), fcntl.LOCK_SH) except Exception, e: raise LockError(e) WriteLock = _fcntl_WriteLock ReadLock = _fcntl_ReadLock except ImportError: try: import win32con, win32file, pywintypes #LOCK_SH = 0 # the default #LOCK_EX = win32con.LOCKFILE_EXCLUSIVE_LOCK #LOCK_NB = win32con.LOCKFILE_FAIL_IMMEDIATELY class _w32c_FileLock(_base_Lock): def _lock(self, filename, openmode, lockmode): try: self._open(filename, openmode) self.hfile = win32file._get_osfhandle(self.f.fileno()) overlapped = pywintypes.OVERLAPPED() win32file.LockFileEx(self.hfile, lockmode, 0, 0x7fff0000, overlapped) except Exception, e: raise LockError(e) def unlock(self): try: overlapped = pywintypes.OVERLAPPED() win32file.UnlockFileEx(self.hfile, 0, 0x7fff0000, overlapped) self.f.close() self.f = None except Exception, e: raise LockError(e) class _w32c_ReadLock(_w32c_FileLock): def __init__(self, filename): _w32c_FileLock._lock(self, filename, 'rb', 0) class _w32c_WriteLock(_w32c_FileLock): def __init__(self, filename): _w32c_FileLock._lock(self, filename, 'wb', win32con.LOCKFILE_EXCLUSIVE_LOCK) WriteLock = _w32c_WriteLock ReadLock = _w32c_ReadLock except ImportError: try: import msvcrt # Unfortunately, msvcrt.locking() doesn't distinguish between # read locks and write locks. Also, the way the combinations # work to get non-blocking is not the same, so we # have to write extra special functions here. class _msvc_FileLock(_base_Lock): LOCK_SH = 1 LOCK_EX = 2 LOCK_NB = 4 def unlock(self): _msvc_unlock(self.f) class _msvc_ReadLock(_msvc_FileLock): def __init__(self, filename): _msvc_lock(self._open(filename, 'rb'), self.LOCK_SH) class _msvc_WriteLock(_msvc_FileLock): def __init__(self, filename): _msvc_lock(self._open(filename, 'wb'), self.LOCK_EX) def _msvc_lock(f, flags): try: # Unfortunately, msvcrt.LK_RLCK is equivalent to msvcrt.LK_LOCK # according to the comments, LK_RLCK is open the lock for writing. # Unfortunately, msvcrt.locking() also has the side effect that it # will only block for 10 seconds at most, and then it will throw an # exception, this isn't terrible, though. if type(f) == file: fpos = f.tell() fn = f.fileno() f.seek(0) else: fn = f fpos = os.lseek(fn, 0,0) os.lseek(fn, 0,0) if flags & self.LOCK_SH: if flags & self.LOCK_NB: lock_mode = msvcrt.LK_NBLCK else: lock_mode = msvcrt.LK_LOCK elif flags & self.LOCK_EX: if flags & self.LOCK_NB: lock_mode = msvcrt.LK_NBRLCK else: lock_mode = msvcrt.LK_RLCK else: raise ValueError('Invalid lock mode: %r' % flags) try: msvcrt.locking(fn, lock_mode, -1) finally: os.lseek(fn, fpos, 0) except Exception, e: raise LockError(e) def _msvc_unlock(f): try: if type(f) == file: fpos = f.tell() fn = f.fileno() f.seek(0) else: fn = f fpos = os.lseek(fn, 0,0) os.lseek(fn, 0,0) try: msvcrt.locking(fn, msvcrt.LK_UNLCK, -1) finally: os.lseek(fn, fpos, 0) except Exception, e: raise LockError(e) WriteLock = _msvc_WriteLock ReadLock = _msvc_ReadLock except ImportError: raise NotImplementedError("please write a locking method " "for platform %r" % sys.platform) commit refs/heads/master mark :657 committer Martin Pool 1118389116 +1000 data 41 - more progress indicators while checking from :656 M 644 inline bzrlib/check.py data 4159 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def check(branch): """Run consistency checks on a branch. """ import sys from bzrlib.trace import mutter from bzrlib.errors import BzrCheckError from bzrlib.osutils import fingerprint_file from bzrlib.progress import ProgressBar, Progress out = sys.stdout pb = ProgressBar() last_ptr = None checked_revs = {} history = branch.revision_history() revno = 0 revcount = len(history) checked_texts = {} for rid in history: revno += 1 pb(Progress('revision', revno, revcount)) mutter(' revision {%s}' % rid) rev = branch.get_revision(rid) if rev.revision_id != rid: raise BzrCheckError('wrong internal revision id in revision {%s}' % rid) if rev.precursor != last_ptr: raise BzrCheckError('mismatched precursor in revision {%s}' % rid) last_ptr = rid if rid in checked_revs: raise BzrCheckError('repeated revision {%s}' % rid) checked_revs[rid] = True ## TODO: Check all the required fields are present on the revision. inv = branch.get_inventory(rev.inventory_id) seen_ids = {} seen_names = {} ## p('revision %d/%d file ids' % (revno, revcount)) for file_id in inv: if file_id in seen_ids: raise BzrCheckError('duplicated file_id {%s} ' 'in inventory for revision {%s}' % (file_id, rid)) seen_ids[file_id] = True i = 0 len_inv = len(inv) for file_id in inv: i += 1 pb(Progress('file texts', i, len_inv)) ie = inv[file_id] if ie.parent_id != None: if ie.parent_id not in seen_ids: raise BzrCheckError('missing parent {%s} in inventory for revision {%s}' % (ie.parent_id, rid)) if ie.kind == 'file': if ie.text_id in checked_texts: fp = checked_texts[ie.text_id] else: if not ie.text_id in branch.text_store: raise BzrCheckError('text {%s} not in text_store' % ie.text_id) tf = branch.text_store[ie.text_id] fp = fingerprint_file(tf) checked_texts[ie.text_id] = fp if ie.text_size != fp['size']: raise BzrCheckError('text {%s} wrong size' % ie.text_id) if ie.text_sha1 != fp['sha1']: raise BzrCheckError('text {%s} wrong sha1' % ie.text_id) elif ie.kind == 'directory': if ie.text_sha1 != None or ie.text_size != None or ie.text_id != None: raise BzrCheckError('directory {%s} has text in revision {%s}' % (file_id, rid)) pb(Progress('file paths', revno, revcount)) for path, ie in inv.iter_entries(): if path in seen_names: raise BzrCheckError('duplicated path %r ' 'in inventory for revision {%s}' % (path, revid)) seen_names[path] = True pb.clear() print 'checked %d revisions, %d file texts' % (revcount, len(checked_texts)) commit refs/heads/master mark :658 committer Martin Pool 1118392833 +1000 data 63 - clean up and add a bunch of options to the progress indicator from :657 M 644 inline bzrlib/check.py data 4145 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def check(branch): """Run consistency checks on a branch. """ import sys from bzrlib.trace import mutter from bzrlib.errors import BzrCheckError from bzrlib.osutils import fingerprint_file from bzrlib.progress import ProgressBar out = sys.stdout pb = ProgressBar(show_spinner=True) last_ptr = None checked_revs = {} history = branch.revision_history() revno = 0 revcount = len(history) checked_texts = {} for rid in history: revno += 1 pb.update('checking revision', revno, revcount) mutter(' revision {%s}' % rid) rev = branch.get_revision(rid) if rev.revision_id != rid: raise BzrCheckError('wrong internal revision id in revision {%s}' % rid) if rev.precursor != last_ptr: raise BzrCheckError('mismatched precursor in revision {%s}' % rid) last_ptr = rid if rid in checked_revs: raise BzrCheckError('repeated revision {%s}' % rid) checked_revs[rid] = True ## TODO: Check all the required fields are present on the revision. inv = branch.get_inventory(rev.inventory_id) seen_ids = {} seen_names = {} ## p('revision %d/%d file ids' % (revno, revcount)) for file_id in inv: if file_id in seen_ids: raise BzrCheckError('duplicated file_id {%s} ' 'in inventory for revision {%s}' % (file_id, rid)) seen_ids[file_id] = True i = 0 len_inv = len(inv) for file_id in inv: i += 1 if (i % 100) == 99: pb.tick() ie = inv[file_id] if ie.parent_id != None: if ie.parent_id not in seen_ids: raise BzrCheckError('missing parent {%s} in inventory for revision {%s}' % (ie.parent_id, rid)) if ie.kind == 'file': if ie.text_id in checked_texts: fp = checked_texts[ie.text_id] else: if not ie.text_id in branch.text_store: raise BzrCheckError('text {%s} not in text_store' % ie.text_id) tf = branch.text_store[ie.text_id] fp = fingerprint_file(tf) checked_texts[ie.text_id] = fp if ie.text_size != fp['size']: raise BzrCheckError('text {%s} wrong size' % ie.text_id) if ie.text_sha1 != fp['sha1']: raise BzrCheckError('text {%s} wrong sha1' % ie.text_id) elif ie.kind == 'directory': if ie.text_sha1 != None or ie.text_size != None or ie.text_id != None: raise BzrCheckError('directory {%s} has text in revision {%s}' % (file_id, rid)) pb.tick() for path, ie in inv.iter_entries(): if path in seen_names: raise BzrCheckError('duplicated path %r ' 'in inventory for revision {%s}' % (path, revid)) seen_names[path] = True pb.clear() print 'checked %d revisions, %d file texts' % (revcount, len(checked_texts)) M 644 inline bzrlib/progress.py data 7408 # Copyright (C) 2005 Aaron Bentley # Copyright (C) 2005 Canonical # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ Simple text-mode progress indicator. Everyone loves ascii art! To display an indicator, create a ProgressBar object. Call it, passing Progress objects indicating the current state. When done, call clear(). Progress is suppressed when output is not sent to a terminal, so as not to clutter log files. """ # TODO: remove functions in favour of keeping everything in one class # TODO: should be a global option e.g. --silent that disables progress # indicators, preferably without needing to adjust all code that # potentially calls them. # TODO: Perhaps don't write updates faster than a certain rate, say # 5/second. import sys import datetime def _width(): """Return estimated terminal width. TODO: Do something smart on Windows? TODO: Is there anything that gets a better update when the window is resized while the program is running? """ import os try: return int(os.environ['COLUMNS']) except (IndexError, KeyError, ValueError): return 80 def _supports_progress(f): return hasattr(f, 'isatty') and f.isatty() class ProgressBar(object): """Progress bar display object. Several options are available to control the display. These can be passed as parameters to the constructor or assigned at any time: show_pct Show percentage complete. show_spinner Show rotating baton. This ticks over on every update even if the values don't change. show_eta Show predicted time-to-completion. show_bar Show bar graph. show_count Show numerical counts. The output file should be in line-buffered or unbuffered mode. """ SPIN_CHARS = r'/-\|' def __init__(self, to_file=sys.stderr, show_pct=False, show_spinner=False, show_eta=True, show_bar=True, show_count=True): object.__init__(self) self.start_time = None self.to_file = to_file self.suppressed = not _supports_progress(self.to_file) self.spin_pos = 0 self.show_pct = show_pct self.show_spinner = show_spinner self.show_eta = show_eta self.show_bar = show_bar self.show_count = show_count def tick(self): self.update(self.last_msg, self.last_cnt, self.last_total) def update(self, msg, current_cnt, total_cnt=None): """Update and redraw progress bar.""" if self.start_time is None: self.start_time = datetime.datetime.now() # save these for the tick() function self.last_msg = msg self.last_cnt = current_cnt self.last_total = total_cnt if self.suppressed: return width = _width() if total_cnt: assert current_cnt <= total_cnt if current_cnt: assert current_cnt >= 0 if self.show_eta and self.start_time and total_cnt: eta = get_eta(self.start_time, current_cnt, total_cnt) eta_str = " " + str_tdelta(eta) else: eta_str = "" if self.show_spinner: spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' ' else: spin_str = '' # always update this; it's also used for the bar self.spin_pos += 1 if self.show_pct and total_cnt and current_cnt: pct = 100.0 * current_cnt / total_cnt pct_str = ' (%5.1f%%)' % pct else: pct_str = '' if not self.show_count: count_str = '' elif current_cnt is None: count_str = '' elif total_cnt is None: count_str = ' %i' % (current_cnt) else: # make both fields the same size t = '%i' % (total_cnt) c = '%*i' % (len(t), current_cnt) count_str = ' ' + c + '/' + t if self.show_bar: # progress bar, if present, soaks up all remaining space cols = width - 1 - len(msg) - len(spin_str) - len(pct_str) \ - len(eta_str) - len(count_str) - 3 if total_cnt: # number of markers highlighted in bar markers = int(round(float(cols) * current_cnt / total_cnt)) bar_str = '[' + ('=' * markers).ljust(cols) + '] ' else: # don't know total, so can't show completion. # so just show an expanded spinning thingy m = self.spin_pos % cols ms = ' ' * cols ms[m] = '*' bar_str = '[' + ms + '] ' else: bar_str = '' m = spin_str + bar_str + msg + count_str + pct_str + eta_str assert len(m) < width self.to_file.write('\r' + m.ljust(width - 1)) #self.to_file.flush() def clear(self): if self.suppressed: return self.to_file.write('\r%s\r' % (' ' * (_width() - 1))) #self.to_file.flush() def divide_timedelta(delt, divisor): """Divides a timedelta object""" return datetime.timedelta(float(delt.days)/divisor, float(delt.seconds)/divisor, float(delt.microseconds)/divisor) def str_tdelta(delt): if delt is None: return "-:--:--" return str(datetime.timedelta(delt.days, delt.seconds)) def get_eta(start_time, current, total, enough_samples=20): if start_time is None or current == 0: return None elif current < enough_samples: # FIXME: No good if it's not a count return None elapsed = datetime.datetime.now() - start_time total_duration = divide_timedelta(elapsed * long(total), current) if elapsed < total_duration: eta = total_duration - elapsed else: eta = total_duration - total_duration return eta def run_tests(): import doctest result = doctest.testmod() if result[1] > 0: if result[0] == 0: print "All tests passed" else: print "No tests to run" def demo(): from time import sleep pb = ProgressBar(show_pct=True, show_bar=True, show_spinner=False) for i in range(100): pb.update('Elephanten', i, 99) sleep(0.1) sleep(2) pb.clear() sleep(1) print 'done!' if __name__ == "__main__": demo() commit refs/heads/master mark :659 committer Martin Pool 1118393054 +1000 data 50 - upload-bzr.dev: fix permissions before uploading from :658 M 644 inline contrib/upload-bzr.dev data 784 #! /bin/sh -ex # example of how to upload a bzr tree using rsync # --include-from is used to make sure that only versioned files and # control files are copied. We use includes/excludes rather than # --files-from so that we can delete any files from the destination # that are no longer present on the source. cd ~/work/bzr echo "checking permissions..." chmod -Rv a+rX . bzr inventory | rsync -av \ . \ escudero.ubuntu.com:/srv/www.bazaar-ng.org/rsync/bzr/bzr.dev/ \ --include-from - \ --include .bzr \ --include '.bzr/**' \ --exclude-from .rsyncexclude \ --exclude-from .bzrignore \ --exclude \* \ --exclude '.*' \ --delete-excluded --delete \ commit refs/heads/master mark :660 committer Martin Pool 1118393602 +1000 data 42 - use plain unix time, not datetime module from :659 M 644 inline bzrlib/progress.py data 7253 # Copyright (C) 2005 Aaron Bentley # Copyright (C) 2005 Canonical # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ Simple text-mode progress indicator. Everyone loves ascii art! To display an indicator, create a ProgressBar object. Call it, passing Progress objects indicating the current state. When done, call clear(). Progress is suppressed when output is not sent to a terminal, so as not to clutter log files. """ # TODO: remove functions in favour of keeping everything in one class # TODO: should be a global option e.g. --silent that disables progress # indicators, preferably without needing to adjust all code that # potentially calls them. # TODO: Perhaps don't write updates faster than a certain rate, say # 5/second. import sys import time def _width(): """Return estimated terminal width. TODO: Do something smart on Windows? TODO: Is there anything that gets a better update when the window is resized while the program is running? """ import os try: return int(os.environ['COLUMNS']) except (IndexError, KeyError, ValueError): return 80 def _supports_progress(f): return hasattr(f, 'isatty') and f.isatty() class ProgressBar(object): """Progress bar display object. Several options are available to control the display. These can be passed as parameters to the constructor or assigned at any time: show_pct Show percentage complete. show_spinner Show rotating baton. This ticks over on every update even if the values don't change. show_eta Show predicted time-to-completion. show_bar Show bar graph. show_count Show numerical counts. The output file should be in line-buffered or unbuffered mode. """ SPIN_CHARS = r'/-\|' def __init__(self, to_file=sys.stderr, show_pct=False, show_spinner=False, show_eta=True, show_bar=True, show_count=True): object.__init__(self) self.start_time = None self.to_file = to_file self.suppressed = not _supports_progress(self.to_file) self.spin_pos = 0 self.show_pct = show_pct self.show_spinner = show_spinner self.show_eta = show_eta self.show_bar = show_bar self.show_count = show_count def tick(self): self.update(self.last_msg, self.last_cnt, self.last_total) def update(self, msg, current_cnt, total_cnt=None): """Update and redraw progress bar.""" if self.start_time is None: self.start_time = time.time() # save these for the tick() function self.last_msg = msg self.last_cnt = current_cnt self.last_total = total_cnt if self.suppressed: return width = _width() if total_cnt: assert current_cnt <= total_cnt if current_cnt: assert current_cnt >= 0 if self.show_eta and self.start_time and total_cnt: eta = get_eta(self.start_time, current_cnt, total_cnt) eta_str = " " + str_tdelta(eta) else: eta_str = "" if self.show_spinner: spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' ' else: spin_str = '' # always update this; it's also used for the bar self.spin_pos += 1 if self.show_pct and total_cnt and current_cnt: pct = 100.0 * current_cnt / total_cnt pct_str = ' (%5.1f%%)' % pct else: pct_str = '' if not self.show_count: count_str = '' elif current_cnt is None: count_str = '' elif total_cnt is None: count_str = ' %i' % (current_cnt) else: # make both fields the same size t = '%i' % (total_cnt) c = '%*i' % (len(t), current_cnt) count_str = ' ' + c + '/' + t if self.show_bar: # progress bar, if present, soaks up all remaining space cols = width - 1 - len(msg) - len(spin_str) - len(pct_str) \ - len(eta_str) - len(count_str) - 3 if total_cnt: # number of markers highlighted in bar markers = int(round(float(cols) * current_cnt / total_cnt)) bar_str = '[' + ('=' * markers).ljust(cols) + '] ' else: # don't know total, so can't show completion. # so just show an expanded spinning thingy m = self.spin_pos % cols ms = ' ' * cols ms[m] = '*' bar_str = '[' + ms + '] ' else: bar_str = '' m = spin_str + bar_str + msg + count_str + pct_str + eta_str assert len(m) < width self.to_file.write('\r' + m.ljust(width - 1)) #self.to_file.flush() def clear(self): if self.suppressed: return self.to_file.write('\r%s\r' % (' ' * (_width() - 1))) #self.to_file.flush() def str_tdelta(delt): if delt is None: return "-:--:--" delt = int(round(delt)) return '%d:%02d:%02d' % (delt/3600, (delt/60) % 60, delt % 60) def get_eta(start_time, current, total, enough_samples=3): if start_time is None: return None if not total: return None if current < enough_samples: return None if current > total: return None # wtf? elapsed = time.time() - start_time if elapsed < 2.0: # not enough time to estimate return None total_duration = float(elapsed) * float(total) / float(current) assert total_duration >= elapsed return total_duration - elapsed def run_tests(): import doctest result = doctest.testmod() if result[1] > 0: if result[0] == 0: print "All tests passed" else: print "No tests to run" def demo(): from time import sleep pb = ProgressBar(show_pct=True, show_bar=True, show_spinner=False) for i in range(100): pb.update('Elephanten', i, 99) sleep(0.1) sleep(2) pb.clear() sleep(1) print 'done!' if __name__ == "__main__": demo() commit refs/heads/master mark :661 committer Martin Pool 1118394461 +1000 data 45 - limit rate at which progress bar is updated from :660 M 644 inline bzrlib/progress.py data 7498 # Copyright (C) 2005 Aaron Bentley # Copyright (C) 2005 Canonical # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ Simple text-mode progress indicator. Everyone loves ascii art! To display an indicator, create a ProgressBar object. Call it, passing Progress objects indicating the current state. When done, call clear(). Progress is suppressed when output is not sent to a terminal, so as not to clutter log files. """ # TODO: remove functions in favour of keeping everything in one class # TODO: should be a global option e.g. --silent that disables progress # indicators, preferably without needing to adjust all code that # potentially calls them. # TODO: Perhaps don't write updates faster than a certain rate, say # 5/second. import sys import time def _width(): """Return estimated terminal width. TODO: Do something smart on Windows? TODO: Is there anything that gets a better update when the window is resized while the program is running? """ import os try: return int(os.environ['COLUMNS']) except (IndexError, KeyError, ValueError): return 80 def _supports_progress(f): return hasattr(f, 'isatty') and f.isatty() class ProgressBar(object): """Progress bar display object. Several options are available to control the display. These can be passed as parameters to the constructor or assigned at any time: show_pct Show percentage complete. show_spinner Show rotating baton. This ticks over on every update even if the values don't change. show_eta Show predicted time-to-completion. show_bar Show bar graph. show_count Show numerical counts. The output file should be in line-buffered or unbuffered mode. """ SPIN_CHARS = r'/-\|' MIN_PAUSE = 0.1 # seconds start_time = None last_update = None def __init__(self, to_file=sys.stderr, show_pct=False, show_spinner=False, show_eta=True, show_bar=True, show_count=True): object.__init__(self) self.to_file = to_file self.suppressed = not _supports_progress(self.to_file) self.spin_pos = 0 self.show_pct = show_pct self.show_spinner = show_spinner self.show_eta = show_eta self.show_bar = show_bar self.show_count = show_count def tick(self): self.update(self.last_msg, self.last_cnt, self.last_total) def update(self, msg, current_cnt, total_cnt=None): """Update and redraw progress bar.""" if self.suppressed: return # save these for the tick() function self.last_msg = msg self.last_cnt = current_cnt self.last_total = total_cnt now = time.time() if self.start_time is None: self.start_time = now else: interval = now - self.last_update if interval > 0 and interval < self.MIN_PAUSE: return self.last_update = now width = _width() if total_cnt: assert current_cnt <= total_cnt if current_cnt: assert current_cnt >= 0 if self.show_eta and self.start_time and total_cnt: eta = get_eta(self.start_time, current_cnt, total_cnt) eta_str = " " + str_tdelta(eta) else: eta_str = "" if self.show_spinner: spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' ' else: spin_str = '' # always update this; it's also used for the bar self.spin_pos += 1 if self.show_pct and total_cnt and current_cnt: pct = 100.0 * current_cnt / total_cnt pct_str = ' (%5.1f%%)' % pct else: pct_str = '' if not self.show_count: count_str = '' elif current_cnt is None: count_str = '' elif total_cnt is None: count_str = ' %i' % (current_cnt) else: # make both fields the same size t = '%i' % (total_cnt) c = '%*i' % (len(t), current_cnt) count_str = ' ' + c + '/' + t if self.show_bar: # progress bar, if present, soaks up all remaining space cols = width - 1 - len(msg) - len(spin_str) - len(pct_str) \ - len(eta_str) - len(count_str) - 3 if total_cnt: # number of markers highlighted in bar markers = int(round(float(cols) * current_cnt / total_cnt)) bar_str = '[' + ('=' * markers).ljust(cols) + '] ' else: # don't know total, so can't show completion. # so just show an expanded spinning thingy m = self.spin_pos % cols ms = ' ' * cols ms[m] = '*' bar_str = '[' + ms + '] ' else: bar_str = '' m = spin_str + bar_str + msg + count_str + pct_str + eta_str assert len(m) < width self.to_file.write('\r' + m.ljust(width - 1)) #self.to_file.flush() def clear(self): if self.suppressed: return self.to_file.write('\r%s\r' % (' ' * (_width() - 1))) #self.to_file.flush() def str_tdelta(delt): if delt is None: return "-:--:--" delt = int(round(delt)) return '%d:%02d:%02d' % (delt/3600, (delt/60) % 60, delt % 60) def get_eta(start_time, current, total, enough_samples=3): if start_time is None: return None if not total: return None if current < enough_samples: return None if current > total: return None # wtf? elapsed = time.time() - start_time if elapsed < 2.0: # not enough time to estimate return None total_duration = float(elapsed) * float(total) / float(current) assert total_duration >= elapsed return total_duration - elapsed def run_tests(): import doctest result = doctest.testmod() if result[1] > 0: if result[0] == 0: print "All tests passed" else: print "No tests to run" def demo(): from time import sleep pb = ProgressBar(show_pct=True, show_bar=True, show_spinner=False) for i in range(100): pb.update('Elephanten', i, 99) sleep(0.1) sleep(2) pb.clear() sleep(1) print 'done!' if __name__ == "__main__": demo() commit refs/heads/master mark :662 committer Martin Pool 1118394477 +1000 data 31 - tune check progress indicator from :661 M 644 inline bzrlib/check.py data 4141 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def check(branch): """Run consistency checks on a branch. """ import sys from bzrlib.trace import mutter from bzrlib.errors import BzrCheckError from bzrlib.osutils import fingerprint_file from bzrlib.progress import ProgressBar out = sys.stdout pb = ProgressBar(show_spinner=True) last_ptr = None checked_revs = {} history = branch.revision_history() revno = 0 revcount = len(history) checked_texts = {} for rid in history: revno += 1 pb.update('checking revision', revno, revcount) mutter(' revision {%s}' % rid) rev = branch.get_revision(rid) if rev.revision_id != rid: raise BzrCheckError('wrong internal revision id in revision {%s}' % rid) if rev.precursor != last_ptr: raise BzrCheckError('mismatched precursor in revision {%s}' % rid) last_ptr = rid if rid in checked_revs: raise BzrCheckError('repeated revision {%s}' % rid) checked_revs[rid] = True ## TODO: Check all the required fields are present on the revision. inv = branch.get_inventory(rev.inventory_id) seen_ids = {} seen_names = {} ## p('revision %d/%d file ids' % (revno, revcount)) for file_id in inv: if file_id in seen_ids: raise BzrCheckError('duplicated file_id {%s} ' 'in inventory for revision {%s}' % (file_id, rid)) seen_ids[file_id] = True i = 0 len_inv = len(inv) for file_id in inv: i += 1 if i & 31 == 0: pb.tick() ie = inv[file_id] if ie.parent_id != None: if ie.parent_id not in seen_ids: raise BzrCheckError('missing parent {%s} in inventory for revision {%s}' % (ie.parent_id, rid)) if ie.kind == 'file': if ie.text_id in checked_texts: fp = checked_texts[ie.text_id] else: if not ie.text_id in branch.text_store: raise BzrCheckError('text {%s} not in text_store' % ie.text_id) tf = branch.text_store[ie.text_id] fp = fingerprint_file(tf) checked_texts[ie.text_id] = fp if ie.text_size != fp['size']: raise BzrCheckError('text {%s} wrong size' % ie.text_id) if ie.text_sha1 != fp['sha1']: raise BzrCheckError('text {%s} wrong sha1' % ie.text_id) elif ie.kind == 'directory': if ie.text_sha1 != None or ie.text_size != None or ie.text_id != None: raise BzrCheckError('directory {%s} has text in revision {%s}' % (file_id, rid)) pb.tick() for path, ie in inv.iter_entries(): if path in seen_names: raise BzrCheckError('duplicated path %r ' 'in inventory for revision {%s}' % (path, revid)) seen_names[path] = True pb.clear() print 'checked %d revisions, %d file texts' % (revcount, len(checked_texts)) commit refs/heads/master mark :663 committer Martin Pool 1118394489 +1000 data 3 doc from :662 M 644 inline bzrlib/branch.py data 34471 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def _relpath(base, path): """Return path relative to base, or raise exception. The path may be either an absolute path or a path relative to the current working directory. Lifted out of Branch.relpath for ease of testing. os.path.commonprefix (python2.4) has a bad bug that it works just on string prefixes, assuming that '/u' is a prefix of '/u2'. This avoids that problem.""" rp = os.path.abspath(path) s = [] head = rp while len(head) >= len(base): if head == base: break head, tail = os.path.split(head) if tail: s.insert(0, tail) else: from errors import NotBranchError raise NotBranchError("path %r is not within branch %r" % (rp, base)) return os.sep.join(s) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head class DivergedBranches(Exception): def __init__(self, branch1, branch2): self.branch1 = branch1 self.branch2 = branch2 Exception.__init__(self, "These branches have diverged.") ###################################################################### # branch objects class Branch(object): """Branch holding a history of revisions. base Base directory of the branch. _lock_mode None, or 'r' or 'w' _lock_count If _lock_mode is true, a positive count of the number of times the lock has been taken. _lock Lock object from bzrlib.lock. """ base = None _lock_mode = None _lock_count = None _lock = None def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): from errors import NotBranchError raise NotBranchError("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def __del__(self): if self._lock_mode or self._lock: from warnings import warn warn("branch %r was not explicitly unlocked" % self) self._lock.unlock() def lock_write(self): if self._lock_mode: if self._lock_mode != 'w': from errors import LockError raise LockError("can't upgrade to a write lock from %r" % self._lock_mode) self._lock_count += 1 else: from bzrlib.lock import WriteLock self._lock = WriteLock(self.controlfilename('branch-lock')) self._lock_mode = 'w' self._lock_count = 1 def lock_read(self): if self._lock_mode: assert self._lock_mode in ('r', 'w'), \ "invalid lock mode %r" % self._lock_mode self._lock_count += 1 else: from bzrlib.lock import ReadLock self._lock = ReadLock(self.controlfilename('branch-lock')) self._lock_mode = 'r' self._lock_count = 1 def unlock(self): if not self._lock_mode: from errors import LockError raise LockError('branch %r is not locked' % (self)) if self._lock_count > 1: self._lock_count -= 1 else: self._lock.unlock() self._lock = None self._lock_mode = self._lock_count = None def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" return _relpath(self.base, path) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: raise BzrError('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. self.lock_read() try: inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv finally: self.unlock() def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. files List of paths to add, relative to the base of the tree. ids If set, use these instead of automatically generated ids. Must be the same length as the list of files, but may contain None for ids that are to be autogenerated. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) self.lock_write() try: inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): raise BzrError("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: raise BzrError("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) finally: self.unlock() def print_file(self, file, revno): """Print `file` to stdout.""" self.lock_read() try: tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: raise BzrError("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) finally: self.unlock() def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] self.lock_write() try: tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: raise BzrError("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) finally: self.unlock() # FIXME: this doesn't need to be a branch method def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self.lock_read() try: return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] finally: self.unlock() def common_ancestor(self, other, self_revno=None, other_revno=None): """ >>> import commit >>> sb = ScratchBranch(files=['foo', 'foo~']) >>> sb.common_ancestor(sb) == (None, None) True >>> commit.commit(sb, "Committing first revision", verbose=False) >>> sb.common_ancestor(sb)[0] 1 >>> clone = sb.clone() >>> commit.commit(sb, "Committing second revision", verbose=False) >>> sb.common_ancestor(sb)[0] 2 >>> sb.common_ancestor(clone)[0] 1 >>> commit.commit(clone, "Committing divergent second revision", ... verbose=False) >>> sb.common_ancestor(clone)[0] 1 >>> sb.common_ancestor(clone) == clone.common_ancestor(sb) True >>> sb.common_ancestor(sb) != clone.common_ancestor(clone) True >>> clone2 = sb.clone() >>> sb.common_ancestor(clone2)[0] 2 >>> sb.common_ancestor(clone2, self_revno=1)[0] 1 >>> sb.common_ancestor(clone2, other_revno=1)[0] 1 """ my_history = self.revision_history() other_history = other.revision_history() if self_revno is None: self_revno = len(my_history) if other_revno is None: other_revno = len(other_history) indices = range(min((self_revno, other_revno))) indices.reverse() for r in indices: if my_history[r] == other_history[r]: return r+1, my_history[r] return None, None def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def missing_revisions(self, other): """ If self and other have not diverged, return a list of the revisions present in other, but missing from self. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch() >>> br2 = ScratchBranch() >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [u'REVISION-ID-1'] >>> br2.missing_revisions(br1) [] >>> commit(br1, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-2A") >>> br1.missing_revisions(br2) [u'REVISION-ID-2A'] >>> commit(br1, "lala!", rev_id="REVISION-ID-2B") >>> br1.missing_revisions(br2) Traceback (most recent call last): DivergedBranches: These branches have diverged. """ self_history = self.revision_history() self_len = len(self_history) other_history = other.revision_history() other_len = len(other_history) common_index = min(self_len, other_len) -1 if common_index >= 0 and \ self_history[common_index] != other_history[common_index]: raise DivergedBranches(self, other) if self_len < other_len: return other_history[self_len:] return [] def update_revisions(self, other): """Pull in all new revisions from other branch. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch(files=['foo', 'bar']) >>> br1.add('foo') >>> br1.add('bar') >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False) >>> br2 = ScratchBranch() >>> br2.update_revisions(br1) Added 2 texts. Added 1 inventories. Added 1 revisions. >>> br2.revision_history() [u'REVISION-ID-1'] >>> br2.update_revisions(br1) Added 0 texts. Added 0 inventories. Added 0 revisions. >>> br1.text_store.total_size() == br2.text_store.total_size() True """ revision_ids = self.missing_revisions(other) revisions = [other.get_revision(f) for f in revision_ids] needed_texts = sets.Set() for rev in revisions: inv = other.get_inventory(str(rev.inventory_id)) for key, entry in inv.iter_entries(): if entry.text_id is None: continue if entry.text_id not in self.text_store: needed_texts.add(entry.text_id) count = self.text_store.copy_multi(other.text_store, needed_texts) print "Added %d texts." % count inventory_ids = [ f.inventory_id for f in revisions ] count = self.inventory_store.copy_multi(other.inventory_store, inventory_ids) print "Added %d inventories." % count revision_ids = [ f.revision_id for f in revisions] count = self.revision_store.copy_multi(other.revision_store, revision_ids) for revision_id in revision_ids: self.append_revision(revision_id) print "Added %d revisions." % count def commit(self, *args, **kw): """Deprecated""" from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self.lock_write() try: tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): raise BzrError("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): raise BzrError("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: raise BzrError("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): raise BzrError("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': raise BzrError("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self.lock_write() try: ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): raise BzrError("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): raise BzrError("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': raise BzrError("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): raise BzrError("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): raise BzrError("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: raise BzrError("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): raise BzrError("destination %r already exists" % dest_path) if f_id in to_idpath: raise BzrError("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[], base=None): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ init = False if base is None: base = tempfile.mkdtemp() init = True Branch.__init__(self, base, init=init) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def clone(self): """ >>> orig = ScratchBranch(files=["file1", "file2"]) >>> clone = orig.clone() >>> os.path.samefile(orig.base, clone.base) False >>> os.path.isfile(os.path.join(clone.base, "file1")) True """ base = tempfile.mkdtemp() os.rmdir(base) shutil.copytree(self.base, base, symlinks=True) return ScratchBranch(base=base) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: if self.base: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :664 committer Martin Pool 1118395728 +1000 data 33 - change rsync command for upload from :663 M 644 inline contrib/upload-bzr.dev data 800 #! /bin/sh -ex # example of how to upload a bzr tree using rsync # --include-from is used to make sure that only versioned files and # control files are copied. We use includes/excludes rather than # --files-from so that we can delete any files from the destination # that are no longer present on the source. cd ~/work/bzr # note: don't use -a because that can mess up the permissions bzr inventory | rsync -rltv \ . \ escudero.ubuntu.com:/srv/www.bazaar-ng.org/rsync/bzr/bzr.dev/ \ --include-from - \ --include .bzr \ --include '.bzr/**' \ --exclude-from .rsyncexclude \ --exclude-from .bzrignore \ --exclude \* \ --exclude '.*' \ --delete-excluded --delete \ commit refs/heads/master mark :665 committer Martin Pool 1118395857 +1000 data 4 todo from :664 M 644 inline TODO data 13267 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * ``bzr log DIR`` should give changes to any files within DIR. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. Perhaps ~/.bzr/http-cache. Baz has a fairly simple cache under ~/.arch-cache, containing revision information encoded almost as a bunch of archives. Perhaps we could simply store full paths. * Maybe also store directories in the statcache so that we can quickly identify that they still exist. * Diff should show timestamps; for files from the working directory we can use the file itself; for files from a revision we should use the commit time of the revision. * Perhaps split command infrastructure from the actual command definitions. * Cleaner support for negative boolean options like --no-recurse. * Statcache should possibly map all file paths to / separators * quotefn doubles all backslashes on Windows; this is probably not the best thing to do. What would be a better way to safely represent filenames? Perhaps we could doublequote things containing spaces, on the principle that filenames containing quotes are unlikely? Nice for humans; less good for machine parsing. * Patches should probably use only forward slashes, even on Windows, otherwise Unix patch can't apply them. (?) * Branch.update_revisions() inefficiently fetches revisions from the remote server twice; once to find out what text and inventory they need and then again to actually get the thing. This is a bit inefficient. One complicating factor here is that we don't really want to have revisions present in the revision-store until all their constituent parts are also stored. The basic problem is that RemoteBranch.get_revision() and similar methods return object, but what we really want is the raw XML, which can be popped into our own store. That needs to be refactored. Medium things ------------- * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. We should be able to just get the id for the selected files, look up their location and diff just those files. No need to traverse the entire inventories. * ``bzr status DIR`` or ``bzr diff DIR`` should report on all changes under that directory. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from ElementTree to an object when it is read in, but rather wait until the program actually wants to know about that node. * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. - Selected-file commit - Impossible selected-file commit: adding things in non-versioned directories, crossing renames, etc. * Write a reproducible benchmark, perhaps importing various kernel versions. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Paranoid mode where we never trust SHA-1 matches. * Don't commit if there are no changes unless forced. * --dry-run mode for commit? (Or maybe just run with check-command=false?) * Generally, be a bit more verbose unless --silent is specified. * Function that finds all changes to files under a given directory; perhaps log should use this if a directory is given. * XML attributes might have trouble with filenames containing \n and \r. Do we really want to support this? I think perhaps not. * Remember execute bits, so that exports will work OK. * Unify smart_add and plain Branch.add(); perhaps smart_add should just build a list of files to add and pass that to the regular add function. * Function to list a directory, saying in which revision each file was last modified. Useful for web and gui interfaces, and slow to compute one file at a time. * unittest is standard, but the results are kind of ugly; would be nice to make it cleaner. * Check locking is correct during merge-related operations. * Perhaps attempts to get locks should timeout after some period of time, or at least display a progress message. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. Possibly this should be done by splitting the commit function into several parts (under a single interface). It is already rather large. Decomposition: - find tree modifications and prepare in-memory inventory - export that inventory to a temporary directory - run the test in that temporary directory - if that succeeded, continue to actually finish the commit What should be done with the text of modified files while this is underway? I don't think we want to count on holding them in memory and we can't trust the working files to stay in one place so I suppose we need to move them into the text store, or otherwise into a temporary directory. If the commit does not actually complete, we would rather the content was not left behind in the stores. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :666 committer Martin Pool 1118396072 +1000 data 27 - add check on revision-ids from :665 M 644 inline bzrlib/branch.py data 34612 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def _relpath(base, path): """Return path relative to base, or raise exception. The path may be either an absolute path or a path relative to the current working directory. Lifted out of Branch.relpath for ease of testing. os.path.commonprefix (python2.4) has a bad bug that it works just on string prefixes, assuming that '/u' is a prefix of '/u2'. This avoids that problem.""" rp = os.path.abspath(path) s = [] head = rp while len(head) >= len(base): if head == base: break head, tail = os.path.split(head) if tail: s.insert(0, tail) else: from errors import NotBranchError raise NotBranchError("path %r is not within branch %r" % (rp, base)) return os.sep.join(s) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head class DivergedBranches(Exception): def __init__(self, branch1, branch2): self.branch1 = branch1 self.branch2 = branch2 Exception.__init__(self, "These branches have diverged.") ###################################################################### # branch objects class Branch(object): """Branch holding a history of revisions. base Base directory of the branch. _lock_mode None, or 'r' or 'w' _lock_count If _lock_mode is true, a positive count of the number of times the lock has been taken. _lock Lock object from bzrlib.lock. """ base = None _lock_mode = None _lock_count = None _lock = None def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): from errors import NotBranchError raise NotBranchError("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def __del__(self): if self._lock_mode or self._lock: from warnings import warn warn("branch %r was not explicitly unlocked" % self) self._lock.unlock() def lock_write(self): if self._lock_mode: if self._lock_mode != 'w': from errors import LockError raise LockError("can't upgrade to a write lock from %r" % self._lock_mode) self._lock_count += 1 else: from bzrlib.lock import WriteLock self._lock = WriteLock(self.controlfilename('branch-lock')) self._lock_mode = 'w' self._lock_count = 1 def lock_read(self): if self._lock_mode: assert self._lock_mode in ('r', 'w'), \ "invalid lock mode %r" % self._lock_mode self._lock_count += 1 else: from bzrlib.lock import ReadLock self._lock = ReadLock(self.controlfilename('branch-lock')) self._lock_mode = 'r' self._lock_count = 1 def unlock(self): if not self._lock_mode: from errors import LockError raise LockError('branch %r is not locked' % (self)) if self._lock_count > 1: self._lock_count -= 1 else: self._lock.unlock() self._lock = None self._lock_mode = self._lock_count = None def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" return _relpath(self.base, path) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: raise BzrError('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. self.lock_read() try: inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv finally: self.unlock() def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. files List of paths to add, relative to the base of the tree. ids If set, use these instead of automatically generated ids. Must be the same length as the list of files, but may contain None for ids that are to be autogenerated. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) self.lock_write() try: inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): raise BzrError("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: raise BzrError("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) finally: self.unlock() def print_file(self, file, revno): """Print `file` to stdout.""" self.lock_read() try: tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: raise BzrError("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) finally: self.unlock() def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] self.lock_write() try: tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: raise BzrError("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) finally: self.unlock() # FIXME: this doesn't need to be a branch method def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" if not revision_id or not isinstance(revision_id, basestring): raise ValueError('invalid revision-id: %r' % revision_id) r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self.lock_read() try: return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] finally: self.unlock() def common_ancestor(self, other, self_revno=None, other_revno=None): """ >>> import commit >>> sb = ScratchBranch(files=['foo', 'foo~']) >>> sb.common_ancestor(sb) == (None, None) True >>> commit.commit(sb, "Committing first revision", verbose=False) >>> sb.common_ancestor(sb)[0] 1 >>> clone = sb.clone() >>> commit.commit(sb, "Committing second revision", verbose=False) >>> sb.common_ancestor(sb)[0] 2 >>> sb.common_ancestor(clone)[0] 1 >>> commit.commit(clone, "Committing divergent second revision", ... verbose=False) >>> sb.common_ancestor(clone)[0] 1 >>> sb.common_ancestor(clone) == clone.common_ancestor(sb) True >>> sb.common_ancestor(sb) != clone.common_ancestor(clone) True >>> clone2 = sb.clone() >>> sb.common_ancestor(clone2)[0] 2 >>> sb.common_ancestor(clone2, self_revno=1)[0] 1 >>> sb.common_ancestor(clone2, other_revno=1)[0] 1 """ my_history = self.revision_history() other_history = other.revision_history() if self_revno is None: self_revno = len(my_history) if other_revno is None: other_revno = len(other_history) indices = range(min((self_revno, other_revno))) indices.reverse() for r in indices: if my_history[r] == other_history[r]: return r+1, my_history[r] return None, None def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def missing_revisions(self, other): """ If self and other have not diverged, return a list of the revisions present in other, but missing from self. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch() >>> br2 = ScratchBranch() >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [u'REVISION-ID-1'] >>> br2.missing_revisions(br1) [] >>> commit(br1, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-2A") >>> br1.missing_revisions(br2) [u'REVISION-ID-2A'] >>> commit(br1, "lala!", rev_id="REVISION-ID-2B") >>> br1.missing_revisions(br2) Traceback (most recent call last): DivergedBranches: These branches have diverged. """ self_history = self.revision_history() self_len = len(self_history) other_history = other.revision_history() other_len = len(other_history) common_index = min(self_len, other_len) -1 if common_index >= 0 and \ self_history[common_index] != other_history[common_index]: raise DivergedBranches(self, other) if self_len < other_len: return other_history[self_len:] return [] def update_revisions(self, other): """Pull in all new revisions from other branch. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch(files=['foo', 'bar']) >>> br1.add('foo') >>> br1.add('bar') >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False) >>> br2 = ScratchBranch() >>> br2.update_revisions(br1) Added 2 texts. Added 1 inventories. Added 1 revisions. >>> br2.revision_history() [u'REVISION-ID-1'] >>> br2.update_revisions(br1) Added 0 texts. Added 0 inventories. Added 0 revisions. >>> br1.text_store.total_size() == br2.text_store.total_size() True """ revision_ids = self.missing_revisions(other) revisions = [other.get_revision(f) for f in revision_ids] needed_texts = sets.Set() for rev in revisions: inv = other.get_inventory(str(rev.inventory_id)) for key, entry in inv.iter_entries(): if entry.text_id is None: continue if entry.text_id not in self.text_store: needed_texts.add(entry.text_id) count = self.text_store.copy_multi(other.text_store, needed_texts) print "Added %d texts." % count inventory_ids = [ f.inventory_id for f in revisions ] count = self.inventory_store.copy_multi(other.inventory_store, inventory_ids) print "Added %d inventories." % count revision_ids = [ f.revision_id for f in revisions] count = self.revision_store.copy_multi(other.revision_store, revision_ids) for revision_id in revision_ids: self.append_revision(revision_id) print "Added %d revisions." % count def commit(self, *args, **kw): """Deprecated""" from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self.lock_write() try: tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): raise BzrError("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): raise BzrError("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: raise BzrError("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): raise BzrError("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': raise BzrError("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self.lock_write() try: ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): raise BzrError("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): raise BzrError("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': raise BzrError("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): raise BzrError("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): raise BzrError("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: raise BzrError("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): raise BzrError("destination %r already exists" % dest_path) if f_id in to_idpath: raise BzrError("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[], base=None): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ init = False if base is None: base = tempfile.mkdtemp() init = True Branch.__init__(self, base, init=init) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def clone(self): """ >>> orig = ScratchBranch(files=["file1", "file2"]) >>> clone = orig.clone() >>> os.path.samefile(orig.base, clone.base) False >>> os.path.isfile(os.path.join(clone.base, "file1")) True """ base = tempfile.mkdtemp() os.rmdir(base) shutil.copytree(self.base, base, symlinks=True) return ScratchBranch(base=base) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: if self.base: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :667 committer Martin Pool 1118396450 +1000 data 61 - allow for progressbar updates with no count, only a message from :666 M 644 inline bzrlib/progress.py data 7503 # Copyright (C) 2005 Aaron Bentley # Copyright (C) 2005 Canonical # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ Simple text-mode progress indicator. Everyone loves ascii art! To display an indicator, create a ProgressBar object. Call it, passing Progress objects indicating the current state. When done, call clear(). Progress is suppressed when output is not sent to a terminal, so as not to clutter log files. """ # TODO: remove functions in favour of keeping everything in one class # TODO: should be a global option e.g. --silent that disables progress # indicators, preferably without needing to adjust all code that # potentially calls them. # TODO: Perhaps don't write updates faster than a certain rate, say # 5/second. import sys import time def _width(): """Return estimated terminal width. TODO: Do something smart on Windows? TODO: Is there anything that gets a better update when the window is resized while the program is running? """ import os try: return int(os.environ['COLUMNS']) except (IndexError, KeyError, ValueError): return 80 def _supports_progress(f): return hasattr(f, 'isatty') and f.isatty() class ProgressBar(object): """Progress bar display object. Several options are available to control the display. These can be passed as parameters to the constructor or assigned at any time: show_pct Show percentage complete. show_spinner Show rotating baton. This ticks over on every update even if the values don't change. show_eta Show predicted time-to-completion. show_bar Show bar graph. show_count Show numerical counts. The output file should be in line-buffered or unbuffered mode. """ SPIN_CHARS = r'/-\|' MIN_PAUSE = 0.1 # seconds start_time = None last_update = None def __init__(self, to_file=sys.stderr, show_pct=False, show_spinner=False, show_eta=True, show_bar=True, show_count=True): object.__init__(self) self.to_file = to_file self.suppressed = not _supports_progress(self.to_file) self.spin_pos = 0 self.show_pct = show_pct self.show_spinner = show_spinner self.show_eta = show_eta self.show_bar = show_bar self.show_count = show_count def tick(self): self.update(self.last_msg, self.last_cnt, self.last_total) def update(self, msg, current_cnt=None, total_cnt=None): """Update and redraw progress bar.""" if self.suppressed: return # save these for the tick() function self.last_msg = msg self.last_cnt = current_cnt self.last_total = total_cnt now = time.time() if self.start_time is None: self.start_time = now else: interval = now - self.last_update if interval > 0 and interval < self.MIN_PAUSE: return self.last_update = now width = _width() if total_cnt: assert current_cnt <= total_cnt if current_cnt: assert current_cnt >= 0 if self.show_eta and self.start_time and total_cnt: eta = get_eta(self.start_time, current_cnt, total_cnt) eta_str = " " + str_tdelta(eta) else: eta_str = "" if self.show_spinner: spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' ' else: spin_str = '' # always update this; it's also used for the bar self.spin_pos += 1 if self.show_pct and total_cnt and current_cnt: pct = 100.0 * current_cnt / total_cnt pct_str = ' (%5.1f%%)' % pct else: pct_str = '' if not self.show_count: count_str = '' elif current_cnt is None: count_str = '' elif total_cnt is None: count_str = ' %i' % (current_cnt) else: # make both fields the same size t = '%i' % (total_cnt) c = '%*i' % (len(t), current_cnt) count_str = ' ' + c + '/' + t if self.show_bar: # progress bar, if present, soaks up all remaining space cols = width - 1 - len(msg) - len(spin_str) - len(pct_str) \ - len(eta_str) - len(count_str) - 3 if total_cnt: # number of markers highlighted in bar markers = int(round(float(cols) * current_cnt / total_cnt)) bar_str = '[' + ('=' * markers).ljust(cols) + '] ' else: # don't know total, so can't show completion. # so just show an expanded spinning thingy m = self.spin_pos % cols ms = ' ' * cols ms[m] = '*' bar_str = '[' + ms + '] ' else: bar_str = '' m = spin_str + bar_str + msg + count_str + pct_str + eta_str assert len(m) < width self.to_file.write('\r' + m.ljust(width - 1)) #self.to_file.flush() def clear(self): if self.suppressed: return self.to_file.write('\r%s\r' % (' ' * (_width() - 1))) #self.to_file.flush() def str_tdelta(delt): if delt is None: return "-:--:--" delt = int(round(delt)) return '%d:%02d:%02d' % (delt/3600, (delt/60) % 60, delt % 60) def get_eta(start_time, current, total, enough_samples=3): if start_time is None: return None if not total: return None if current < enough_samples: return None if current > total: return None # wtf? elapsed = time.time() - start_time if elapsed < 2.0: # not enough time to estimate return None total_duration = float(elapsed) * float(total) / float(current) assert total_duration >= elapsed return total_duration - elapsed def run_tests(): import doctest result = doctest.testmod() if result[1] > 0: if result[0] == 0: print "All tests passed" else: print "No tests to run" def demo(): from time import sleep pb = ProgressBar(show_pct=True, show_bar=True, show_spinner=False) for i in range(100): pb.update('Elephanten', i, 99) sleep(0.1) sleep(2) pb.clear() sleep(1) print 'done!' if __name__ == "__main__": demo() commit refs/heads/master mark :668 committer Martin Pool 1118396534 +1000 data 37 - fix sweeping bar progress indicator from :667 M 644 inline bzrlib/progress.py data 7492 # Copyright (C) 2005 Aaron Bentley # Copyright (C) 2005 Canonical # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ Simple text-mode progress indicator. Everyone loves ascii art! To display an indicator, create a ProgressBar object. Call it, passing Progress objects indicating the current state. When done, call clear(). Progress is suppressed when output is not sent to a terminal, so as not to clutter log files. """ # TODO: remove functions in favour of keeping everything in one class # TODO: should be a global option e.g. --silent that disables progress # indicators, preferably without needing to adjust all code that # potentially calls them. # TODO: Perhaps don't write updates faster than a certain rate, say # 5/second. import sys import time def _width(): """Return estimated terminal width. TODO: Do something smart on Windows? TODO: Is there anything that gets a better update when the window is resized while the program is running? """ import os try: return int(os.environ['COLUMNS']) except (IndexError, KeyError, ValueError): return 80 def _supports_progress(f): return hasattr(f, 'isatty') and f.isatty() class ProgressBar(object): """Progress bar display object. Several options are available to control the display. These can be passed as parameters to the constructor or assigned at any time: show_pct Show percentage complete. show_spinner Show rotating baton. This ticks over on every update even if the values don't change. show_eta Show predicted time-to-completion. show_bar Show bar graph. show_count Show numerical counts. The output file should be in line-buffered or unbuffered mode. """ SPIN_CHARS = r'/-\|' MIN_PAUSE = 0.1 # seconds start_time = None last_update = None def __init__(self, to_file=sys.stderr, show_pct=False, show_spinner=False, show_eta=True, show_bar=True, show_count=True): object.__init__(self) self.to_file = to_file self.suppressed = not _supports_progress(self.to_file) self.spin_pos = 0 self.show_pct = show_pct self.show_spinner = show_spinner self.show_eta = show_eta self.show_bar = show_bar self.show_count = show_count def tick(self): self.update(self.last_msg, self.last_cnt, self.last_total) def update(self, msg, current_cnt=None, total_cnt=None): """Update and redraw progress bar.""" if self.suppressed: return # save these for the tick() function self.last_msg = msg self.last_cnt = current_cnt self.last_total = total_cnt now = time.time() if self.start_time is None: self.start_time = now else: interval = now - self.last_update if interval > 0 and interval < self.MIN_PAUSE: return self.last_update = now width = _width() if total_cnt: assert current_cnt <= total_cnt if current_cnt: assert current_cnt >= 0 if self.show_eta and self.start_time and total_cnt: eta = get_eta(self.start_time, current_cnt, total_cnt) eta_str = " " + str_tdelta(eta) else: eta_str = "" if self.show_spinner: spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' ' else: spin_str = '' # always update this; it's also used for the bar self.spin_pos += 1 if self.show_pct and total_cnt and current_cnt: pct = 100.0 * current_cnt / total_cnt pct_str = ' (%5.1f%%)' % pct else: pct_str = '' if not self.show_count: count_str = '' elif current_cnt is None: count_str = '' elif total_cnt is None: count_str = ' %i' % (current_cnt) else: # make both fields the same size t = '%i' % (total_cnt) c = '%*i' % (len(t), current_cnt) count_str = ' ' + c + '/' + t if self.show_bar: # progress bar, if present, soaks up all remaining space cols = width - 1 - len(msg) - len(spin_str) - len(pct_str) \ - len(eta_str) - len(count_str) - 3 if total_cnt: # number of markers highlighted in bar markers = int(round(float(cols) * current_cnt / total_cnt)) bar_str = '[' + ('=' * markers).ljust(cols) + '] ' else: # don't know total, so can't show completion. # so just show an expanded spinning thingy m = self.spin_pos % cols ms = (' ' * m + '*').ljust(cols) bar_str = '[' + ms + '] ' else: bar_str = '' m = spin_str + bar_str + msg + count_str + pct_str + eta_str assert len(m) < width self.to_file.write('\r' + m.ljust(width - 1)) #self.to_file.flush() def clear(self): if self.suppressed: return self.to_file.write('\r%s\r' % (' ' * (_width() - 1))) #self.to_file.flush() def str_tdelta(delt): if delt is None: return "-:--:--" delt = int(round(delt)) return '%d:%02d:%02d' % (delt/3600, (delt/60) % 60, delt % 60) def get_eta(start_time, current, total, enough_samples=3): if start_time is None: return None if not total: return None if current < enough_samples: return None if current > total: return None # wtf? elapsed = time.time() - start_time if elapsed < 2.0: # not enough time to estimate return None total_duration = float(elapsed) * float(total) / float(current) assert total_duration >= elapsed return total_duration - elapsed def run_tests(): import doctest result = doctest.testmod() if result[1] > 0: if result[0] == 0: print "All tests passed" else: print "No tests to run" def demo(): from time import sleep pb = ProgressBar(show_pct=True, show_bar=True, show_spinner=False) for i in range(100): pb.update('Elephanten', i, 99) sleep(0.1) sleep(2) pb.clear() sleep(1) print 'done!' if __name__ == "__main__": demo() commit refs/heads/master mark :669 committer Martin Pool 1118453602 +1000 data 52 - don't show progress bar unless completion is known from :668 M 644 inline bzrlib/progress.py data 7545 # Copyright (C) 2005 Aaron Bentley # Copyright (C) 2005 Canonical # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ Simple text-mode progress indicator. Everyone loves ascii art! To display an indicator, create a ProgressBar object. Call it, passing Progress objects indicating the current state. When done, call clear(). Progress is suppressed when output is not sent to a terminal, so as not to clutter log files. """ # TODO: remove functions in favour of keeping everything in one class # TODO: should be a global option e.g. --silent that disables progress # indicators, preferably without needing to adjust all code that # potentially calls them. # TODO: Perhaps don't write updates faster than a certain rate, say # 5/second. import sys import time def _width(): """Return estimated terminal width. TODO: Do something smart on Windows? TODO: Is there anything that gets a better update when the window is resized while the program is running? """ import os try: return int(os.environ['COLUMNS']) except (IndexError, KeyError, ValueError): return 80 def _supports_progress(f): return hasattr(f, 'isatty') and f.isatty() class ProgressBar(object): """Progress bar display object. Several options are available to control the display. These can be passed as parameters to the constructor or assigned at any time: show_pct Show percentage complete. show_spinner Show rotating baton. This ticks over on every update even if the values don't change. show_eta Show predicted time-to-completion. show_bar Show bar graph. show_count Show numerical counts. The output file should be in line-buffered or unbuffered mode. """ SPIN_CHARS = r'/-\|' MIN_PAUSE = 0.1 # seconds start_time = None last_update = None def __init__(self, to_file=sys.stderr, show_pct=False, show_spinner=False, show_eta=True, show_bar=True, show_count=True): object.__init__(self) self.to_file = to_file self.suppressed = not _supports_progress(self.to_file) self.spin_pos = 0 self.show_pct = show_pct self.show_spinner = show_spinner self.show_eta = show_eta self.show_bar = show_bar self.show_count = show_count def tick(self): self.update(self.last_msg, self.last_cnt, self.last_total) def update(self, msg, current_cnt=None, total_cnt=None): """Update and redraw progress bar.""" if self.suppressed: return # save these for the tick() function self.last_msg = msg self.last_cnt = current_cnt self.last_total = total_cnt now = time.time() if self.start_time is None: self.start_time = now else: interval = now - self.last_update if interval > 0 and interval < self.MIN_PAUSE: return self.last_update = now width = _width() if total_cnt: assert current_cnt <= total_cnt if current_cnt: assert current_cnt >= 0 if self.show_eta and self.start_time and total_cnt: eta = get_eta(self.start_time, current_cnt, total_cnt) eta_str = " " + str_tdelta(eta) else: eta_str = "" if self.show_spinner: spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' ' else: spin_str = '' # always update this; it's also used for the bar self.spin_pos += 1 if self.show_pct and total_cnt and current_cnt: pct = 100.0 * current_cnt / total_cnt pct_str = ' (%5.1f%%)' % pct else: pct_str = '' if not self.show_count: count_str = '' elif current_cnt is None: count_str = '' elif total_cnt is None: count_str = ' %i' % (current_cnt) else: # make both fields the same size t = '%i' % (total_cnt) c = '%*i' % (len(t), current_cnt) count_str = ' ' + c + '/' + t if self.show_bar: # progress bar, if present, soaks up all remaining space cols = width - 1 - len(msg) - len(spin_str) - len(pct_str) \ - len(eta_str) - len(count_str) - 3 if total_cnt: # number of markers highlighted in bar markers = int(round(float(cols) * current_cnt / total_cnt)) bar_str = '[' + ('=' * markers).ljust(cols) + '] ' elif False: # don't know total, so can't show completion. # so just show an expanded spinning thingy m = self.spin_pos % cols ms = (' ' * m + '*').ljust(cols) bar_str = '[' + ms + '] ' else: bar_str = '' else: bar_str = '' m = spin_str + bar_str + msg + count_str + pct_str + eta_str assert len(m) < width self.to_file.write('\r' + m.ljust(width - 1)) #self.to_file.flush() def clear(self): if self.suppressed: return self.to_file.write('\r%s\r' % (' ' * (_width() - 1))) #self.to_file.flush() def str_tdelta(delt): if delt is None: return "-:--:--" delt = int(round(delt)) return '%d:%02d:%02d' % (delt/3600, (delt/60) % 60, delt % 60) def get_eta(start_time, current, total, enough_samples=3): if start_time is None: return None if not total: return None if current < enough_samples: return None if current > total: return None # wtf? elapsed = time.time() - start_time if elapsed < 2.0: # not enough time to estimate return None total_duration = float(elapsed) * float(total) / float(current) assert total_duration >= elapsed return total_duration - elapsed def run_tests(): import doctest result = doctest.testmod() if result[1] > 0: if result[0] == 0: print "All tests passed" else: print "No tests to run" def demo(): from time import sleep pb = ProgressBar(show_pct=True, show_bar=True, show_spinner=False) for i in range(100): pb.update('Elephanten', i, 99) sleep(0.1) sleep(2) pb.clear() sleep(1) print 'done!' if __name__ == "__main__": demo() commit refs/heads/master mark :670 committer Martin Pool 1118453724 +1000 data 31 - Show progress while branching from :669 M 644 inline bzrlib/branch.py data 34911 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def _relpath(base, path): """Return path relative to base, or raise exception. The path may be either an absolute path or a path relative to the current working directory. Lifted out of Branch.relpath for ease of testing. os.path.commonprefix (python2.4) has a bad bug that it works just on string prefixes, assuming that '/u' is a prefix of '/u2'. This avoids that problem.""" rp = os.path.abspath(path) s = [] head = rp while len(head) >= len(base): if head == base: break head, tail = os.path.split(head) if tail: s.insert(0, tail) else: from errors import NotBranchError raise NotBranchError("path %r is not within branch %r" % (rp, base)) return os.sep.join(s) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head class DivergedBranches(Exception): def __init__(self, branch1, branch2): self.branch1 = branch1 self.branch2 = branch2 Exception.__init__(self, "These branches have diverged.") ###################################################################### # branch objects class Branch(object): """Branch holding a history of revisions. base Base directory of the branch. _lock_mode None, or 'r' or 'w' _lock_count If _lock_mode is true, a positive count of the number of times the lock has been taken. _lock Lock object from bzrlib.lock. """ base = None _lock_mode = None _lock_count = None _lock = None def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): from errors import NotBranchError raise NotBranchError("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def __del__(self): if self._lock_mode or self._lock: from warnings import warn warn("branch %r was not explicitly unlocked" % self) self._lock.unlock() def lock_write(self): if self._lock_mode: if self._lock_mode != 'w': from errors import LockError raise LockError("can't upgrade to a write lock from %r" % self._lock_mode) self._lock_count += 1 else: from bzrlib.lock import WriteLock self._lock = WriteLock(self.controlfilename('branch-lock')) self._lock_mode = 'w' self._lock_count = 1 def lock_read(self): if self._lock_mode: assert self._lock_mode in ('r', 'w'), \ "invalid lock mode %r" % self._lock_mode self._lock_count += 1 else: from bzrlib.lock import ReadLock self._lock = ReadLock(self.controlfilename('branch-lock')) self._lock_mode = 'r' self._lock_count = 1 def unlock(self): if not self._lock_mode: from errors import LockError raise LockError('branch %r is not locked' % (self)) if self._lock_count > 1: self._lock_count -= 1 else: self._lock.unlock() self._lock = None self._lock_mode = self._lock_count = None def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" return _relpath(self.base, path) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: raise BzrError('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. self.lock_read() try: inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv finally: self.unlock() def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. files List of paths to add, relative to the base of the tree. ids If set, use these instead of automatically generated ids. Must be the same length as the list of files, but may contain None for ids that are to be autogenerated. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) self.lock_write() try: inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): raise BzrError("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: raise BzrError("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) finally: self.unlock() def print_file(self, file, revno): """Print `file` to stdout.""" self.lock_read() try: tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: raise BzrError("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) finally: self.unlock() def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] self.lock_write() try: tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: raise BzrError("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) finally: self.unlock() # FIXME: this doesn't need to be a branch method def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" if not revision_id or not isinstance(revision_id, basestring): raise ValueError('invalid revision-id: %r' % revision_id) r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self.lock_read() try: return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] finally: self.unlock() def common_ancestor(self, other, self_revno=None, other_revno=None): """ >>> import commit >>> sb = ScratchBranch(files=['foo', 'foo~']) >>> sb.common_ancestor(sb) == (None, None) True >>> commit.commit(sb, "Committing first revision", verbose=False) >>> sb.common_ancestor(sb)[0] 1 >>> clone = sb.clone() >>> commit.commit(sb, "Committing second revision", verbose=False) >>> sb.common_ancestor(sb)[0] 2 >>> sb.common_ancestor(clone)[0] 1 >>> commit.commit(clone, "Committing divergent second revision", ... verbose=False) >>> sb.common_ancestor(clone)[0] 1 >>> sb.common_ancestor(clone) == clone.common_ancestor(sb) True >>> sb.common_ancestor(sb) != clone.common_ancestor(clone) True >>> clone2 = sb.clone() >>> sb.common_ancestor(clone2)[0] 2 >>> sb.common_ancestor(clone2, self_revno=1)[0] 1 >>> sb.common_ancestor(clone2, other_revno=1)[0] 1 """ my_history = self.revision_history() other_history = other.revision_history() if self_revno is None: self_revno = len(my_history) if other_revno is None: other_revno = len(other_history) indices = range(min((self_revno, other_revno))) indices.reverse() for r in indices: if my_history[r] == other_history[r]: return r+1, my_history[r] return None, None def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def missing_revisions(self, other): """ If self and other have not diverged, return a list of the revisions present in other, but missing from self. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch() >>> br2 = ScratchBranch() >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [u'REVISION-ID-1'] >>> br2.missing_revisions(br1) [] >>> commit(br1, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-2A") >>> br1.missing_revisions(br2) [u'REVISION-ID-2A'] >>> commit(br1, "lala!", rev_id="REVISION-ID-2B") >>> br1.missing_revisions(br2) Traceback (most recent call last): DivergedBranches: These branches have diverged. """ self_history = self.revision_history() self_len = len(self_history) other_history = other.revision_history() other_len = len(other_history) common_index = min(self_len, other_len) -1 if common_index >= 0 and \ self_history[common_index] != other_history[common_index]: raise DivergedBranches(self, other) if self_len < other_len: return other_history[self_len:] return [] def update_revisions(self, other): """Pull in all new revisions from other branch. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch(files=['foo', 'bar']) >>> br1.add('foo') >>> br1.add('bar') >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False) >>> br2 = ScratchBranch() >>> br2.update_revisions(br1) Added 2 texts. Added 1 inventories. Added 1 revisions. >>> br2.revision_history() [u'REVISION-ID-1'] >>> br2.update_revisions(br1) Added 0 texts. Added 0 inventories. Added 0 revisions. >>> br1.text_store.total_size() == br2.text_store.total_size() True """ from bzrlib.progress import ProgressBar pb = ProgressBar() pb.update('comparing histories') revision_ids = self.missing_revisions(other) revisions = [] needed_texts = sets.Set() i = 0 for rev_id in revision_ids: i += 1 pb.update('fetching revision', i, len(revision_ids)) rev = other.get_revision(rev_id) revisions.append(rev) inv = other.get_inventory(str(rev.inventory_id)) for key, entry in inv.iter_entries(): if entry.text_id is None: continue if entry.text_id not in self.text_store: needed_texts.add(entry.text_id) pb.clear() count = self.text_store.copy_multi(other.text_store, needed_texts) print "Added %d texts." % count inventory_ids = [ f.inventory_id for f in revisions ] count = self.inventory_store.copy_multi(other.inventory_store, inventory_ids) print "Added %d inventories." % count revision_ids = [ f.revision_id for f in revisions] count = self.revision_store.copy_multi(other.revision_store, revision_ids) for revision_id in revision_ids: self.append_revision(revision_id) print "Added %d revisions." % count def commit(self, *args, **kw): """Deprecated""" from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self.lock_write() try: tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): raise BzrError("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): raise BzrError("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: raise BzrError("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): raise BzrError("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': raise BzrError("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self.lock_write() try: ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): raise BzrError("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): raise BzrError("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': raise BzrError("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): raise BzrError("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): raise BzrError("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: raise BzrError("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): raise BzrError("destination %r already exists" % dest_path) if f_id in to_idpath: raise BzrError("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[], base=None): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ init = False if base is None: base = tempfile.mkdtemp() init = True Branch.__init__(self, base, init=init) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def clone(self): """ >>> orig = ScratchBranch(files=["file1", "file2"]) >>> clone = orig.clone() >>> os.path.samefile(orig.base, clone.base) False >>> os.path.isfile(os.path.join(clone.base, "file1")) True """ base = tempfile.mkdtemp() os.rmdir(base) shutil.copytree(self.base, base, symlinks=True) return ScratchBranch(base=base) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: if self.base: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/store.py data 6033 # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Stores are the main data-storage mechanism for Bazaar-NG. A store is a simple write-once container indexed by a universally unique ID, which is typically the SHA-1 of the content.""" __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import os, tempfile, types, osutils, gzip, errno from stat import ST_SIZE from StringIO import StringIO from trace import mutter ###################################################################### # stores class StoreError(Exception): pass class ImmutableStore(object): """Store that holds files indexed by unique names. Files can be added, but not modified once they are in. Typically the hash is used as the name, or something else known to be unique, such as a UUID. >>> st = ImmutableScratchStore() >>> st.add(StringIO('hello'), 'aa') >>> 'aa' in st True >>> 'foo' in st False You are not allowed to add an id that is already present. Entries can be retrieved as files, which may then be read. >>> st.add(StringIO('goodbye'), '123123') >>> st['123123'].read() 'goodbye' TODO: Atomic add by writing to a temporary file and renaming. TODO: Perhaps automatically transform to/from XML in a method? Would just need to tell the constructor what class to use... TODO: Even within a simple disk store like this, we could gzip the files. But since many are less than one disk block, that might not help a lot. """ def __init__(self, basedir): """ImmutableStore constructor.""" self._basedir = basedir def _path(self, id): assert '/' not in id return os.path.join(self._basedir, id) def __repr__(self): return "%s(%r)" % (self.__class__.__name__, self._basedir) def add(self, f, fileid, compressed=True): """Add contents of a file into the store. f -- An open file, or file-like object.""" # FIXME: Only works on smallish files # TODO: Can be optimized by copying at the same time as # computing the sum. mutter("add store entry %r" % (fileid)) if isinstance(f, types.StringTypes): content = f else: content = f.read() p = self._path(fileid) if os.access(p, os.F_OK) or os.access(p + '.gz', os.F_OK): bailout("store %r already contains id %r" % (self._basedir, fileid)) if compressed: f = gzip.GzipFile(p + '.gz', 'wb') os.chmod(p + '.gz', 0444) else: f = file(p, 'wb') os.chmod(p, 0444) f.write(content) f.close() def copy_multi(self, other, ids): """Copy texts for ids from other into self. If an id is present in self, it is skipped. A count of copied ids is returned, which may be less than len(ids). """ from bzrlib.progress import ProgressBar pb = ProgressBar() pb.update('preparing to copy') to_copy = [id for id in ids if id not in self] count = 0 for id in to_copy: count += 1 pb.update('copy', count, len(to_copy)) self.add(other[id], id) assert count == len(to_copy) pb.clear() return count def __contains__(self, fileid): """""" p = self._path(fileid) return (os.access(p, os.R_OK) or os.access(p + '.gz', os.R_OK)) # TODO: Guard against the same thing being stored twice, compressed and uncompresse def __iter__(self): for f in os.listdir(self._basedir): if f[-3:] == '.gz': # TODO: case-insensitive? yield f[:-3] else: yield f def __len__(self): return len(os.listdir(self._basedir)) def __getitem__(self, fileid): """Returns a file reading from a particular entry.""" p = self._path(fileid) try: return gzip.GzipFile(p + '.gz', 'rb') except IOError, e: if e.errno == errno.ENOENT: return file(p, 'rb') else: raise e def total_size(self): """Return (count, bytes) This is the (compressed) size stored on disk, not the size of the content.""" total = 0 count = 0 for fid in self: count += 1 p = self._path(fid) try: total += os.stat(p)[ST_SIZE] except OSError: total += os.stat(p + '.gz')[ST_SIZE] return count, total class ImmutableScratchStore(ImmutableStore): """Self-destructing test subclass of ImmutableStore. The Store only exists for the lifetime of the Python object. Obviously you should not put anything precious in it. """ def __init__(self): ImmutableStore.__init__(self, tempfile.mkdtemp()) def __del__(self): for f in os.listdir(self._basedir): fpath = os.path.join(self._basedir, f) # needed on windows, and maybe some other filesystems os.chmod(fpath, 0600) os.remove(fpath) os.rmdir(self._basedir) mutter("%r destroyed" % self) commit refs/heads/master mark :671 committer Martin Pool 1118453769 +1000 data 104 - Don't create an empty destination directory when branch fails to open the source. Fix from aaron. from :670 M 644 inline bzrlib/commands.py data 46516 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _find_plugins(): """Find all python files which are plugins, and load their commands to add to the list of "all commands" The environment variable BZRPATH is considered a delimited set of paths to look through. Each entry is searched for *.py files. If a directory is found, it is also searched, but they are not searched recursively. This allows you to revctl the plugins. Inside the plugin should be a series of cmd_* function, which inherit from the bzrlib.commands.Command class. """ bzrpath = os.environ.get('BZRPLUGINPATH', '') plugin_cmds = {} if not bzrpath: return plugin_cmds _platform_extensions = { 'win32':'.pyd', 'cygwin':'.dll', 'darwin':'.dylib', 'linux2':'.so' } if _platform_extensions.has_key(sys.platform): platform_extension = _platform_extensions[sys.platform] else: platform_extension = None for d in bzrpath.split(os.pathsep): plugin_names = {} # This should really be a set rather than a dict for f in os.listdir(d): if f.endswith('.py'): f = f[:-3] elif f.endswith('.pyc') or f.endswith('.pyo'): f = f[:-4] elif platform_extension and f.endswith(platform_extension): f = f[:-len(platform_extension)] if f.endswidth('module'): f = f[:-len('module')] else: continue if not plugin_names.has_key(f): plugin_names[f] = True plugin_names = plugin_names.keys() plugin_names.sort() try: sys.path.insert(0, d) for name in plugin_names: try: old_module = None try: if sys.modules.has_key(name): old_module = sys.modules[name] del sys.modules[name] plugin = __import__(name, locals()) for k in dir(plugin): if k.startswith('cmd_'): k_unsquished = _unsquish_command_name(k) if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = getattr(plugin, k) else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r in dir %r' % (name, d)) finally: if old_module: sys.modules[name] = old_module except ImportError, e: log_error('Unable to load plugin: %r from %r\n%s' % (name, d, e)) finally: sys.path.pop(0) return plugin_cmds def _get_cmd_dict(include_plugins=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v if include_plugins: d.update(_find_plugins()) return d def get_all_cmds(include_plugins=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(include_plugins=include_plugins).iteritems(): yield k,v def get_cmd_class(cmd,include_plugins=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(include_plugins=include_plugins) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import errno br_to = Branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if errno == errno.ENOENT: raise if location is None: location = stored_loc if location is None: raise BzrCommandError("No pull location known or specified.") from branch import find_branch, DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged. Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. """ takes_args = ['from_location', 'to_location?'] def run(self, from_location, to_location=None): import errno from bzrlib.merge import merge from branch import find_branch, DivergedBranches try: br_from = find_branch(from_location) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location) # FIXME: If there's a trailing slash, keep removing them # until we find the right bit try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) from_location = pull_loc(br_from) br_to.update_revisions(br_from) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False) br_to.controlfile("x-pull", "wb").write(from_location + "\n") def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest if selftest(): return 0 else: return 1 class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] include_plugins=True try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd,include_plugins=include_plugins) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :672 committer Martin Pool 1118453977 +1000 data 109 - revision records include the hash of their inventory and of their predecessor. patch from John A Meinel from :671 M 644 inline bzrlib/branch.py data 35630 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_file, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def _relpath(base, path): """Return path relative to base, or raise exception. The path may be either an absolute path or a path relative to the current working directory. Lifted out of Branch.relpath for ease of testing. os.path.commonprefix (python2.4) has a bad bug that it works just on string prefixes, assuming that '/u' is a prefix of '/u2'. This avoids that problem.""" rp = os.path.abspath(path) s = [] head = rp while len(head) >= len(base): if head == base: break head, tail = os.path.split(head) if tail: s.insert(0, tail) else: from errors import NotBranchError raise NotBranchError("path %r is not within branch %r" % (rp, base)) return os.sep.join(s) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head class DivergedBranches(Exception): def __init__(self, branch1, branch2): self.branch1 = branch1 self.branch2 = branch2 Exception.__init__(self, "These branches have diverged.") ###################################################################### # branch objects class Branch(object): """Branch holding a history of revisions. base Base directory of the branch. _lock_mode None, or 'r' or 'w' _lock_count If _lock_mode is true, a positive count of the number of times the lock has been taken. _lock Lock object from bzrlib.lock. """ base = None _lock_mode = None _lock_count = None _lock = None def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): from errors import NotBranchError raise NotBranchError("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def __del__(self): if self._lock_mode or self._lock: from warnings import warn warn("branch %r was not explicitly unlocked" % self) self._lock.unlock() def lock_write(self): if self._lock_mode: if self._lock_mode != 'w': from errors import LockError raise LockError("can't upgrade to a write lock from %r" % self._lock_mode) self._lock_count += 1 else: from bzrlib.lock import WriteLock self._lock = WriteLock(self.controlfilename('branch-lock')) self._lock_mode = 'w' self._lock_count = 1 def lock_read(self): if self._lock_mode: assert self._lock_mode in ('r', 'w'), \ "invalid lock mode %r" % self._lock_mode self._lock_count += 1 else: from bzrlib.lock import ReadLock self._lock = ReadLock(self.controlfilename('branch-lock')) self._lock_mode = 'r' self._lock_count = 1 def unlock(self): if not self._lock_mode: from errors import LockError raise LockError('branch %r is not locked' % (self)) if self._lock_count > 1: self._lock_count -= 1 else: self._lock.unlock() self._lock = None self._lock_mode = self._lock_count = None def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" return _relpath(self.base, path) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: raise BzrError('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. self.lock_read() try: inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv finally: self.unlock() def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. files List of paths to add, relative to the base of the tree. ids If set, use these instead of automatically generated ids. Must be the same length as the list of files, but may contain None for ids that are to be autogenerated. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) self.lock_write() try: inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): raise BzrError("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: raise BzrError("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) finally: self.unlock() def print_file(self, file, revno): """Print `file` to stdout.""" self.lock_read() try: tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: raise BzrError("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) finally: self.unlock() def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] self.lock_write() try: tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: raise BzrError("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) finally: self.unlock() # FIXME: this doesn't need to be a branch method def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" if not revision_id or not isinstance(revision_id, basestring): raise ValueError('invalid revision-id: %r' % revision_id) r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_revision_sha1(self, revision_id): """Hash the stored value of a revision, and return it.""" # In the future, revision entries will be signed. At that # point, it is probably best *not* to include the signature # in the revision hash. Because that lets you re-sign # the revision, (add signatures/remove signatures) and still # have all hash pointers stay consistent. # But for now, just hash the contents. return sha_file(self.revision_store[revision_id]) def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_inventory_sha1(self, inventory_id): """Return the sha1 hash of the inventory entry """ return sha_file(self.inventory_store[inventory_id]) def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self.lock_read() try: return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] finally: self.unlock() def common_ancestor(self, other, self_revno=None, other_revno=None): """ >>> import commit >>> sb = ScratchBranch(files=['foo', 'foo~']) >>> sb.common_ancestor(sb) == (None, None) True >>> commit.commit(sb, "Committing first revision", verbose=False) >>> sb.common_ancestor(sb)[0] 1 >>> clone = sb.clone() >>> commit.commit(sb, "Committing second revision", verbose=False) >>> sb.common_ancestor(sb)[0] 2 >>> sb.common_ancestor(clone)[0] 1 >>> commit.commit(clone, "Committing divergent second revision", ... verbose=False) >>> sb.common_ancestor(clone)[0] 1 >>> sb.common_ancestor(clone) == clone.common_ancestor(sb) True >>> sb.common_ancestor(sb) != clone.common_ancestor(clone) True >>> clone2 = sb.clone() >>> sb.common_ancestor(clone2)[0] 2 >>> sb.common_ancestor(clone2, self_revno=1)[0] 1 >>> sb.common_ancestor(clone2, other_revno=1)[0] 1 """ my_history = self.revision_history() other_history = other.revision_history() if self_revno is None: self_revno = len(my_history) if other_revno is None: other_revno = len(other_history) indices = range(min((self_revno, other_revno))) indices.reverse() for r in indices: if my_history[r] == other_history[r]: return r+1, my_history[r] return None, None def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def missing_revisions(self, other): """ If self and other have not diverged, return a list of the revisions present in other, but missing from self. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch() >>> br2 = ScratchBranch() >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [u'REVISION-ID-1'] >>> br2.missing_revisions(br1) [] >>> commit(br1, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-2A") >>> br1.missing_revisions(br2) [u'REVISION-ID-2A'] >>> commit(br1, "lala!", rev_id="REVISION-ID-2B") >>> br1.missing_revisions(br2) Traceback (most recent call last): DivergedBranches: These branches have diverged. """ self_history = self.revision_history() self_len = len(self_history) other_history = other.revision_history() other_len = len(other_history) common_index = min(self_len, other_len) -1 if common_index >= 0 and \ self_history[common_index] != other_history[common_index]: raise DivergedBranches(self, other) if self_len < other_len: return other_history[self_len:] return [] def update_revisions(self, other): """Pull in all new revisions from other branch. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch(files=['foo', 'bar']) >>> br1.add('foo') >>> br1.add('bar') >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False) >>> br2 = ScratchBranch() >>> br2.update_revisions(br1) Added 2 texts. Added 1 inventories. Added 1 revisions. >>> br2.revision_history() [u'REVISION-ID-1'] >>> br2.update_revisions(br1) Added 0 texts. Added 0 inventories. Added 0 revisions. >>> br1.text_store.total_size() == br2.text_store.total_size() True """ from bzrlib.progress import ProgressBar pb = ProgressBar() pb.update('comparing histories') revision_ids = self.missing_revisions(other) revisions = [] needed_texts = sets.Set() i = 0 for rev_id in revision_ids: i += 1 pb.update('fetching revision', i, len(revision_ids)) rev = other.get_revision(rev_id) revisions.append(rev) inv = other.get_inventory(str(rev.inventory_id)) for key, entry in inv.iter_entries(): if entry.text_id is None: continue if entry.text_id not in self.text_store: needed_texts.add(entry.text_id) pb.clear() count = self.text_store.copy_multi(other.text_store, needed_texts) print "Added %d texts." % count inventory_ids = [ f.inventory_id for f in revisions ] count = self.inventory_store.copy_multi(other.inventory_store, inventory_ids) print "Added %d inventories." % count revision_ids = [ f.revision_id for f in revisions] count = self.revision_store.copy_multi(other.revision_store, revision_ids) for revision_id in revision_ids: self.append_revision(revision_id) print "Added %d revisions." % count def commit(self, *args, **kw): """Deprecated""" from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self.lock_write() try: tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): raise BzrError("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): raise BzrError("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: raise BzrError("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): raise BzrError("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': raise BzrError("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self.lock_write() try: ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): raise BzrError("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): raise BzrError("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': raise BzrError("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): raise BzrError("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): raise BzrError("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: raise BzrError("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): raise BzrError("destination %r already exists" % dest_path) if f_id in to_idpath: raise BzrError("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[], base=None): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ init = False if base is None: base = tempfile.mkdtemp() init = True Branch.__init__(self, base, init=init) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def clone(self): """ >>> orig = ScratchBranch(files=["file1", "file2"]) >>> clone = orig.clone() >>> os.path.samefile(orig.base, clone.base) False >>> os.path.isfile(os.path.join(clone.base, "file1")) True """ base = tempfile.mkdtemp() os.rmdir(base) shutil.copytree(self.base, base, symlinks=True) return ScratchBranch(base=base) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: if self.base: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/commit.py data 10466 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def commit(branch, message, timestamp=None, timezone=None, committer=None, verbose=True, specific_files=None, rev_id=None): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. specific_files If true, commit only those files. rev_id If set, use this as the new revision id. Useful for test or import commands that need to tightly control what revisions are assigned. If you duplicate a revision id that exists elsewhere it is your own fault. If null (default), a time/random revision id is generated. """ import time, tempfile from osutils import local_time_offset, username from branch import gen_file_id from errors import BzrError from revision import Revision from trace import mutter, note branch.lock_write() try: # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_tree = branch.working_tree() work_inv = work_tree.inventory basis = branch.basis_tree() basis_inv = basis.inventory if verbose: note('looking for changes...') missing_ids, new_inv = _gather_commit(branch, work_tree, work_inv, basis_inv, specific_files, verbose) for file_id in missing_ids: # Any files that have been deleted are now removed from the # working inventory. Files that were not selected for commit # are left as they were in the working inventory and ommitted # from the revision inventory. # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itbranch. if work_inv.has_id(file_id): del work_inv[file_id] if rev_id is None: rev_id = _gen_revision_id(time.time()) inv_id = rev_id inv_tmp = tempfile.TemporaryFile() new_inv.write_xml(inv_tmp) inv_tmp.seek(0) branch.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) # We could also just sha hash the inv_tmp file # however, in the case that branch.inventory_store.add() # ever actually does anything special inv_sha1 = branch.get_inventory_sha1(inv_id) precursor = branch.last_patch() if precursor: precursor_sha1 = branch.get_revision_sha1(precursor) else: precursor_sha1 = None branch._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, precursor = precursor, precursor_sha1 = precursor_sha1, message = message, inventory_id=inv_id, inventory_sha1=inv_sha1, revision_id=rev_id) rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) branch.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (branch.revno() + 1)) branch.append_revision(rev_id) if verbose: note("commited r%d" % branch.revno()) finally: branch.unlock() def _gen_revision_id(when): """Return new revision-id.""" from binascii import hexlify from osutils import rand_bytes, compact_date, user_email s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def _gather_commit(branch, work_tree, work_inv, basis_inv, specific_files, verbose): """Build inventory preparatory to commit. This adds any changed files into the text store, and sets their test-id, sha and size in the returned inventory appropriately. missing_ids Modified to hold a list of files that have been deleted from the working directory; these should be removed from the working inventory. """ from bzrlib.inventory import Inventory from osutils import isdir, isfile, sha_string, quotefn, \ local_time_offset, username, kind_marker, is_inside_any from branch import gen_file_id from errors import BzrError from revision import Revision from bzrlib.trace import mutter, note inv = Inventory() missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). p = branch.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if specific_files and not is_inside_any(specific_files, path): if basis_inv.has_id(file_id): # carry over with previous state inv.add(basis_inv[file_id].copy()) else: # omit this from committed inventory pass continue if not work_tree.has_id(file_id): if verbose: print('deleted %s%s' % (path, kind_marker(entry.kind))) mutter(" file is missing, removing from inventory") missing_ids.append(file_id) continue # this is present in the new inventory; may be new, modified or # unchanged. old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] entry = entry.copy() inv.add(entry) if old_ie: old_kind = old_ie.kind if old_kind != entry.kind: raise BzrError("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): raise BzrError("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): raise BzrError("%s is entered as file but is not a file" % quotefn(p)) new_sha1 = work_tree.get_file_sha1(file_id) if (old_ie and old_ie.text_sha1 == new_sha1): ## assert content == basis.get_file(file_id).read() entry.text_id = old_ie.text_id entry.text_sha1 = new_sha1 entry.text_size = old_ie.text_size mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: content = file(p, 'rb').read() # calculate the sha again, just in case the file contents # changed since we updated the cache entry.text_sha1 = sha_string(content) entry.text_size = len(content) entry.text_id = gen_file_id(entry.name) branch.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: marked = path + kind_marker(entry.kind) if not old_ie: print 'added', marked elif old_ie == entry: pass # unchanged elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): print 'modified', marked else: print 'renamed', marked return missing_ids, inv M 644 inline bzrlib/revision.py data 3237 # (C) 2005 Canonical # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from xml import XMLMixin try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from errors import BzrError class Revision(XMLMixin): """Single revision on a branch. Revisions may know their revision_hash, but only once they've been written out. This is not stored because you cannot write the hash into the file it describes. TODO: Perhaps make predecessor be a child element, not an attribute? """ def __init__(self, **args): self.inventory_id = None self.inventory_sha1 = None self.revision_id = None self.timestamp = None self.message = None self.timezone = None self.committer = None self.precursor = None self.precursor_sha1 = None self.__dict__.update(args) def __repr__(self): return "" % self.revision_id def to_element(self): root = Element('revision', committer = self.committer, timestamp = '%.9f' % self.timestamp, revision_id = self.revision_id, inventory_id = self.inventory_id, inventory_sha1 = self.inventory_sha1, timezone = str(self.timezone)) if self.precursor: root.set('precursor', self.precursor) if self.precursor_sha1: root.set('precursor_sha1', self.precursor_sha1) root.text = '\n' msg = SubElement(root, 'message') msg.text = self.message msg.tail = '\n' return root def from_element(cls, elt): # is deprecated... if elt.tag not in ('revision', 'changeset'): raise BzrError("unexpected tag in revision file: %r" % elt) cs = cls(committer = elt.get('committer'), timestamp = float(elt.get('timestamp')), precursor = elt.get('precursor'), precursor_sha1 = elt.get('precursor_sha1'), revision_id = elt.get('revision_id'), inventory_id = elt.get('inventory_id'), inventory_sha1 = elt.get('inventory_sha1') ) v = elt.get('timezone') cs.timezone = v and int(v) cs.message = elt.findtext('message') # text of return cs from_element = classmethod(from_element) commit refs/heads/master mark :673 committer Martin Pool 1118454654 +1000 data 84 - cope when writing out revisions with no timezone recorded fix from John A Meinel from :672 M 644 inline bzrlib/revision.py data 3287 # (C) 2005 Canonical # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from xml import XMLMixin try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from errors import BzrError class Revision(XMLMixin): """Single revision on a branch. Revisions may know their revision_hash, but only once they've been written out. This is not stored because you cannot write the hash into the file it describes. TODO: Perhaps make predecessor be a child element, not an attribute? """ def __init__(self, **args): self.inventory_id = None self.inventory_sha1 = None self.revision_id = None self.timestamp = None self.message = None self.timezone = None self.committer = None self.precursor = None self.precursor_sha1 = None self.__dict__.update(args) def __repr__(self): return "" % self.revision_id def to_element(self): root = Element('revision', committer = self.committer, timestamp = '%.9f' % self.timestamp, revision_id = self.revision_id, inventory_id = self.inventory_id, inventory_sha1 = self.inventory_sha1, ) if self.timezone: root.set('timezone', str(self.timezone)) if self.precursor: root.set('precursor', self.precursor) if self.precursor_sha1: root.set('precursor_sha1', self.precursor_sha1) root.text = '\n' msg = SubElement(root, 'message') msg.text = self.message msg.tail = '\n' return root def from_element(cls, elt): # is deprecated... if elt.tag not in ('revision', 'changeset'): raise BzrError("unexpected tag in revision file: %r" % elt) cs = cls(committer = elt.get('committer'), timestamp = float(elt.get('timestamp')), precursor = elt.get('precursor'), precursor_sha1 = elt.get('precursor_sha1'), revision_id = elt.get('revision_id'), inventory_id = elt.get('inventory_id'), inventory_sha1 = elt.get('inventory_sha1') ) v = elt.get('timezone') cs.timezone = v and int(v) cs.message = elt.findtext('message') # text of return cs from_element = classmethod(from_element) commit refs/heads/master mark :674 committer Martin Pool 1118454767 +1000 data 159 - check command now also checks new inventory_sha1 and precursor_sha1 - check has --update option to fix these if they're missing patch from John A Meinel from :673 M 644 inline bzrlib/check.py data 8294 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def check(branch, update=False): """Run consistency checks on a branch. If update is True, for revisions missing certain information (right now this is inventory_sha1 and revision_sha1), update them to include the appropriate values. """ import sys from bzrlib.trace import mutter from bzrlib.errors import BzrCheckError from bzrlib.osutils import fingerprint_file from bzrlib.progress import ProgressBar out = sys.stdout pb = ProgressBar(show_spinner=True) last_ptr = None checked_revs = {} missing_inventory_sha_cnt = 0 history = branch.revision_history() revno = 0 revcount = len(history) checked_texts = {} updated_revisions = [] # Set to True in the case that the previous revision entry # was updated, since this will affect future revision entries updated_previous_revision = False for rid in history: revno += 1 pb.update('checking revision', revno, revcount) mutter(' revision {%s}' % rid) rev = branch.get_revision(rid) if rev.revision_id != rid: raise BzrCheckError('wrong internal revision id in revision {%s}' % rid) if rev.precursor != last_ptr: raise BzrCheckError('mismatched precursor in revision {%s}' % rid) last_ptr = rid if rid in checked_revs: raise BzrCheckError('repeated revision {%s}' % rid) checked_revs[rid] = True ## TODO: Check all the required fields are present on the revision. updated = False if rev.inventory_sha1: #mutter(' checking inventory hash {%s}' % rev.inventory_sha1) inv_sha1 = branch.get_inventory_sha1(rev.inventory_id) if inv_sha1 != rev.inventory_sha1: raise BzrCheckError('Inventory sha1 hash doesn\'t match' ' value in revision {%s}' % rid) elif update: inv_sha1 = branch.get_inventory_sha1(rev.inventory_id) rev.inventory_sha1 = inv_sha1 updated = True else: missing_inventory_sha_cnt += 1 mutter("no inventory_sha1 on revision {%s}" % rid) if rev.precursor: if rev.precursor_sha1: precursor_sha1 = branch.get_revision_sha1(rev.precursor) if updated_previous_revision: # we don't expect the hashes to match, because # we had to modify the previous revision_history entry. rev.precursor_sha1 = precursor_sha1 updated = True else: #mutter(' checking precursor hash {%s}' % rev.precursor_sha1) if rev.precursor_sha1 != precursor_sha1: raise BzrCheckError('Precursor sha1 hash doesn\'t match' ' value in revision {%s}' % rid) elif update: precursor_sha1 = branch.get_revision_sha1(rev.precursor) rev.precursor_sha1 = precursor_sha1 updated = True if updated: updated_previous_revision = True # We had to update this revision entries hashes # Now we need to write out a new value # This is a little bit invasive, since we are *rewriting* a # revision entry. I'm not supremely happy about it, but # there should be *some* way of making old entries have # the full meta information. import tempfile, os, errno rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) tmpfd, tmp_path = tempfile.mkstemp(prefix=rid, suffix='.gz', dir=branch.controlfilename('revision-store')) os.close(tmpfd) def special_rename(p1, p2): if sys.platform == 'win32': try: os.remove(p2) except OSError, e: if e.errno != e.ENOENT: raise os.rename(p1, p2) try: # TODO: We may need to handle the case where the old revision # entry was not compressed (and thus did not end with .gz) # Remove the old revision entry out of the way rev_path = branch.controlfilename(['revision-store', rid+'.gz']) special_rename(rev_path, tmp_path) branch.revision_store.add(rev_tmp, rid) # Add the new one os.remove(tmp_path) # Remove the old name mutter(' Updated revision entry {%s}' % rid) except: # On any exception, restore the old entry special_rename(tmp_path, rev_path) raise rev_tmp.close() updated_revisions.append(rid) else: updated_previous_revision = False inv = branch.get_inventory(rev.inventory_id) seen_ids = {} seen_names = {} ## p('revision %d/%d file ids' % (revno, revcount)) for file_id in inv: if file_id in seen_ids: raise BzrCheckError('duplicated file_id {%s} ' 'in inventory for revision {%s}' % (file_id, rid)) seen_ids[file_id] = True i = 0 len_inv = len(inv) for file_id in inv: i += 1 if i & 31 == 0: pb.tick() ie = inv[file_id] if ie.parent_id != None: if ie.parent_id not in seen_ids: raise BzrCheckError('missing parent {%s} in inventory for revision {%s}' % (ie.parent_id, rid)) if ie.kind == 'file': if ie.text_id in checked_texts: fp = checked_texts[ie.text_id] else: if not ie.text_id in branch.text_store: raise BzrCheckError('text {%s} not in text_store' % ie.text_id) tf = branch.text_store[ie.text_id] fp = fingerprint_file(tf) checked_texts[ie.text_id] = fp if ie.text_size != fp['size']: raise BzrCheckError('text {%s} wrong size' % ie.text_id) if ie.text_sha1 != fp['sha1']: raise BzrCheckError('text {%s} wrong sha1' % ie.text_id) elif ie.kind == 'directory': if ie.text_sha1 != None or ie.text_size != None or ie.text_id != None: raise BzrCheckError('directory {%s} has text in revision {%s}' % (file_id, rid)) pb.tick() for path, ie in inv.iter_entries(): if path in seen_names: raise BzrCheckError('duplicated path %r ' 'in inventory for revision {%s}' % (path, revid)) seen_names[path] = True pb.clear() print 'checked %d revisions, %d file texts' % (revcount, len(checked_texts)) if updated_revisions: print '%d revisions updated to current format' % len(updated_revisions) if missing_inventory_sha_cnt: print '%d revisions are missing inventory_sha1' % missing_inventory_sha_cnt print ' (use bzr check --update to fix them)' M 644 inline bzrlib/commands.py data 46719 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _find_plugins(): """Find all python files which are plugins, and load their commands to add to the list of "all commands" The environment variable BZRPATH is considered a delimited set of paths to look through. Each entry is searched for *.py files. If a directory is found, it is also searched, but they are not searched recursively. This allows you to revctl the plugins. Inside the plugin should be a series of cmd_* function, which inherit from the bzrlib.commands.Command class. """ bzrpath = os.environ.get('BZRPLUGINPATH', '') plugin_cmds = {} if not bzrpath: return plugin_cmds _platform_extensions = { 'win32':'.pyd', 'cygwin':'.dll', 'darwin':'.dylib', 'linux2':'.so' } if _platform_extensions.has_key(sys.platform): platform_extension = _platform_extensions[sys.platform] else: platform_extension = None for d in bzrpath.split(os.pathsep): plugin_names = {} # This should really be a set rather than a dict for f in os.listdir(d): if f.endswith('.py'): f = f[:-3] elif f.endswith('.pyc') or f.endswith('.pyo'): f = f[:-4] elif platform_extension and f.endswith(platform_extension): f = f[:-len(platform_extension)] if f.endswidth('module'): f = f[:-len('module')] else: continue if not plugin_names.has_key(f): plugin_names[f] = True plugin_names = plugin_names.keys() plugin_names.sort() try: sys.path.insert(0, d) for name in plugin_names: try: old_module = None try: if sys.modules.has_key(name): old_module = sys.modules[name] del sys.modules[name] plugin = __import__(name, locals()) for k in dir(plugin): if k.startswith('cmd_'): k_unsquished = _unsquish_command_name(k) if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = getattr(plugin, k) else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r in dir %r' % (name, d)) finally: if old_module: sys.modules[name] = old_module except ImportError, e: log_error('Unable to load plugin: %r from %r\n%s' % (name, d, e)) finally: sys.path.pop(0) return plugin_cmds def _get_cmd_dict(include_plugins=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v if include_plugins: d.update(_find_plugins()) return d def get_all_cmds(include_plugins=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(include_plugins=include_plugins).iteritems(): yield k,v def get_cmd_class(cmd,include_plugins=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(include_plugins=include_plugins) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import errno br_to = Branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if errno == errno.ENOENT: raise if location is None: location = stored_loc if location is None: raise BzrCommandError("No pull location known or specified.") from branch import find_branch, DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged. Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. """ takes_args = ['from_location', 'to_location?'] def run(self, from_location, to_location=None): import errno from bzrlib.merge import merge from branch import find_branch, DivergedBranches try: br_from = find_branch(from_location) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location) # FIXME: If there's a trailing slash, keep removing them # until we find the right bit try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) from_location = pull_loc(br_from) br_to.update_revisions(br_from) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False) br_to.controlfile("x-pull", "wb").write(from_location + "\n") def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision.""" takes_args = ['dest'] takes_options = ['revision'] def run(self, dest, revision=None): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] takes_options = ['update'] def run(self, dir='.', update=False): import bzrlib.check bzrlib.check.check(Branch(dir), update=update) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest if selftest(): return 0 else: return 1 class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'update': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] include_plugins=True try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd,include_plugins=include_plugins) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :675 committer Martin Pool 1118454929 +1000 data 31 - help formatting fix from ndim from :674 M 644 inline bzrlib/help.py data 4616 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA global_help = \ """Bazaar-NG -- a free distributed version-control tool http://bazaar-ng.org/ **WARNING: THIS IS AN UNSTABLE DEVELOPMENT VERSION** * Metadata format is not stable yet -- you may need to discard history in the future. * Many commands unimplemented or partially implemented. * Space-inefficient storage. * No merge operators yet. To make a branch, use 'bzr init' in an existing directory, then 'bzr add' to make files versioned. 'bzr add .' will recursively add all non-ignored files. 'bzr status' describes files that are unknown, ignored, or modified. 'bzr diff' shows the text changes to the tree or named files. 'bzr commit -m ' commits all changes in that branch. 'bzr move' and 'bzr rename' allow you to rename files or directories. 'bzr remove' makes a file unversioned but keeps the working copy; to delete that too simply delete the file. 'bzr log' shows a history of changes, and 'bzr info' gives summary statistical information. 'bzr check' validates all files are stored safely. Files can be ignored by giving a path or a glob in .bzrignore at the top of the tree. Use 'bzr ignored' to see what files are ignored and why, and 'bzr unknowns' to see files that are neither versioned or ignored. For more help on any command, type 'bzr help COMMAND', or 'bzr help commands' for a list. """ import sys def help(topic=None, outfile = None): if outfile == None: outfile = sys.stdout if topic == None: outfile.write(global_help) elif topic == 'commands': help_commands(outfile = outfile) else: help_on_command(topic, outfile = outfile) def command_usage(cmdname, cmdclass): """Return single-line grammar for command. Only describes arguments, not options. """ s = cmdname + ' ' for aname in cmdclass.takes_args: aname = aname.upper() if aname[-1] in ['$', '+']: aname = aname[:-1] + '...' elif aname[-1] == '?': aname = '[' + aname[:-1] + ']' elif aname[-1] == '*': aname = '[' + aname[:-1] + '...]' s += aname + ' ' assert s[-1] == ' ' s = s[:-1] return s def help_on_command(cmdname, outfile = None): cmdname = str(cmdname) if outfile == None: outfile = sys.stdout from inspect import getdoc import commands topic, cmdclass = commands.get_cmd_class(cmdname) doc = getdoc(cmdclass) if doc == None: raise NotImplementedError("sorry, no detailed help yet for %r" % cmdname) outfile.write('usage: ' + command_usage(topic, cmdclass) + '\n') if cmdclass.aliases: outfile.write('aliases: ' + ', '.join(cmdclass.aliases) + '\n') outfile.write(doc) if doc[-1] != '\n': outfile.write('\n') help_on_option(cmdclass.takes_options, outfile = None) def help_on_option(options, outfile = None): import commands if not options: return if outfile == None: outfile = sys.stdout outfile.write('\noptions:\n') for on in options: l = ' --' + on for shortname, longname in commands.SHORT_OPTIONS.items(): if longname == on: l += ', -' + shortname break outfile.write(l + '\n') def help_commands(outfile = None): """List all commands""" import inspect import commands if outfile == None: outfile = sys.stdout accu = [] for cmdname, cmdclass in commands.get_all_cmds(): accu.append((cmdname, cmdclass)) accu.sort() for cmdname, cmdclass in accu: if cmdclass.hidden: continue outfile.write(command_usage(cmdname, cmdclass) + '\n') help = inspect.getdoc(cmdclass) if help: outfile.write(" " + help.split('\n', 1)[0] + '\n') commit refs/heads/master mark :676 committer Martin Pool 1118455126 +1000 data 28 - lock branch while checking from :675 M 644 inline bzrlib/check.py data 8966 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def check(branch, update=False): """Run consistency checks on a branch. If update is True, for revisions missing certain information (right now this is inventory_sha1 and revision_sha1), update them to include the appropriate values. """ import sys from bzrlib.trace import mutter from bzrlib.errors import BzrCheckError from bzrlib.osutils import fingerprint_file from bzrlib.progress import ProgressBar if update: branch.lock_write() else: branch.lock_read() try: out = sys.stdout pb = ProgressBar(show_spinner=True) last_ptr = None checked_revs = {} missing_inventory_sha_cnt = 0 history = branch.revision_history() revno = 0 revcount = len(history) checked_texts = {} updated_revisions = [] # Set to True in the case that the previous revision entry # was updated, since this will affect future revision entries updated_previous_revision = False for rid in history: revno += 1 pb.update('checking revision', revno, revcount) mutter(' revision {%s}' % rid) rev = branch.get_revision(rid) if rev.revision_id != rid: raise BzrCheckError('wrong internal revision id in revision {%s}' % rid) if rev.precursor != last_ptr: raise BzrCheckError('mismatched precursor in revision {%s}' % rid) last_ptr = rid if rid in checked_revs: raise BzrCheckError('repeated revision {%s}' % rid) checked_revs[rid] = True ## TODO: Check all the required fields are present on the revision. updated = False if rev.inventory_sha1: #mutter(' checking inventory hash {%s}' % rev.inventory_sha1) inv_sha1 = branch.get_inventory_sha1(rev.inventory_id) if inv_sha1 != rev.inventory_sha1: raise BzrCheckError('Inventory sha1 hash doesn\'t match' ' value in revision {%s}' % rid) elif update: inv_sha1 = branch.get_inventory_sha1(rev.inventory_id) rev.inventory_sha1 = inv_sha1 updated = True else: missing_inventory_sha_cnt += 1 mutter("no inventory_sha1 on revision {%s}" % rid) if rev.precursor: if rev.precursor_sha1: precursor_sha1 = branch.get_revision_sha1(rev.precursor) if updated_previous_revision: # we don't expect the hashes to match, because # we had to modify the previous revision_history entry. rev.precursor_sha1 = precursor_sha1 updated = True else: #mutter(' checking precursor hash {%s}' % rev.precursor_sha1) if rev.precursor_sha1 != precursor_sha1: raise BzrCheckError('Precursor sha1 hash doesn\'t match' ' value in revision {%s}' % rid) elif update: precursor_sha1 = branch.get_revision_sha1(rev.precursor) rev.precursor_sha1 = precursor_sha1 updated = True if updated: updated_previous_revision = True # We had to update this revision entries hashes # Now we need to write out a new value # This is a little bit invasive, since we are *rewriting* a # revision entry. I'm not supremely happy about it, but # there should be *some* way of making old entries have # the full meta information. import tempfile, os, errno rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) tmpfd, tmp_path = tempfile.mkstemp(prefix=rid, suffix='.gz', dir=branch.controlfilename('revision-store')) os.close(tmpfd) def special_rename(p1, p2): if sys.platform == 'win32': try: os.remove(p2) except OSError, e: if e.errno != e.ENOENT: raise os.rename(p1, p2) try: # TODO: We may need to handle the case where the old revision # entry was not compressed (and thus did not end with .gz) # Remove the old revision entry out of the way rev_path = branch.controlfilename(['revision-store', rid+'.gz']) special_rename(rev_path, tmp_path) branch.revision_store.add(rev_tmp, rid) # Add the new one os.remove(tmp_path) # Remove the old name mutter(' Updated revision entry {%s}' % rid) except: # On any exception, restore the old entry special_rename(tmp_path, rev_path) raise rev_tmp.close() updated_revisions.append(rid) else: updated_previous_revision = False inv = branch.get_inventory(rev.inventory_id) seen_ids = {} seen_names = {} ## p('revision %d/%d file ids' % (revno, revcount)) for file_id in inv: if file_id in seen_ids: raise BzrCheckError('duplicated file_id {%s} ' 'in inventory for revision {%s}' % (file_id, rid)) seen_ids[file_id] = True i = 0 len_inv = len(inv) for file_id in inv: i += 1 if i & 31 == 0: pb.tick() ie = inv[file_id] if ie.parent_id != None: if ie.parent_id not in seen_ids: raise BzrCheckError('missing parent {%s} in inventory for revision {%s}' % (ie.parent_id, rid)) if ie.kind == 'file': if ie.text_id in checked_texts: fp = checked_texts[ie.text_id] else: if not ie.text_id in branch.text_store: raise BzrCheckError('text {%s} not in text_store' % ie.text_id) tf = branch.text_store[ie.text_id] fp = fingerprint_file(tf) checked_texts[ie.text_id] = fp if ie.text_size != fp['size']: raise BzrCheckError('text {%s} wrong size' % ie.text_id) if ie.text_sha1 != fp['sha1']: raise BzrCheckError('text {%s} wrong sha1' % ie.text_id) elif ie.kind == 'directory': if ie.text_sha1 != None or ie.text_size != None or ie.text_id != None: raise BzrCheckError('directory {%s} has text in revision {%s}' % (file_id, rid)) pb.tick() for path, ie in inv.iter_entries(): if path in seen_names: raise BzrCheckError('duplicated path %r ' 'in inventory for revision {%s}' % (path, revid)) seen_names[path] = True finally: branch.unlock() pb.clear() print 'checked %d revisions, %d file texts' % (revcount, len(checked_texts)) if updated_revisions: print '%d revisions updated to current format' % len(updated_revisions) if missing_inventory_sha_cnt: print '%d revisions are missing inventory_sha1' % missing_inventory_sha_cnt print ' (use bzr check --update to fix them)' commit refs/heads/master mark :677 committer Martin Pool 1118664892 +1000 data 30 - draft 'meta' command by john from :676 M 644 inline patches/meta-data-in-inventory.patch data 10735 *** modified file 'bzrlib/commands.py' --- bzrlib/commands.py +++ bzrlib/commands.py @@ -1175,7 +1175,68 @@ b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) - +class cmd_meta(Command): + """Get or set the meta information properties about a file. + + bzr meta FILENAME # Display all meta information + bzr meta FILENAME prop # Display the value of meta-info named prop, return error if not found + bzr meta FILENAME --set prop value # Set the value of prop to value + echo "the value" | bzr meta FILENAME --set prop # Set the value to whatever is read from stdin + bzr meta FILENAME --unset prop # Remove the property + + bzr meta FILENAME --revision=10 # Display the meta information for a given revision + (Not supported yet) + """ + hidden = True + takes_args = ['filename', 'property?', 'value?'] + takes_options = ['revision', 'set', 'unset'] + + def run(self, filename, property=None, value=None, revision=None, set=False, unset=False): + if isinstance(revision, list) and len(revision) > 1: + raise BzrCommandError('bzr meta takes at most 1 revision.') + if revision is not None and (set or unset): + raise BzrCommandError('Cannot set/unset meta information in an old version.') + if set and unset: + raise BzrCommandError('Cannot set and unset at the same time') + if not set and value: + raise BzrCommandError('You must supply --set if you want to set the value of a property.') + + b = Branch(filename) + inv = b.inventory + file_id = inv.path2id(b.relpath(filename)) + inv_entry = inv[file_id] + if not property: + meta = inv_entry.meta + if meta: # Not having meta is the same as having an empty meta + keys = meta.properties.keys() + keys.sort() + # The output really needs to be parseable + for key in keys: + print '%s: %r' % (key, meta.properties[key]) + else: + meta = inv_entry.meta + if set: + if value is None: + value = sys.stdin.read() + if not meta: + from bzrlib.inventory import Meta + inv_entry.meta = Meta({property:value}) + else: + inv_entry.meta.properties[property] = value + b.inventory = inv # This should cause it to be saved + elif unset: + if not meta or not meta.properties.has_key(property): + return 3 # Cannot unset a property that doesn't exist + # I wonder if this should be a different return code + del inv_entry.meta.properties[property] + b.inventory = inv + else: + if not meta or not meta.properties.has_key(property): + return 3 # Missing property + # Probably this should not be print, but + # sys.stdout.write() so that you get back exactly + # what was given. But I'm leaving it this way for now + print meta.properties[property] # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks @@ -1196,6 +1257,8 @@ 'verbose': None, 'version': None, 'email': None, + 'set': None, + 'unset': None, } SHORT_OPTIONS = { *** modified file 'bzrlib/diff.py' --- bzrlib/diff.py +++ bzrlib/diff.py @@ -226,8 +226,10 @@ new_tree.get_file(file_id).readlines(), to_file) - for old_path, new_path, file_id, kind, text_modified in delta.renamed: + for old_path, new_path, file_id, kind, text_modified, meta_modified in delta.renamed: print '*** renamed %s %r => %r' % (kind, old_path, new_path) + if meta_modified: + print '## Meta-info modified' if text_modified: diff_file(old_label + old_path, old_tree.get_file(file_id).readlines(), @@ -235,9 +237,11 @@ new_tree.get_file(file_id).readlines(), to_file) - for path, file_id, kind in delta.modified: + for path, file_id, kind, text_modified, meta_modified in delta.modified: print '*** modified %s %r' % (kind, path) - if kind == 'file': + if meta_modified: + print '## Meta-info modified' + if kind == 'file' and text_modified: diff_file(old_label + path, old_tree.get_file(file_id).readlines(), new_label + path, @@ -316,7 +320,7 @@ if self.renamed: print >>to_file, 'renamed:' - for oldpath, newpath, fid, kind, text_modified in self.renamed: + for oldpath, newpath, fid, kind, text_modified, meta_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: @@ -324,7 +328,16 @@ if self.modified: print >>to_file, 'modified:' - show_list(self.modified) + for path, fid, kind, text_modified, meta_modified in self.modified: + if kind == 'directory': + path += '/' + elif kind == 'symlink': + path += '@' + + if show_ids: + print >>to_file, ' %-30s %s' % (path, fid) + else: + print >>to_file, ' ', path if show_unchanged and self.unchanged: print >>to_file, 'unchanged:' @@ -388,6 +401,14 @@ ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False + old_meta = old_inv[file_id].meta + new_meta = new_inv[file_id].meta + + if old_meta != new_meta: + meta_modified = True + else: + meta_modified = False + # TODO: Can possibly avoid calculating path strings if the # two files are unchanged and their names and parents are # the same and the parents are unchanged all the way up. @@ -395,9 +416,10 @@ if old_path != new_path: delta.renamed.append((old_path, new_path, file_id, kind, - text_modified)) - elif text_modified: - delta.modified.append((new_path, file_id, kind)) + text_modified, meta_modified)) + elif text_modified or meta_modified: + delta.modified.append((new_path, file_id, kind, + text_modified, meta_modified)) elif want_unchanged: delta.unchanged.append((new_path, file_id, kind)) else: *** modified file 'bzrlib/inventory.py' --- bzrlib/inventory.py +++ bzrlib/inventory.py @@ -33,6 +33,67 @@ import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter + +class Meta(XMLMixin): + """Meta information about a single inventory entry. + + This is basically just a set of key->value pairs. + + In general, bzr does not handle this information, it only provides + a location for plugins and other data sources to store this data. + + """ + def __init__(self, properties): + self.properties = properties + + def __eq__(self, other): + if other is None: + return False + if not isinstance(other, Meta): + return NotImplemented + + return (self.properties == other.properties) + + def __ne__(self, other): + return not (self == other) + + def __hash__(self): + raise ValueError('not hashable') + + def from_element(cls, elt): + assert elt.tag == 'meta' + + properties = {} + for child in elt: + if child.tag == 'property': + if child.text is None: + properties[child.get('name')] = '' + else: + properties[child.get('name')] = child.text + self = cls(properties) + return self + + from_element = classmethod(from_element) + + def to_element(self): + e = Element('meta') + keys = self.properties.keys() + keys.sort() + + # The blah.text is just to make things look pretty in the files + # We may want to remove it + if len(keys) > 0: + e.text = '\n' + + for key in keys: + prop = Element('property') + prop.set('name', key) + prop.text=self.properties[key] + prop.tail='\n' + e.append(prop) + + return e + class InventoryEntry(XMLMixin): """Description of a versioned file. @@ -100,6 +161,7 @@ text_sha1 = None text_size = None + meta = None def __init__(self, file_id, name, kind, parent_id, text_id=None): """Create an InventoryEntry @@ -124,6 +186,7 @@ self.kind = kind self.text_id = text_id self.parent_id = parent_id + self.meta = None if kind == 'directory': self.children = {} elif kind == 'file': @@ -146,6 +209,10 @@ other.text_size = self.text_size # note that children are *not* copied; they're pulled across when # others are added + if self.meta: + other.meta = Meta(self.meta.properties) + else: + other.meta = None return other @@ -182,6 +249,9 @@ e.set('parent_id', self.parent_id) e.tail = '\n' + + if self.meta: + e.append(self.meta.to_element()) return e @@ -205,6 +275,11 @@ v = elt.get('text_size') self.text_size = v and int(v) + self.meta = None + for child in elt: + if child.tag == 'meta': + self.meta = Meta.from_element(child) + return self @@ -220,7 +295,8 @@ and (self.text_size == other.text_size) \ and (self.text_id == other.text_id) \ and (self.parent_id == other.parent_id) \ - and (self.kind == other.kind) + and (self.kind == other.kind) \ + and (self.meta == other.meta) def __ne__(self, other): commit refs/heads/master mark :678 committer Martin Pool 1118735565 +1000 data 38 - export to tarballs patch from lalo from :677 M 644 inline bzrlib/commands.py data 46962 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _find_plugins(): """Find all python files which are plugins, and load their commands to add to the list of "all commands" The environment variable BZRPATH is considered a delimited set of paths to look through. Each entry is searched for *.py files. If a directory is found, it is also searched, but they are not searched recursively. This allows you to revctl the plugins. Inside the plugin should be a series of cmd_* function, which inherit from the bzrlib.commands.Command class. """ bzrpath = os.environ.get('BZRPLUGINPATH', '') plugin_cmds = {} if not bzrpath: return plugin_cmds _platform_extensions = { 'win32':'.pyd', 'cygwin':'.dll', 'darwin':'.dylib', 'linux2':'.so' } if _platform_extensions.has_key(sys.platform): platform_extension = _platform_extensions[sys.platform] else: platform_extension = None for d in bzrpath.split(os.pathsep): plugin_names = {} # This should really be a set rather than a dict for f in os.listdir(d): if f.endswith('.py'): f = f[:-3] elif f.endswith('.pyc') or f.endswith('.pyo'): f = f[:-4] elif platform_extension and f.endswith(platform_extension): f = f[:-len(platform_extension)] if f.endswidth('module'): f = f[:-len('module')] else: continue if not plugin_names.has_key(f): plugin_names[f] = True plugin_names = plugin_names.keys() plugin_names.sort() try: sys.path.insert(0, d) for name in plugin_names: try: old_module = None try: if sys.modules.has_key(name): old_module = sys.modules[name] del sys.modules[name] plugin = __import__(name, locals()) for k in dir(plugin): if k.startswith('cmd_'): k_unsquished = _unsquish_command_name(k) if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = getattr(plugin, k) else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r in dir %r' % (name, d)) finally: if old_module: sys.modules[name] = old_module except ImportError, e: log_error('Unable to load plugin: %r from %r\n%s' % (name, d, e)) finally: sys.path.pop(0) return plugin_cmds def _get_cmd_dict(include_plugins=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v if include_plugins: d.update(_find_plugins()) return d def get_all_cmds(include_plugins=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(include_plugins=include_plugins).iteritems(): yield k,v def get_cmd_class(cmd,include_plugins=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(include_plugins=include_plugins) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import errno br_to = Branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if errno == errno.ENOENT: raise if location is None: location = stored_loc if location is None: raise BzrCommandError("No pull location known or specified.") from branch import find_branch, DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged. Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. """ takes_args = ['from_location', 'to_location?'] def run(self, from_location, to_location=None): import errno from bzrlib.merge import merge from branch import find_branch, DivergedBranches try: br_from = find_branch(from_location) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location) # FIXME: If there's a trailing slash, keep removing them # until we find the right bit try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) from_location = pull_loc(br_from) br_to.update_revisions(br_from) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False) br_to.controlfile("x-pull", "wb").write(from_location + "\n") def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, exports to a directory (equivalent to --format=dir).""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format'] def run(self, dest, revision=None, format='dir'): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest, format) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] takes_options = ['update'] def run(self, dir='.', update=False): import bzrlib.check bzrlib.check.check(Branch(dir), update=update) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest if selftest(): return 0 else: return 1 class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'update': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt not in SHORT_OPTIONS: bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] include_plugins=True try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd,include_plugins=include_plugins) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/tree.py data 10292 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tree classes, representing directory at point in time. """ from sets import Set import os.path, os, fnmatch, time from osutils import pumpfile, filesize, quotefn, sha_file, \ joinpath, splitpath, appendpath, isdir, isfile, file_kind, fingerprint_file import errno from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE from inventory import Inventory from trace import mutter, note from errors import bailout import branch import bzrlib exporters = {} class Tree(object): """Abstract file tree. There are several subclasses: * `WorkingTree` exists as files on disk editable by the user. * `RevisionTree` is a tree as recorded at some point in the past. * `EmptyTree` Trees contain an `Inventory` object, and also know how to retrieve file texts mentioned in the inventory, either from a working directory or from a store. It is possible for trees to contain files that are not described in their inventory or vice versa; for this use `filenames()`. Trees can be compared, etc, regardless of whether they are working trees or versioned trees. """ def has_filename(self, filename): """True if the tree has given filename.""" raise NotImplementedError() def has_id(self, file_id): return self.inventory.has_id(file_id) __contains__ = has_id def __iter__(self): return iter(self.inventory) def id2path(self, file_id): return self.inventory.id2path(file_id) def _get_inventory(self): return self._inventory inventory = property(_get_inventory, doc="Inventory of this Tree") def _check_retrieved(self, ie, f): fp = fingerprint_file(f) f.seek(0) if ie.text_size != None: if ie.text_size != fp['size']: bailout("mismatched size for file %r in %r" % (ie.file_id, self._store), ["inventory expects %d bytes" % ie.text_size, "file is actually %d bytes" % fp['size'], "store is probably damaged/corrupt"]) if ie.text_sha1 != fp['sha1']: bailout("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store), ["inventory expects %s" % ie.text_sha1, "file is actually %s" % fp['sha1'], "store is probably damaged/corrupt"]) def print_file(self, fileid): """Print file with id `fileid` to stdout.""" import sys pumpfile(self.get_file(fileid), sys.stdout) def export(self, dest, format='dir'): """Export this tree.""" try: exporter = exporters[format] except KeyError: raise BzrCommandError("export format %r not supported" % format) exporter(self, dest) class RevisionTree(Tree): """Tree viewing a previous revision. File text can be retrieved from the text store. TODO: Some kind of `__repr__` method, but a good one probably means knowing the branch and revision number, or at least passing a description to the constructor. """ def __init__(self, store, inv): self._store = store self._inventory = inv def get_file(self, file_id): ie = self._inventory[file_id] f = self._store[ie.text_id] mutter(" get fileid{%s} from %r" % (file_id, self)) self._check_retrieved(ie, f) return f def get_file_size(self, file_id): return self._inventory[file_id].text_size def get_file_sha1(self, file_id): ie = self._inventory[file_id] return ie.text_sha1 def has_filename(self, filename): return bool(self.inventory.path2id(filename)) def list_files(self): # The only files returned by this are those from the version for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id class EmptyTree(Tree): def __init__(self): self._inventory = Inventory() def has_filename(self, filename): return False def list_files(self): if False: # just to make it a generator yield None ###################################################################### # diff # TODO: Merge these two functions into a single one that can operate # on either a whole tree or a set of files. # TODO: Return the diff in order by filename, not by category or in # random order. Can probably be done by lock-stepping through the # filenames from both trees. def file_status(filename, old_tree, new_tree): """Return single-letter status, old and new names for a file. The complexity here is in deciding how to represent renames; many complex cases are possible. """ old_inv = old_tree.inventory new_inv = new_tree.inventory new_id = new_inv.path2id(filename) old_id = old_inv.path2id(filename) if not new_id and not old_id: # easy: doesn't exist in either; not versioned at all if new_tree.is_ignored(filename): return 'I', None, None else: return '?', None, None elif new_id: # There is now a file of this name, great. pass else: # There is no longer a file of this name, but we can describe # what happened to the file that used to have # this name. There are two possibilities: either it was # deleted entirely, or renamed. assert old_id if new_inv.has_id(old_id): return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id) else: return 'D', old_inv.id2path(old_id), None # if the file_id is new in this revision, it is added if new_id and not old_inv.has_id(new_id): return 'A' # if there used to be a file of this name, but that ID has now # disappeared, it is deleted if old_id and not new_inv.has_id(old_id): return 'D' return 'wtf?' def find_renames(old_inv, new_inv): for file_id in old_inv: if file_id not in new_inv: continue old_name = old_inv.id2path(file_id) new_name = new_inv.id2path(file_id) if old_name != new_name: yield (old_name, new_name) ###################################################################### # export def dir_exporter(tree, dest): """Export this tree to a new directory. `dest` should not exist, and will be created holding the contents of this tree. TODO: To handle subdirectories we need to create the directories first. :note: If the export fails, the destination directory will be left in a half-assed state. """ os.mkdir(dest) mutter('export version %r' % tree) inv = tree.inventory for dp, ie in inv.iter_entries(): kind = ie.kind fullpath = appendpath(dest, dp) if kind == 'directory': os.mkdir(fullpath) elif kind == 'file': pumpfile(tree.get_file(ie.file_id), file(fullpath, 'wb')) else: bailout("don't know how to export {%s} of kind %r" % (ie.file_id, kind)) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) exporters['dir'] = dir_exporter try: import tarfile except ImportError: pass else: def tar_exporter(tree, dest, compression=None): """Export this tree to a new tar file. `dest` will be created holding the contents of this tree; if it already exists, it will be clobbered, like with "tar -c". """ now = time.time() compression = str(compression or '') try: ball = tarfile.open(dest, 'w:' + compression) except tarfile.CompressionError, e: bailout(str(e)) mutter('export version %r' % tree) inv = tree.inventory for dp, ie in inv.iter_entries(): mutter(" export {%s} kind %s to %s" % (ie.file_id, ie.kind, dest)) item = tarfile.TarInfo(dp) # TODO: would be cool to actually set it to the timestamp of the # revision it was last changed item.mtime = now if ie.kind == 'directory': item.type = tarfile.DIRTYPE fileobj = None item.name += '/' item.size = 0 item.mode = 0755 elif ie.kind == 'file': item.type = tarfile.REGTYPE fileobj = tree.get_file(ie.file_id) item.size = _find_file_size(fileobj) item.mode = 0644 else: bailout("don't know how to export {%s} of kind %r" % (ie.file_id, ie.kind)) ball.addfile(item, fileobj) ball.close() exporters['tar'] = tar_exporter def tgz_exporter(tree, dest): tar_exporter(tree, dest, compression='gz') exporters['tgz'] = tgz_exporter def tbz_exporter(tree, dest): tar_exporter(tree, dest, compression='bz2') exporters['tbz2'] = tbz_exporter def _find_file_size(fileobj): offset = fileobj.tell() try: fileobj.seek(0, 2) size = fileobj.tell() except TypeError: # gzip doesn't accept second argument to seek() fileobj.seek(0) size = 0 while True: nread = len(fileobj.read()) if nread == 0: break size += nread fileobj.seek(offset) return size commit refs/heads/master mark :679 committer Martin Pool 1118736979 +1000 data 51 - put trailing newline on newly-created .bzr/README from :678 M 644 inline bzrlib/branch.py data 35632 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_file, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def _relpath(base, path): """Return path relative to base, or raise exception. The path may be either an absolute path or a path relative to the current working directory. Lifted out of Branch.relpath for ease of testing. os.path.commonprefix (python2.4) has a bad bug that it works just on string prefixes, assuming that '/u' is a prefix of '/u2'. This avoids that problem.""" rp = os.path.abspath(path) s = [] head = rp while len(head) >= len(base): if head == base: break head, tail = os.path.split(head) if tail: s.insert(0, tail) else: from errors import NotBranchError raise NotBranchError("path %r is not within branch %r" % (rp, base)) return os.sep.join(s) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head class DivergedBranches(Exception): def __init__(self, branch1, branch2): self.branch1 = branch1 self.branch2 = branch2 Exception.__init__(self, "These branches have diverged.") ###################################################################### # branch objects class Branch(object): """Branch holding a history of revisions. base Base directory of the branch. _lock_mode None, or 'r' or 'w' _lock_count If _lock_mode is true, a positive count of the number of times the lock has been taken. _lock Lock object from bzrlib.lock. """ base = None _lock_mode = None _lock_count = None _lock = None def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): from errors import NotBranchError raise NotBranchError("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def __del__(self): if self._lock_mode or self._lock: from warnings import warn warn("branch %r was not explicitly unlocked" % self) self._lock.unlock() def lock_write(self): if self._lock_mode: if self._lock_mode != 'w': from errors import LockError raise LockError("can't upgrade to a write lock from %r" % self._lock_mode) self._lock_count += 1 else: from bzrlib.lock import WriteLock self._lock = WriteLock(self.controlfilename('branch-lock')) self._lock_mode = 'w' self._lock_count = 1 def lock_read(self): if self._lock_mode: assert self._lock_mode in ('r', 'w'), \ "invalid lock mode %r" % self._lock_mode self._lock_count += 1 else: from bzrlib.lock import ReadLock self._lock = ReadLock(self.controlfilename('branch-lock')) self._lock_mode = 'r' self._lock_count = 1 def unlock(self): if not self._lock_mode: from errors import LockError raise LockError('branch %r is not locked' % (self)) if self._lock_count > 1: self._lock_count -= 1 else: self._lock.unlock() self._lock = None self._lock_mode = self._lock_count = None def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" return _relpath(self.base, path) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.\n") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: raise BzrError('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. self.lock_read() try: inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv finally: self.unlock() def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. files List of paths to add, relative to the base of the tree. ids If set, use these instead of automatically generated ids. Must be the same length as the list of files, but may contain None for ids that are to be autogenerated. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) self.lock_write() try: inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): raise BzrError("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: raise BzrError("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) finally: self.unlock() def print_file(self, file, revno): """Print `file` to stdout.""" self.lock_read() try: tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: raise BzrError("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) finally: self.unlock() def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] self.lock_write() try: tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: raise BzrError("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) finally: self.unlock() # FIXME: this doesn't need to be a branch method def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" if not revision_id or not isinstance(revision_id, basestring): raise ValueError('invalid revision-id: %r' % revision_id) r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_revision_sha1(self, revision_id): """Hash the stored value of a revision, and return it.""" # In the future, revision entries will be signed. At that # point, it is probably best *not* to include the signature # in the revision hash. Because that lets you re-sign # the revision, (add signatures/remove signatures) and still # have all hash pointers stay consistent. # But for now, just hash the contents. return sha_file(self.revision_store[revision_id]) def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_inventory_sha1(self, inventory_id): """Return the sha1 hash of the inventory entry """ return sha_file(self.inventory_store[inventory_id]) def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self.lock_read() try: return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] finally: self.unlock() def common_ancestor(self, other, self_revno=None, other_revno=None): """ >>> import commit >>> sb = ScratchBranch(files=['foo', 'foo~']) >>> sb.common_ancestor(sb) == (None, None) True >>> commit.commit(sb, "Committing first revision", verbose=False) >>> sb.common_ancestor(sb)[0] 1 >>> clone = sb.clone() >>> commit.commit(sb, "Committing second revision", verbose=False) >>> sb.common_ancestor(sb)[0] 2 >>> sb.common_ancestor(clone)[0] 1 >>> commit.commit(clone, "Committing divergent second revision", ... verbose=False) >>> sb.common_ancestor(clone)[0] 1 >>> sb.common_ancestor(clone) == clone.common_ancestor(sb) True >>> sb.common_ancestor(sb) != clone.common_ancestor(clone) True >>> clone2 = sb.clone() >>> sb.common_ancestor(clone2)[0] 2 >>> sb.common_ancestor(clone2, self_revno=1)[0] 1 >>> sb.common_ancestor(clone2, other_revno=1)[0] 1 """ my_history = self.revision_history() other_history = other.revision_history() if self_revno is None: self_revno = len(my_history) if other_revno is None: other_revno = len(other_history) indices = range(min((self_revno, other_revno))) indices.reverse() for r in indices: if my_history[r] == other_history[r]: return r+1, my_history[r] return None, None def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def missing_revisions(self, other): """ If self and other have not diverged, return a list of the revisions present in other, but missing from self. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch() >>> br2 = ScratchBranch() >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [u'REVISION-ID-1'] >>> br2.missing_revisions(br1) [] >>> commit(br1, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-2A") >>> br1.missing_revisions(br2) [u'REVISION-ID-2A'] >>> commit(br1, "lala!", rev_id="REVISION-ID-2B") >>> br1.missing_revisions(br2) Traceback (most recent call last): DivergedBranches: These branches have diverged. """ self_history = self.revision_history() self_len = len(self_history) other_history = other.revision_history() other_len = len(other_history) common_index = min(self_len, other_len) -1 if common_index >= 0 and \ self_history[common_index] != other_history[common_index]: raise DivergedBranches(self, other) if self_len < other_len: return other_history[self_len:] return [] def update_revisions(self, other): """Pull in all new revisions from other branch. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch(files=['foo', 'bar']) >>> br1.add('foo') >>> br1.add('bar') >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False) >>> br2 = ScratchBranch() >>> br2.update_revisions(br1) Added 2 texts. Added 1 inventories. Added 1 revisions. >>> br2.revision_history() [u'REVISION-ID-1'] >>> br2.update_revisions(br1) Added 0 texts. Added 0 inventories. Added 0 revisions. >>> br1.text_store.total_size() == br2.text_store.total_size() True """ from bzrlib.progress import ProgressBar pb = ProgressBar() pb.update('comparing histories') revision_ids = self.missing_revisions(other) revisions = [] needed_texts = sets.Set() i = 0 for rev_id in revision_ids: i += 1 pb.update('fetching revision', i, len(revision_ids)) rev = other.get_revision(rev_id) revisions.append(rev) inv = other.get_inventory(str(rev.inventory_id)) for key, entry in inv.iter_entries(): if entry.text_id is None: continue if entry.text_id not in self.text_store: needed_texts.add(entry.text_id) pb.clear() count = self.text_store.copy_multi(other.text_store, needed_texts) print "Added %d texts." % count inventory_ids = [ f.inventory_id for f in revisions ] count = self.inventory_store.copy_multi(other.inventory_store, inventory_ids) print "Added %d inventories." % count revision_ids = [ f.revision_id for f in revisions] count = self.revision_store.copy_multi(other.revision_store, revision_ids) for revision_id in revision_ids: self.append_revision(revision_id) print "Added %d revisions." % count def commit(self, *args, **kw): """Deprecated""" from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self.lock_write() try: tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): raise BzrError("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): raise BzrError("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: raise BzrError("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): raise BzrError("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': raise BzrError("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self.lock_write() try: ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): raise BzrError("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): raise BzrError("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': raise BzrError("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): raise BzrError("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): raise BzrError("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: raise BzrError("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): raise BzrError("destination %r already exists" % dest_path) if f_id in to_idpath: raise BzrError("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[], base=None): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ init = False if base is None: base = tempfile.mkdtemp() init = True Branch.__init__(self, base, init=init) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def clone(self): """ >>> orig = ScratchBranch(files=["file1", "file2"]) >>> clone = orig.clone() >>> os.path.samefile(orig.base, clone.base) False >>> os.path.isfile(os.path.join(clone.base, "file1")) True """ base = tempfile.mkdtemp() os.rmdir(base) shutil.copytree(self.base, base, symlinks=True) return ScratchBranch(base=base) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: if self.base: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :680 committer Martin Pool 1118790246 +1000 data 57 - try again to fix permissions when uploading to escudero from :679 M 644 inline contrib/upload-bzr.dev data 829 #! /bin/sh -ex # example of how to upload a bzr tree using rsync # --include-from is used to make sure that only versioned files and # control files are copied. We use includes/excludes rather than # --files-from so that we can delete any files from the destination # that are no longer present on the source. cd ~/work/bzr # note: don't use -a because that can mess up the permissions chmod a+rX `bzr inventory` bzr inventory | rsync -rltv \ . \ escudero.ubuntu.com:/srv/www.bazaar-ng.org/rsync/bzr/bzr.dev/ \ --include-from - \ --include .bzr \ --include '.bzr/**' \ --exclude-from .rsyncexclude \ --exclude-from .bzrignore \ --exclude \* \ --exclude '.*' \ --delete-excluded --delete \ commit refs/heads/master mark :681 committer Martin Pool 1118790266 +1000 data 42 - assign missing fields in Progress object from :680 M 644 inline bzrlib/progress.py data 7634 # Copyright (C) 2005 Aaron Bentley # Copyright (C) 2005 Canonical # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ Simple text-mode progress indicator. Everyone loves ascii art! To display an indicator, create a ProgressBar object. Call it, passing Progress objects indicating the current state. When done, call clear(). Progress is suppressed when output is not sent to a terminal, so as not to clutter log files. """ # TODO: remove functions in favour of keeping everything in one class # TODO: should be a global option e.g. --silent that disables progress # indicators, preferably without needing to adjust all code that # potentially calls them. # TODO: Perhaps don't write updates faster than a certain rate, say # 5/second. import sys import time def _width(): """Return estimated terminal width. TODO: Do something smart on Windows? TODO: Is there anything that gets a better update when the window is resized while the program is running? """ import os try: return int(os.environ['COLUMNS']) except (IndexError, KeyError, ValueError): return 80 def _supports_progress(f): return hasattr(f, 'isatty') and f.isatty() class ProgressBar(object): """Progress bar display object. Several options are available to control the display. These can be passed as parameters to the constructor or assigned at any time: show_pct Show percentage complete. show_spinner Show rotating baton. This ticks over on every update even if the values don't change. show_eta Show predicted time-to-completion. show_bar Show bar graph. show_count Show numerical counts. The output file should be in line-buffered or unbuffered mode. """ SPIN_CHARS = r'/-\|' MIN_PAUSE = 0.1 # seconds start_time = None last_update = None def __init__(self, to_file=sys.stderr, show_pct=False, show_spinner=False, show_eta=True, show_bar=True, show_count=True): object.__init__(self) self.to_file = to_file self.suppressed = not _supports_progress(self.to_file) self.spin_pos = 0 self.last_msg = None self.last_cnt = None self.last_total = None self.show_pct = show_pct self.show_spinner = show_spinner self.show_eta = show_eta self.show_bar = show_bar self.show_count = show_count def tick(self): self.update(self.last_msg, self.last_cnt, self.last_total) def update(self, msg, current_cnt=None, total_cnt=None): """Update and redraw progress bar.""" if self.suppressed: return # save these for the tick() function self.last_msg = msg self.last_cnt = current_cnt self.last_total = total_cnt now = time.time() if self.start_time is None: self.start_time = now else: interval = now - self.last_update if interval > 0 and interval < self.MIN_PAUSE: return self.last_update = now width = _width() if total_cnt: assert current_cnt <= total_cnt if current_cnt: assert current_cnt >= 0 if self.show_eta and self.start_time and total_cnt: eta = get_eta(self.start_time, current_cnt, total_cnt) eta_str = " " + str_tdelta(eta) else: eta_str = "" if self.show_spinner: spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' ' else: spin_str = '' # always update this; it's also used for the bar self.spin_pos += 1 if self.show_pct and total_cnt and current_cnt: pct = 100.0 * current_cnt / total_cnt pct_str = ' (%5.1f%%)' % pct else: pct_str = '' if not self.show_count: count_str = '' elif current_cnt is None: count_str = '' elif total_cnt is None: count_str = ' %i' % (current_cnt) else: # make both fields the same size t = '%i' % (total_cnt) c = '%*i' % (len(t), current_cnt) count_str = ' ' + c + '/' + t if self.show_bar: # progress bar, if present, soaks up all remaining space cols = width - 1 - len(msg) - len(spin_str) - len(pct_str) \ - len(eta_str) - len(count_str) - 3 if total_cnt: # number of markers highlighted in bar markers = int(round(float(cols) * current_cnt / total_cnt)) bar_str = '[' + ('=' * markers).ljust(cols) + '] ' elif False: # don't know total, so can't show completion. # so just show an expanded spinning thingy m = self.spin_pos % cols ms = (' ' * m + '*').ljust(cols) bar_str = '[' + ms + '] ' else: bar_str = '' else: bar_str = '' m = spin_str + bar_str + msg + count_str + pct_str + eta_str assert len(m) < width self.to_file.write('\r' + m.ljust(width - 1)) #self.to_file.flush() def clear(self): if self.suppressed: return self.to_file.write('\r%s\r' % (' ' * (_width() - 1))) #self.to_file.flush() def str_tdelta(delt): if delt is None: return "-:--:--" delt = int(round(delt)) return '%d:%02d:%02d' % (delt/3600, (delt/60) % 60, delt % 60) def get_eta(start_time, current, total, enough_samples=3): if start_time is None: return None if not total: return None if current < enough_samples: return None if current > total: return None # wtf? elapsed = time.time() - start_time if elapsed < 2.0: # not enough time to estimate return None total_duration = float(elapsed) * float(total) / float(current) assert total_duration >= elapsed return total_duration - elapsed def run_tests(): import doctest result = doctest.testmod() if result[1] > 0: if result[0] == 0: print "All tests passed" else: print "No tests to run" def demo(): from time import sleep pb = ProgressBar(show_pct=True, show_bar=True, show_spinner=False) for i in range(100): pb.update('Elephanten', i, 99) sleep(0.1) sleep(2) pb.clear() sleep(1) print 'done!' if __name__ == "__main__": demo() commit refs/heads/master mark :682 committer Martin Pool 1118790291 +1000 data 46 - don't doctest modules which don't have tests from :681 M 644 inline bzrlib/selftest.py data 1260 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def selftest(): import unittest from unittest import TestLoader import bzrlib from doctest import DocTestSuite tr = unittest.TextTestRunner(verbosity=2) suite = unittest.TestSuite() import bzrlib.whitebox suite.addTest(TestLoader().loadTestsFromModule(bzrlib.whitebox)) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands: suite.addTest(DocTestSuite(m)) result = tr.run(suite) return result.wasSuccessful() commit refs/heads/master mark :683 committer Martin Pool 1118806211 +1000 data 48 - short option stacking patch from John A Meinel from :682 M 644 inline bzrlib/commands.py data 48683 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _find_plugins(): """Find all python files which are plugins, and load their commands to add to the list of "all commands" The environment variable BZRPATH is considered a delimited set of paths to look through. Each entry is searched for *.py files. If a directory is found, it is also searched, but they are not searched recursively. This allows you to revctl the plugins. Inside the plugin should be a series of cmd_* function, which inherit from the bzrlib.commands.Command class. """ bzrpath = os.environ.get('BZRPLUGINPATH', '') plugin_cmds = {} if not bzrpath: return plugin_cmds _platform_extensions = { 'win32':'.pyd', 'cygwin':'.dll', 'darwin':'.dylib', 'linux2':'.so' } if _platform_extensions.has_key(sys.platform): platform_extension = _platform_extensions[sys.platform] else: platform_extension = None for d in bzrpath.split(os.pathsep): plugin_names = {} # This should really be a set rather than a dict for f in os.listdir(d): if f.endswith('.py'): f = f[:-3] elif f.endswith('.pyc') or f.endswith('.pyo'): f = f[:-4] elif platform_extension and f.endswith(platform_extension): f = f[:-len(platform_extension)] if f.endswidth('module'): f = f[:-len('module')] else: continue if not plugin_names.has_key(f): plugin_names[f] = True plugin_names = plugin_names.keys() plugin_names.sort() try: sys.path.insert(0, d) for name in plugin_names: try: old_module = None try: if sys.modules.has_key(name): old_module = sys.modules[name] del sys.modules[name] plugin = __import__(name, locals()) for k in dir(plugin): if k.startswith('cmd_'): k_unsquished = _unsquish_command_name(k) if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = getattr(plugin, k) else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r in dir %r' % (name, d)) finally: if old_module: sys.modules[name] = old_module except ImportError, e: log_error('Unable to load plugin: %r from %r\n%s' % (name, d, e)) finally: sys.path.pop(0) return plugin_cmds def _get_cmd_dict(include_plugins=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v if include_plugins: d.update(_find_plugins()) return d def get_all_cmds(include_plugins=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(include_plugins=include_plugins).iteritems(): yield k,v def get_cmd_class(cmd,include_plugins=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(include_plugins=include_plugins) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import errno br_to = Branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if errno == errno.ENOENT: raise if location is None: location = stored_loc if location is None: raise BzrCommandError("No pull location known or specified.") from branch import find_branch, DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged. Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. """ takes_args = ['from_location', 'to_location?'] def run(self, from_location, to_location=None): import errno from bzrlib.merge import merge from branch import find_branch, DivergedBranches try: br_from = find_branch(from_location) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location) # FIXME: If there's a trailing slash, keep removing them # until we find the right bit try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) from_location = pull_loc(br_from) br_to.update_revisions(br_from) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False) br_to.controlfile("x-pull", "wb").write(from_location + "\n") def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, exports to a directory (equivalent to --format=dir).""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format'] def run(self, dest, revision=None, format='dir'): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest, format) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] takes_options = ['update'] def run(self, dir='.', update=False): import bzrlib.check bzrlib.check.check(Branch(dir), update=update) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest if selftest(): return 0 else: return 1 class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'update': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) >>> parse_args('log -r 500'.split()) (['log'], {'revision': 500}) >>> parse_args('log -r500:600'.split()) (['log'], {'revision': [500, 600]}) >>> parse_args('log -vr500:600'.split()) (['log'], {'verbose': True, 'revision': [500, 600]}) >>> parse_args('log -rv500:600'.split()) #the r takes an argument Traceback (most recent call last): ... ValueError: invalid literal for int(): v500 """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt in SHORT_OPTIONS: # Multi-character options must have a space to delimit # their value optname = SHORT_OPTIONS[shortopt] else: # Single character short options, can be chained, # and have their value appended to their name shortopt = a[1:2] if shortopt not in SHORT_OPTIONS: # We didn't find the multi-character name, and we # didn't find the single char name bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if a[2:]: # There are extra things on this option # see if it is the value, or if it is another # short option optargfn = OPTIONS[optname] if optargfn is None: # This option does not take an argument, so the # next entry is another short option, pack it back # into the list argv.insert(0, '-' + a[2:]) else: # This option takes an argument, so pack it # into the array optarg = a[2:] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] include_plugins=True try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd,include_plugins=include_plugins) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :684 committer Martin Pool 1118806647 +1000 data 160 - Strip any number of trailing slashes and backslashes from the path name when creating a new branch name from the user-supplied branch name. patch from aaron from :683 M 644 inline bzrlib/commands.py data 48586 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _find_plugins(): """Find all python files which are plugins, and load their commands to add to the list of "all commands" The environment variable BZRPATH is considered a delimited set of paths to look through. Each entry is searched for *.py files. If a directory is found, it is also searched, but they are not searched recursively. This allows you to revctl the plugins. Inside the plugin should be a series of cmd_* function, which inherit from the bzrlib.commands.Command class. """ bzrpath = os.environ.get('BZRPLUGINPATH', '') plugin_cmds = {} if not bzrpath: return plugin_cmds _platform_extensions = { 'win32':'.pyd', 'cygwin':'.dll', 'darwin':'.dylib', 'linux2':'.so' } if _platform_extensions.has_key(sys.platform): platform_extension = _platform_extensions[sys.platform] else: platform_extension = None for d in bzrpath.split(os.pathsep): plugin_names = {} # This should really be a set rather than a dict for f in os.listdir(d): if f.endswith('.py'): f = f[:-3] elif f.endswith('.pyc') or f.endswith('.pyo'): f = f[:-4] elif platform_extension and f.endswith(platform_extension): f = f[:-len(platform_extension)] if f.endswidth('module'): f = f[:-len('module')] else: continue if not plugin_names.has_key(f): plugin_names[f] = True plugin_names = plugin_names.keys() plugin_names.sort() try: sys.path.insert(0, d) for name in plugin_names: try: old_module = None try: if sys.modules.has_key(name): old_module = sys.modules[name] del sys.modules[name] plugin = __import__(name, locals()) for k in dir(plugin): if k.startswith('cmd_'): k_unsquished = _unsquish_command_name(k) if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = getattr(plugin, k) else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r in dir %r' % (name, d)) finally: if old_module: sys.modules[name] = old_module except ImportError, e: log_error('Unable to load plugin: %r from %r\n%s' % (name, d, e)) finally: sys.path.pop(0) return plugin_cmds def _get_cmd_dict(include_plugins=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v if include_plugins: d.update(_find_plugins()) return d def get_all_cmds(include_plugins=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(include_plugins=include_plugins).iteritems(): yield k,v def get_cmd_class(cmd,include_plugins=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(include_plugins=include_plugins) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import errno br_to = Branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if errno == errno.ENOENT: raise if location is None: location = stored_loc if location is None: raise BzrCommandError("No pull location known or specified.") from branch import find_branch, DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged. Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. """ takes_args = ['from_location', 'to_location?'] def run(self, from_location, to_location=None): import errno from bzrlib.merge import merge from branch import find_branch, DivergedBranches try: br_from = find_branch(from_location) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location.rstrip("/\\")) try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) from_location = pull_loc(br_from) br_to.update_revisions(br_from) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False) br_to.controlfile("x-pull", "wb").write(from_location + "\n") def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, exports to a directory (equivalent to --format=dir).""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format'] def run(self, dest, revision=None, format='dir'): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest, format) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] takes_options = ['update'] def run(self, dir='.', update=False): import bzrlib.check bzrlib.check.check(Branch(dir), update=update) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest if selftest(): return 0 else: return 1 class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'update': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) >>> parse_args('log -r 500'.split()) (['log'], {'revision': 500}) >>> parse_args('log -r500:600'.split()) (['log'], {'revision': [500, 600]}) >>> parse_args('log -vr500:600'.split()) (['log'], {'verbose': True, 'revision': [500, 600]}) >>> parse_args('log -rv500:600'.split()) #the r takes an argument Traceback (most recent call last): ... ValueError: invalid literal for int(): v500 """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt in SHORT_OPTIONS: # Multi-character options must have a space to delimit # their value optname = SHORT_OPTIONS[shortopt] else: # Single character short options, can be chained, # and have their value appended to their name shortopt = a[1:2] if shortopt not in SHORT_OPTIONS: # We didn't find the multi-character name, and we # didn't find the single char name bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if a[2:]: # There are extra things on this option # see if it is the value, or if it is another # short option optargfn = OPTIONS[optname] if optargfn is None: # This option does not take an argument, so the # next entry is another short option, pack it back # into the list argv.insert(0, '-' + a[2:]) else: # This option takes an argument, so pack it # into the array optarg = a[2:] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] include_plugins=True try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd,include_plugins=include_plugins) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :685 committer Martin Pool 1118806984 +1000 data 56 - add -r option to the branch command patch from aaron from :684 M 644 inline bzrlib/branch.py data 36063 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_file, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def _relpath(base, path): """Return path relative to base, or raise exception. The path may be either an absolute path or a path relative to the current working directory. Lifted out of Branch.relpath for ease of testing. os.path.commonprefix (python2.4) has a bad bug that it works just on string prefixes, assuming that '/u' is a prefix of '/u2'. This avoids that problem.""" rp = os.path.abspath(path) s = [] head = rp while len(head) >= len(base): if head == base: break head, tail = os.path.split(head) if tail: s.insert(0, tail) else: from errors import NotBranchError raise NotBranchError("path %r is not within branch %r" % (rp, base)) return os.sep.join(s) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head class DivergedBranches(Exception): def __init__(self, branch1, branch2): self.branch1 = branch1 self.branch2 = branch2 Exception.__init__(self, "These branches have diverged.") class NoSuchRevision(BzrError): def __init__(self, branch, revision): self.branch = branch self.revision = revision msg = "Branch %s has no revision %d" % (branch, revision) BzrError.__init__(self, msg) ###################################################################### # branch objects class Branch(object): """Branch holding a history of revisions. base Base directory of the branch. _lock_mode None, or 'r' or 'w' _lock_count If _lock_mode is true, a positive count of the number of times the lock has been taken. _lock Lock object from bzrlib.lock. """ base = None _lock_mode = None _lock_count = None _lock = None def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): from errors import NotBranchError raise NotBranchError("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def __del__(self): if self._lock_mode or self._lock: from warnings import warn warn("branch %r was not explicitly unlocked" % self) self._lock.unlock() def lock_write(self): if self._lock_mode: if self._lock_mode != 'w': from errors import LockError raise LockError("can't upgrade to a write lock from %r" % self._lock_mode) self._lock_count += 1 else: from bzrlib.lock import WriteLock self._lock = WriteLock(self.controlfilename('branch-lock')) self._lock_mode = 'w' self._lock_count = 1 def lock_read(self): if self._lock_mode: assert self._lock_mode in ('r', 'w'), \ "invalid lock mode %r" % self._lock_mode self._lock_count += 1 else: from bzrlib.lock import ReadLock self._lock = ReadLock(self.controlfilename('branch-lock')) self._lock_mode = 'r' self._lock_count = 1 def unlock(self): if not self._lock_mode: from errors import LockError raise LockError('branch %r is not locked' % (self)) if self._lock_count > 1: self._lock_count -= 1 else: self._lock.unlock() self._lock = None self._lock_mode = self._lock_count = None def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" return _relpath(self.base, path) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.\n") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: raise BzrError('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. self.lock_read() try: inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv finally: self.unlock() def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. files List of paths to add, relative to the base of the tree. ids If set, use these instead of automatically generated ids. Must be the same length as the list of files, but may contain None for ids that are to be autogenerated. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) self.lock_write() try: inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): raise BzrError("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: raise BzrError("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) finally: self.unlock() def print_file(self, file, revno): """Print `file` to stdout.""" self.lock_read() try: tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: raise BzrError("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) finally: self.unlock() def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] self.lock_write() try: tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: raise BzrError("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) finally: self.unlock() # FIXME: this doesn't need to be a branch method def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" if not revision_id or not isinstance(revision_id, basestring): raise ValueError('invalid revision-id: %r' % revision_id) r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_revision_sha1(self, revision_id): """Hash the stored value of a revision, and return it.""" # In the future, revision entries will be signed. At that # point, it is probably best *not* to include the signature # in the revision hash. Because that lets you re-sign # the revision, (add signatures/remove signatures) and still # have all hash pointers stay consistent. # But for now, just hash the contents. return sha_file(self.revision_store[revision_id]) def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_inventory_sha1(self, inventory_id): """Return the sha1 hash of the inventory entry """ return sha_file(self.inventory_store[inventory_id]) def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self.lock_read() try: return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] finally: self.unlock() def common_ancestor(self, other, self_revno=None, other_revno=None): """ >>> import commit >>> sb = ScratchBranch(files=['foo', 'foo~']) >>> sb.common_ancestor(sb) == (None, None) True >>> commit.commit(sb, "Committing first revision", verbose=False) >>> sb.common_ancestor(sb)[0] 1 >>> clone = sb.clone() >>> commit.commit(sb, "Committing second revision", verbose=False) >>> sb.common_ancestor(sb)[0] 2 >>> sb.common_ancestor(clone)[0] 1 >>> commit.commit(clone, "Committing divergent second revision", ... verbose=False) >>> sb.common_ancestor(clone)[0] 1 >>> sb.common_ancestor(clone) == clone.common_ancestor(sb) True >>> sb.common_ancestor(sb) != clone.common_ancestor(clone) True >>> clone2 = sb.clone() >>> sb.common_ancestor(clone2)[0] 2 >>> sb.common_ancestor(clone2, self_revno=1)[0] 1 >>> sb.common_ancestor(clone2, other_revno=1)[0] 1 """ my_history = self.revision_history() other_history = other.revision_history() if self_revno is None: self_revno = len(my_history) if other_revno is None: other_revno = len(other_history) indices = range(min((self_revno, other_revno))) indices.reverse() for r in indices: if my_history[r] == other_history[r]: return r+1, my_history[r] return None, None def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def missing_revisions(self, other, stop_revision=None): """ If self and other have not diverged, return a list of the revisions present in other, but missing from self. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch() >>> br2 = ScratchBranch() >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [u'REVISION-ID-1'] >>> br2.missing_revisions(br1) [] >>> commit(br1, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-2A") >>> br1.missing_revisions(br2) [u'REVISION-ID-2A'] >>> commit(br1, "lala!", rev_id="REVISION-ID-2B") >>> br1.missing_revisions(br2) Traceback (most recent call last): DivergedBranches: These branches have diverged. """ self_history = self.revision_history() self_len = len(self_history) other_history = other.revision_history() other_len = len(other_history) common_index = min(self_len, other_len) -1 if common_index >= 0 and \ self_history[common_index] != other_history[common_index]: raise DivergedBranches(self, other) if stop_revision is None: stop_revision = other_len elif stop_revision > other_len: raise NoSuchRevision(self, stop_revision) return other_history[self_len:stop_revision] def update_revisions(self, other, stop_revision=None): """Pull in all new revisions from other branch. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch(files=['foo', 'bar']) >>> br1.add('foo') >>> br1.add('bar') >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False) >>> br2 = ScratchBranch() >>> br2.update_revisions(br1) Added 2 texts. Added 1 inventories. Added 1 revisions. >>> br2.revision_history() [u'REVISION-ID-1'] >>> br2.update_revisions(br1) Added 0 texts. Added 0 inventories. Added 0 revisions. >>> br1.text_store.total_size() == br2.text_store.total_size() True """ from bzrlib.progress import ProgressBar pb = ProgressBar() pb.update('comparing histories') revision_ids = self.missing_revisions(other, stop_revision) revisions = [] needed_texts = sets.Set() i = 0 for rev_id in revision_ids: i += 1 pb.update('fetching revision', i, len(revision_ids)) rev = other.get_revision(rev_id) revisions.append(rev) inv = other.get_inventory(str(rev.inventory_id)) for key, entry in inv.iter_entries(): if entry.text_id is None: continue if entry.text_id not in self.text_store: needed_texts.add(entry.text_id) pb.clear() count = self.text_store.copy_multi(other.text_store, needed_texts) print "Added %d texts." % count inventory_ids = [ f.inventory_id for f in revisions ] count = self.inventory_store.copy_multi(other.inventory_store, inventory_ids) print "Added %d inventories." % count revision_ids = [ f.revision_id for f in revisions] count = self.revision_store.copy_multi(other.revision_store, revision_ids) for revision_id in revision_ids: self.append_revision(revision_id) print "Added %d revisions." % count def commit(self, *args, **kw): """Deprecated""" from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self.lock_write() try: tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): raise BzrError("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): raise BzrError("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: raise BzrError("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): raise BzrError("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': raise BzrError("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self.lock_write() try: ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): raise BzrError("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): raise BzrError("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': raise BzrError("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): raise BzrError("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): raise BzrError("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: raise BzrError("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): raise BzrError("destination %r already exists" % dest_path) if f_id in to_idpath: raise BzrError("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[], base=None): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ init = False if base is None: base = tempfile.mkdtemp() init = True Branch.__init__(self, base, init=init) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def clone(self): """ >>> orig = ScratchBranch(files=["file1", "file2"]) >>> clone = orig.clone() >>> os.path.samefile(orig.base, clone.base) False >>> os.path.isfile(os.path.join(clone.base, "file1")) True """ base = tempfile.mkdtemp() os.rmdir(base) shutil.copytree(self.base, base, symlinks=True) return ScratchBranch(base=base) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: if self.base: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/commands.py data 49085 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _find_plugins(): """Find all python files which are plugins, and load their commands to add to the list of "all commands" The environment variable BZRPATH is considered a delimited set of paths to look through. Each entry is searched for *.py files. If a directory is found, it is also searched, but they are not searched recursively. This allows you to revctl the plugins. Inside the plugin should be a series of cmd_* function, which inherit from the bzrlib.commands.Command class. """ bzrpath = os.environ.get('BZRPLUGINPATH', '') plugin_cmds = {} if not bzrpath: return plugin_cmds _platform_extensions = { 'win32':'.pyd', 'cygwin':'.dll', 'darwin':'.dylib', 'linux2':'.so' } if _platform_extensions.has_key(sys.platform): platform_extension = _platform_extensions[sys.platform] else: platform_extension = None for d in bzrpath.split(os.pathsep): plugin_names = {} # This should really be a set rather than a dict for f in os.listdir(d): if f.endswith('.py'): f = f[:-3] elif f.endswith('.pyc') or f.endswith('.pyo'): f = f[:-4] elif platform_extension and f.endswith(platform_extension): f = f[:-len(platform_extension)] if f.endswidth('module'): f = f[:-len('module')] else: continue if not plugin_names.has_key(f): plugin_names[f] = True plugin_names = plugin_names.keys() plugin_names.sort() try: sys.path.insert(0, d) for name in plugin_names: try: old_module = None try: if sys.modules.has_key(name): old_module = sys.modules[name] del sys.modules[name] plugin = __import__(name, locals()) for k in dir(plugin): if k.startswith('cmd_'): k_unsquished = _unsquish_command_name(k) if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = getattr(plugin, k) else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r in dir %r' % (name, d)) finally: if old_module: sys.modules[name] = old_module except ImportError, e: log_error('Unable to load plugin: %r from %r\n%s' % (name, d, e)) finally: sys.path.pop(0) return plugin_cmds def _get_cmd_dict(include_plugins=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v if include_plugins: d.update(_find_plugins()) return d def get_all_cmds(include_plugins=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(include_plugins=include_plugins).iteritems(): yield k,v def get_cmd_class(cmd,include_plugins=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(include_plugins=include_plugins) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path # TODO: If either of these fail, we should detect that and # assume that path is not really a bzr plugin after all. pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() self.takes_args = pipe.readline().split() pipe.close() pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() pipe.close() def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import errno br_to = Branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if errno == errno.ENOENT: raise if location is None: location = stored_loc if location is None: raise BzrCommandError("No pull location known or specified.") from branch import find_branch, DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged. Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. To retrieve the branch as of a particular revision, supply the --revision parameter, as in "branch foo/bar -r 5". """ takes_args = ['from_location', 'to_location?'] takes_options = ['revision'] def run(self, from_location, to_location=None, revision=None): import errno from bzrlib.merge import merge from branch import find_branch, DivergedBranches, NoSuchRevision from shutil import rmtree try: br_from = find_branch(from_location) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location.rstrip("/\\")) try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) try: br_to.update_revisions(br_from, stop_revision=revision) except NoSuchRevision: rmtree(to_location) msg = "The branch %s has no revision %d." % (from_location, revision) raise BzrCommandError(msg) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False) from_location = pull_loc(br_from) br_to.controlfile("x-pull", "wb").write(from_location + "\n") def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, exports to a directory (equivalent to --format=dir).""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format'] def run(self, dest, revision=None, format='dir'): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest, format) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] takes_options = ['update'] def run(self, dir='.', update=False): import bzrlib.check bzrlib.check.check(Branch(dir), update=update) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest if selftest(): return 0 else: return 1 class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'update': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) >>> parse_args('log -r 500'.split()) (['log'], {'revision': 500}) >>> parse_args('log -r500:600'.split()) (['log'], {'revision': [500, 600]}) >>> parse_args('log -vr500:600'.split()) (['log'], {'verbose': True, 'revision': [500, 600]}) >>> parse_args('log -rv500:600'.split()) #the r takes an argument Traceback (most recent call last): ... ValueError: invalid literal for int(): v500 """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt in SHORT_OPTIONS: # Multi-character options must have a space to delimit # their value optname = SHORT_OPTIONS[shortopt] else: # Single character short options, can be chained, # and have their value appended to their name shortopt = a[1:2] if shortopt not in SHORT_OPTIONS: # We didn't find the multi-character name, and we # didn't find the single char name bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if a[2:]: # There are extra things on this option # see if it is the value, or if it is another # short option optargfn = OPTIONS[optname] if optargfn is None: # This option does not take an argument, so the # next entry is another short option, pack it back # into the list argv.insert(0, '-' + a[2:]) else: # This option takes an argument, so pack it # into the array optarg = a[2:] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] include_plugins=True try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd,include_plugins=include_plugins) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :686 committer Martin Pool 1118808866 +1000 data 66 - glob expand add arguments on win32 patch from Roncaglia Julien from :685 M 644 inline bzrlib/add.py data 3621 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, sys import bzrlib from trace import mutter, note def glob_expand_for_win32(file_list): import glob expanded_file_list = [] for possible_glob in file_list: glob_files = glob.glob(possible_glob) if glob_files == []: # special case to let the normal code path handle # files that do not exists expanded_file_list.append(possible_glob) else: expanded_file_list += glob_files return expanded_file_list def smart_add(file_list, verbose=True, recurse=True): """Add files to version, optionally recursing into directories. This is designed more towards DWIM for humans than API simplicity. For the specific behaviour see the help for cmd_add(). """ from bzrlib.osutils import quotefn, kind_marker from bzrlib.errors import BadFileKindError, ForbiddenFileError assert file_list if sys.platform == 'win32': file_list = glob_expand_for_win32(file_list) user_list = file_list[:] assert not isinstance(file_list, basestring) b = bzrlib.branch.Branch(file_list[0], find_root=True) inv = b.read_working_inventory() tree = b.working_tree() count = 0 for f in file_list: rf = b.relpath(f) af = b.abspath(rf) kind = bzrlib.osutils.file_kind(af) if kind != 'file' and kind != 'directory': if f in user_list: raise BadFileKindError("cannot add %s of type %s" % (f, kind)) else: print "skipping %s (can't add file of kind '%s')" % (f, kind) continue bzrlib.mutter("smart add of %r, abs=%r" % (f, af)) if bzrlib.branch.is_control_file(af): raise ForbiddenFileError('cannot add control file %s' % f) versioned = (inv.path2id(rf) != None) if rf == '': mutter("branch root doesn't need to be added") elif versioned: mutter("%r is already versioned" % f) else: file_id = bzrlib.branch.gen_file_id(rf) inv.add_path(rf, kind=kind, file_id=file_id) bzrlib.mutter("added %r kind %r file_id={%s}" % (rf, kind, file_id)) count += 1 print 'added', quotefn(f) if kind == 'directory' and recurse: for subf in os.listdir(af): subp = os.path.join(rf, subf) if subf == bzrlib.BZRDIR: mutter("skip control directory %r" % subp) elif tree.is_ignored(subp): mutter("skip ignored sub-file %r" % subp) else: mutter("queue to add sub-file %r" % subp) file_list.append(b.abspath(subp)) if count > 0: if verbose: note('added %d' % count) b._write_inventory(inv) commit refs/heads/master mark :687 committer Martin Pool 1118808994 +1000 data 58 - trap more errors from external commands patch from mpe from :686 M 644 inline bzrlib/commands.py data 49365 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _find_plugins(): """Find all python files which are plugins, and load their commands to add to the list of "all commands" The environment variable BZRPATH is considered a delimited set of paths to look through. Each entry is searched for *.py files. If a directory is found, it is also searched, but they are not searched recursively. This allows you to revctl the plugins. Inside the plugin should be a series of cmd_* function, which inherit from the bzrlib.commands.Command class. """ bzrpath = os.environ.get('BZRPLUGINPATH', '') plugin_cmds = {} if not bzrpath: return plugin_cmds _platform_extensions = { 'win32':'.pyd', 'cygwin':'.dll', 'darwin':'.dylib', 'linux2':'.so' } if _platform_extensions.has_key(sys.platform): platform_extension = _platform_extensions[sys.platform] else: platform_extension = None for d in bzrpath.split(os.pathsep): plugin_names = {} # This should really be a set rather than a dict for f in os.listdir(d): if f.endswith('.py'): f = f[:-3] elif f.endswith('.pyc') or f.endswith('.pyo'): f = f[:-4] elif platform_extension and f.endswith(platform_extension): f = f[:-len(platform_extension)] if f.endswidth('module'): f = f[:-len('module')] else: continue if not plugin_names.has_key(f): plugin_names[f] = True plugin_names = plugin_names.keys() plugin_names.sort() try: sys.path.insert(0, d) for name in plugin_names: try: old_module = None try: if sys.modules.has_key(name): old_module = sys.modules[name] del sys.modules[name] plugin = __import__(name, locals()) for k in dir(plugin): if k.startswith('cmd_'): k_unsquished = _unsquish_command_name(k) if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = getattr(plugin, k) else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r in dir %r' % (name, d)) finally: if old_module: sys.modules[name] = old_module except ImportError, e: log_error('Unable to load plugin: %r from %r\n%s' % (name, d, e)) finally: sys.path.pop(0) return plugin_cmds def _get_cmd_dict(include_plugins=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v if include_plugins: d.update(_find_plugins()) return d def get_all_cmds(include_plugins=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(include_plugins=include_plugins).iteritems(): yield k,v def get_cmd_class(cmd,include_plugins=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(include_plugins=include_plugins) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() for opt in self.takes_options: if not opt in OPTIONS: bailout("Unknown option '%s' returned by external command %s" % (opt, path)) # TODO: Is there any way to check takes_args is valid here? self.takes_args = pipe.readline().split() if pipe.close() is not None: bailout("Failed funning '%s --bzr-usage'" % path) pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() if pipe.close() is not None: bailout("Failed funning '%s --bzr-help'" % path) def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: value = kargs[name] if OPTIONS.has_key(name): # it's an option opts.append('--%s' % name) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import errno br_to = Branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if errno == errno.ENOENT: raise if location is None: location = stored_loc if location is None: raise BzrCommandError("No pull location known or specified.") from branch import find_branch, DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged. Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. To retrieve the branch as of a particular revision, supply the --revision parameter, as in "branch foo/bar -r 5". """ takes_args = ['from_location', 'to_location?'] takes_options = ['revision'] def run(self, from_location, to_location=None, revision=None): import errno from bzrlib.merge import merge from branch import find_branch, DivergedBranches, NoSuchRevision from shutil import rmtree try: br_from = find_branch(from_location) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location.rstrip("/\\")) try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) try: br_to.update_revisions(br_from, stop_revision=revision) except NoSuchRevision: rmtree(to_location) msg = "The branch %s has no revision %d." % (from_location, revision) raise BzrCommandError(msg) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False) from_location = pull_loc(br_from) br_to.controlfile("x-pull", "wb").write(from_location + "\n") def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, exports to a directory (equivalent to --format=dir).""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format'] def run(self, dest, revision=None, format='dir'): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest, format) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] takes_options = ['update'] def run(self, dir='.', update=False): import bzrlib.check bzrlib.check.check(Branch(dir), update=update) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest if selftest(): return 0 else: return 1 class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'update': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) >>> parse_args('log -r 500'.split()) (['log'], {'revision': 500}) >>> parse_args('log -r500:600'.split()) (['log'], {'revision': [500, 600]}) >>> parse_args('log -vr500:600'.split()) (['log'], {'verbose': True, 'revision': [500, 600]}) >>> parse_args('log -rv500:600'.split()) #the r takes an argument Traceback (most recent call last): ... ValueError: invalid literal for int(): v500 """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt in SHORT_OPTIONS: # Multi-character options must have a space to delimit # their value optname = SHORT_OPTIONS[shortopt] else: # Single character short options, can be chained, # and have their value appended to their name shortopt = a[1:2] if shortopt not in SHORT_OPTIONS: # We didn't find the multi-character name, and we # didn't find the single char name bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if a[2:]: # There are extra things on this option # see if it is the value, or if it is another # short option optargfn = OPTIONS[optname] if optargfn is None: # This option does not take an argument, so the # next entry is another short option, pack it back # into the list argv.insert(0, '-' + a[2:]) else: # This option takes an argument, so pack it # into the array optarg = a[2:] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] include_plugins=True try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd,include_plugins=include_plugins) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :688 committer Martin Pool 1118809032 +1000 data 93 - add deferred patch from abentley to fetch remote Revision XML only once when branching from :687 M 644 inline patches/cache-remote-revisions.diff data 4965 *************** *** 738,777 **** revisions = [] pb = ProgressBar(show_spinner=True) total = len(revision_ids) - for i,f in enumerate(revision_ids): - revisions.append(other.get_revision(f)) - pb.update('retrieving revisions', i+1, total) - pb.clear() - - needed_texts = sets.Set() - - for index, rev in enumerate(revisions): - pb.update('Scanning revisions for file contents', index, total) - inv = other.get_inventory(str(rev.inventory_id)) - for key, entry in inv.iter_entries(): - if entry.text_id is None: - continue - if entry.text_id not in self.text_store: - needed_texts.add(entry.text_id) - pb.clear() - count = self.text_store.copy_multi(other.text_store, needed_texts, pb, - "Copying file contents") - pb.clear() - print "Added %d file contents." % count - inventory_ids = [ f.inventory_id for f in revisions ] - count = self.inventory_store.copy_multi(other.inventory_store, - inventory_ids, pb, - "Copying inventories") - pb.clear() - print "Added %d inventories." % count - revision_ids = [ f.revision_id for f in revisions] - count = self.revision_store.copy_multi(other.revision_store, - revision_ids, pb, - "Copying revisions") - pb.clear() - for revision_id in revision_ids: - self.append_revision(revision_id) - print "Added %d revisions." % count def commit(self, *args, **kw): --- 738,799 ---- revisions = [] pb = ProgressBar(show_spinner=True) total = len(revision_ids) + tmp_dir = tempfile.mkdtemp(prefix = "temp-stores-") + try: + tmp_rev_dir = os.path.join(tmp_dir, "revisions") + os.mkdir(tmp_rev_dir) + tmp_revs = ImmutableStore(tmp_rev_dir) + count = tmp_revs.copy_multi(other.revision_store, revision_ids, pb, + "Caching revisions") + #EVIL! Substituting a local partial store for a complete one + #This is a significant performance boost when complete one is + #a remote store. + other.revision_store = tmp_revs + pb.clear() + + for i,f in enumerate(revision_ids): + revisions.append(other.get_revision(f)) + pb.update("Parsing revisions", i, len(revision_ids)) + + needed_texts = sets.Set() + + #Again with the EVIL. + tmp_rev_dir = os.path.join(tmp_dir, "inventories") + os.mkdir(tmp_rev_dir) + inv_ids = [r.inventory_id for r in revisions] + tmp_revs = ImmutableStore(tmp_rev_dir) + count = tmp_revs.copy_multi(other.inventory_store, inv_ids, pb, + "Caching inventories") + other.inventory_store = tmp_revs + pb.clear() + for index, rev in enumerate(revisions): + pb.update('Scanning revisions for file contents', index, total) + inv = other.get_inventory(str(rev.inventory_id)) + for key, entry in inv.iter_entries(): + if entry.text_id is None: + continue + if entry.text_id not in self.text_store: + needed_texts.add(entry.text_id) + pb.clear() + count = self.text_store.copy_multi(other.text_store, needed_texts, pb, + "Copying file contents") + pb.clear() + print "Added %d file contents." % count + inventory_ids = [ f.inventory_id for f in revisions ] + count = self.inventory_store.copy_multi(other.inventory_store, + inventory_ids, pb, + "Copying inventories") + pb.clear() + print "Added %d inventories." % count + revision_ids = [ f.revision_id for f in revisions] + count = self.revision_store.copy_multi(other.revision_store, + revision_ids) + pb.clear() + for revision_id in revision_ids: + self.append_revision(revision_id) + print "Added %d revisions." % count + finally: + shutil.rmtree(tmp_dir) def commit(self, *args, **kw): commit refs/heads/master mark :689 committer Martin Pool 1118809122 +1000 data 66 - make options with - work with external commands patch from mpe from :688 M 644 inline bzrlib/commands.py data 49415 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import bailout, BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _find_plugins(): """Find all python files which are plugins, and load their commands to add to the list of "all commands" The environment variable BZRPATH is considered a delimited set of paths to look through. Each entry is searched for *.py files. If a directory is found, it is also searched, but they are not searched recursively. This allows you to revctl the plugins. Inside the plugin should be a series of cmd_* function, which inherit from the bzrlib.commands.Command class. """ bzrpath = os.environ.get('BZRPLUGINPATH', '') plugin_cmds = {} if not bzrpath: return plugin_cmds _platform_extensions = { 'win32':'.pyd', 'cygwin':'.dll', 'darwin':'.dylib', 'linux2':'.so' } if _platform_extensions.has_key(sys.platform): platform_extension = _platform_extensions[sys.platform] else: platform_extension = None for d in bzrpath.split(os.pathsep): plugin_names = {} # This should really be a set rather than a dict for f in os.listdir(d): if f.endswith('.py'): f = f[:-3] elif f.endswith('.pyc') or f.endswith('.pyo'): f = f[:-4] elif platform_extension and f.endswith(platform_extension): f = f[:-len(platform_extension)] if f.endswidth('module'): f = f[:-len('module')] else: continue if not plugin_names.has_key(f): plugin_names[f] = True plugin_names = plugin_names.keys() plugin_names.sort() try: sys.path.insert(0, d) for name in plugin_names: try: old_module = None try: if sys.modules.has_key(name): old_module = sys.modules[name] del sys.modules[name] plugin = __import__(name, locals()) for k in dir(plugin): if k.startswith('cmd_'): k_unsquished = _unsquish_command_name(k) if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = getattr(plugin, k) else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r in dir %r' % (name, d)) finally: if old_module: sys.modules[name] = old_module except ImportError, e: log_error('Unable to load plugin: %r from %r\n%s' % (name, d, e)) finally: sys.path.pop(0) return plugin_cmds def _get_cmd_dict(include_plugins=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v if include_plugins: d.update(_find_plugins()) return d def get_all_cmds(include_plugins=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(include_plugins=include_plugins).iteritems(): yield k,v def get_cmd_class(cmd,include_plugins=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(include_plugins=include_plugins) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() for opt in self.takes_options: if not opt in OPTIONS: bailout("Unknown option '%s' returned by external command %s" % (opt, path)) # TODO: Is there any way to check takes_args is valid here? self.takes_args = pipe.readline().split() if pipe.close() is not None: bailout("Failed funning '%s --bzr-usage'" % path) pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() if pipe.close() is not None: bailout("Failed funning '%s --bzr-help'" % path) def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: optname = name.replace('_','-') value = kargs[name] if OPTIONS.has_key(optname): # it's an option opts.append('--%s' % optname) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import errno br_to = Branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if errno == errno.ENOENT: raise if location is None: location = stored_loc if location is None: raise BzrCommandError("No pull location known or specified.") from branch import find_branch, DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged. Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. To retrieve the branch as of a particular revision, supply the --revision parameter, as in "branch foo/bar -r 5". """ takes_args = ['from_location', 'to_location?'] takes_options = ['revision'] def run(self, from_location, to_location=None, revision=None): import errno from bzrlib.merge import merge from branch import find_branch, DivergedBranches, NoSuchRevision from shutil import rmtree try: br_from = find_branch(from_location) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location.rstrip("/\\")) try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) try: br_to.update_revisions(br_from, stop_revision=revision) except NoSuchRevision: rmtree(to_location) msg = "The branch %s has no revision %d." % (from_location, revision) raise BzrCommandError(msg) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False) from_location = pull_loc(br_from) br_to.controlfile("x-pull", "wb").write(from_location + "\n") def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: bailout("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: bailout("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, exports to a directory (equivalent to --format=dir).""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format'] def run(self, dest, revision=None, format='dir'): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest, format) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] takes_options = ['update'] def run(self, dir='.', update=False): import bzrlib.check bzrlib.check.check(Branch(dir), update=update) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest if selftest(): return 0 else: return 1 class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'update': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) >>> parse_args('log -r 500'.split()) (['log'], {'revision': 500}) >>> parse_args('log -r500:600'.split()) (['log'], {'revision': [500, 600]}) >>> parse_args('log -vr500:600'.split()) (['log'], {'verbose': True, 'revision': [500, 600]}) >>> parse_args('log -rv500:600'.split()) #the r takes an argument Traceback (most recent call last): ... ValueError: invalid literal for int(): v500 """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: bailout('unknown long option %r' % a) else: shortopt = a[1:] if shortopt in SHORT_OPTIONS: # Multi-character options must have a space to delimit # their value optname = SHORT_OPTIONS[shortopt] else: # Single character short options, can be chained, # and have their value appended to their name shortopt = a[1:2] if shortopt not in SHORT_OPTIONS: # We didn't find the multi-character name, and we # didn't find the single char name bailout('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if a[2:]: # There are extra things on this option # see if it is the value, or if it is another # short option optargfn = OPTIONS[optname] if optargfn is None: # This option does not take an argument, so the # next entry is another short option, pack it back # into the list argv.insert(0, '-' + a[2:]) else: # This option takes an argument, so pack it # into the array optarg = a[2:] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? bailout('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: bailout('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: bailout('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] include_plugins=True try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd,include_plugins=include_plugins) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :690 committer Martin Pool 1118809441 +1000 data 40 add-bzr-to-baz allows multiple arguments from :689 M 644 inline contrib/add-bzr-to-baz data 223 #! /bin/sh -e # Take a file that is versioned by bzr and # add it to baz with the same file-id. if [ $# -lt 1 ] then echo "usage: $0 FILE" >&2 exit 1 fi for f do baz add -i "$( bzr file-id "$f" )" "$f" done commit refs/heads/master mark :691 committer Martin Pool 1118809477 +1000 data 4 todo from :690 M 644 inline TODO data 13325 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * ``bzr log DIR`` should give changes to any files within DIR. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. Perhaps ~/.bzr/http-cache. Baz has a fairly simple cache under ~/.arch-cache, containing revision information encoded almost as a bunch of archives. Perhaps we could simply store full paths. * Maybe also store directories in the statcache so that we can quickly identify that they still exist. * Diff should show timestamps; for files from the working directory we can use the file itself; for files from a revision we should use the commit time of the revision. * Perhaps split command infrastructure from the actual command definitions. * Cleaner support for negative boolean options like --no-recurse. * Statcache should possibly map all file paths to / separators * quotefn doubles all backslashes on Windows; this is probably not the best thing to do. What would be a better way to safely represent filenames? Perhaps we could doublequote things containing spaces, on the principle that filenames containing quotes are unlikely? Nice for humans; less good for machine parsing. * Patches should probably use only forward slashes, even on Windows, otherwise Unix patch can't apply them. (?) * Branch.update_revisions() inefficiently fetches revisions from the remote server twice; once to find out what text and inventory they need and then again to actually get the thing. This is a bit inefficient. One complicating factor here is that we don't really want to have revisions present in the revision-store until all their constituent parts are also stored. The basic problem is that RemoteBranch.get_revision() and similar methods return object, but what we really want is the raw XML, which can be popped into our own store. That needs to be refactored. * ``bzr status FOO`` where foo is ignored should say so. Medium things ------------- * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. We should be able to just get the id for the selected files, look up their location and diff just those files. No need to traverse the entire inventories. * ``bzr status DIR`` or ``bzr diff DIR`` should report on all changes under that directory. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from ElementTree to an object when it is read in, but rather wait until the program actually wants to know about that node. * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. - Selected-file commit - Impossible selected-file commit: adding things in non-versioned directories, crossing renames, etc. * Write a reproducible benchmark, perhaps importing various kernel versions. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Paranoid mode where we never trust SHA-1 matches. * Don't commit if there are no changes unless forced. * --dry-run mode for commit? (Or maybe just run with check-command=false?) * Generally, be a bit more verbose unless --silent is specified. * Function that finds all changes to files under a given directory; perhaps log should use this if a directory is given. * XML attributes might have trouble with filenames containing \n and \r. Do we really want to support this? I think perhaps not. * Remember execute bits, so that exports will work OK. * Unify smart_add and plain Branch.add(); perhaps smart_add should just build a list of files to add and pass that to the regular add function. * Function to list a directory, saying in which revision each file was last modified. Useful for web and gui interfaces, and slow to compute one file at a time. * unittest is standard, but the results are kind of ugly; would be nice to make it cleaner. * Check locking is correct during merge-related operations. * Perhaps attempts to get locks should timeout after some period of time, or at least display a progress message. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. Possibly this should be done by splitting the commit function into several parts (under a single interface). It is already rather large. Decomposition: - find tree modifications and prepare in-memory inventory - export that inventory to a temporary directory - run the test in that temporary directory - if that succeeded, continue to actually finish the commit What should be done with the text of modified files while this is underway? I don't think we want to count on holding them in memory and we can't trust the working files to stay in one place so I suppose we need to move them into the text store, or otherwise into a temporary directory. If the commit does not actually complete, we would rather the content was not left behind in the stores. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :692 committer Martin Pool 1118815396 +1000 data 13 0.0.5 release from :691 M 644 inline NEWS data 8309 bzr-0.0.5 2005-06-15 CHANGES: * ``bzr`` with no command now shows help rather than giving an error. Suggested by Michael Ellerman. * ``bzr status`` output format changed, because svn-style output doesn't really match the model of bzr. Now files are grouped by status and can be shown with their IDs. ``bzr status --all`` shows all versioned files and unknown files but not ignored files. * ``bzr log`` runs from most-recent to least-recent, the reverse of the previous order. The previous behaviour can be obtained with the ``--forward`` option. * ``bzr inventory`` by default shows only filenames, and also ids if ``--show-ids`` is given, in which case the id is the second field. ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New ``bzr ignore PATTERN`` command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.sw[nop] .git .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. File may be in a branch other than the working directory. * ``bzr log`` and ``bzr root`` can be given an http URL instead of a filename. * Commands can now be defined by external programs or scripts in a directory on $BZRPATH. * New "stat cache" avoids reading the contents of files if they haven't changed since the previous time. * If the Python interpreter is too old, try to find a better one or give an error. Based on a patch from Fredrik Lundh. * New optional parameter ``bzr info [BRANCH]``. * New form ``bzr commit SELECTED`` to commit only selected files. * New form ``bzr log -r FROM:TO`` shows changes in selected range; contributed by John A Meinel. * New option ``bzr diff --diff-options 'OPTS'`` allows passing options through to an external GNU diff. * New option ``bzr add --no-recurse`` to add a directory but not their contents. * ``bzr --version`` now shows more information if bzr is being run from a branch. BUG FIXES: * Fixed diff format so that added and removed files will be handled properly by patch. Fix from Lalo Martins. * Various fixes for files whose names contain spaces or other metacharacters. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. * testbzr requires python2.4, but can be used to test bzr running under a different version. * Tests added for many other changes in this release. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. * New developer commands ``added``, ``modified``. PORTABILITY: * Cope on Windows on python2.3 by using the weaker random seed. 2.4 is now only recommended. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/__init__.py data 2129 # (C) 2005 Canonical Development Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr library""" from inventory import Inventory, InventoryEntry from branch import Branch, ScratchBranch, find_branch from osutils import format_date from tree import Tree from diff import compare_trees from trace import mutter, warning, open_tracefile from log import show_log import add BZRDIR = ".bzr" DEFAULT_IGNORE = ['.bzr.log', '*~', '#*#', '*$', '.#*', '.*.sw[nop]', '.*.tmp', '*.tmp', '*.bak', '*.BAK', '*.orig', '*.o', '*.obj', '*.a', '*.py[oc]', '*.so', '*.exe', '*.elc', '{arch}', 'CVS', 'CVS.adm', '.svn', '_darcs', 'SCCS', 'RCS', '*,v', 'BitKeeper', '.git', 'TAGS', '.make.state', '.sconsign', '.tmp*', '.del-*'] IGNORE_FILENAME = ".bzrignore" import locale user_encoding = locale.getpreferredencoding() del locale __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __version__ = '0.0.5' def get_bzr_revision(): """If bzr is run from a branch, return (revno,revid) or None""" from errors import BzrError try: branch = Branch(__path__[0]) rh = branch.revision_history() if rh: return len(rh), rh[-1] else: return None except BzrError: return None commit refs/heads/master mark :693 committer Martin Pool 1118827488 +1000 data 36 - notes on tracking multiple parents from :692 M 644 inline doc/formats.txt data 8424 ***************** Bazaar-NG formats ***************** .. contents:: Since branches are working directories there is just a single directory format. There is one metadata directory called ``.bzr`` at the top of each tree. Control files inside ``.bzr`` are never touched by patches and should not normally be edited by the user. These files are designed so that repository-level operations are ACID without depending on atomic operations spanning multiple files. There are two particular cases: aborting a transaction in the middle, and contention from multiple processes. We also need to be careful to flush files to disk at appropriate points; even this may not be totally safe if the filesystem does not guarantee ordering between multiple file changes, so we need to be sure to roll back. The design must also be such that the directory can simply be copied and that hardlinked directories will work. (So we must always replace files, never just append.) A cache is kept under here of easily-accessible information about previous revisions. This should be under a single directory so that it can be easily identified, excluded from backups, removed, etc. This might contain pristine tree from previous revisions, manifests and inventories, etc. It might also contain working directories when building a commit, etc. Call this maybe ``cache`` or ``tmp``. I wonder if we should use .zip files for revisions and cacherevs rather than tar files so that random access is easier/more efficient. There is a Python library ``zipfile``. Signing XML files ***************** bzr relies on storing hashes or GPG signatures of various XML files. There can be multiple equivalent representations of the same XML tree, but these will have different byte-by-byte hashes. Once signed files are written out, they must be stored byte-for-byte and never re-encoded or renormalized, because that would break their hash or signature. Branch metadata *************** All inside ``.bzr`` ``README`` Tells people not to touch anything here. ``branch-format`` Identifies the parent as a Bazaar-NG branch; contains the overall branch metadata format as a string. ``pristine-directory`` Identifies that this is a pristine directory and may not be committed to. ``patches/`` Directory containing all patches applied to this branch, one per file. Patches are stored as compressed deltas. We also store the hash of the delta, hash of the before and after manifests, and optionally a GPG signature. ``cache/`` Contains various cached data that can be destroyed and will be recreated. (It should not be modified.) ``cache/pristine/`` Contains cached full trees for selected previous revisions, used when generating diffs, etc. ``cache/inventory/`` Contains cached inventories of previous revisions. ``cache/snapshot/`` Contains tarballs of cached revisions of the tree, named by their revision id. These can also be removed, but ``patch-history`` File containing the UUIDs of all patches taken in this branch, in the order they were taken. Each commit adds exactly one line to this file; lines are never removed or reordered. ``merged-patches`` List of foreign patches that have been merged into this branch. Must have no entries in common with ``patch-history``. Commits that include merges add to this file; lines are never removed or reordered. ``pending-merge-patches`` List of foreign patches that have been merged and are waiting to be committed. ``branch-name`` User-qualified name of the branch, for the purpose of describing the origin of patches, e.g. ``mbp@sourcefrog.net/distcc--main``. ``friends`` List of branches from which we have pulled; file containing a list of pairs of branch-name and location. ``parent`` Default pull/push target. ``pending-inventory`` Mapping from UUIDs to file name in the current working directory. ``branch-lock`` Lock held while modifying the branch, to protect against clashing updates. Locking ******* Is locking a good strategy? Perhaps somekind of read-copy-update or seq-lock based mechanism would work better? If we do use a locking algorithm, is it OK to rely on filesystem locking or do we need our own mechanism? I think most hosts should have reasonable ``flock()`` or equivalent, even on NFS. One risk is that on NFS it is easy to have broken locking and not know it, so it might be better to have something that will fail safe. Filesystem locks go away if the machine crashes or the process is terminated; this can be a feature in that we do not need to deal with stale locks but also a feature in that the lock itself does not indicate cleanup may be needed. robertc points out that tla converged on renaming a directory as a mechanism: this is one thing which is known to be atomic on almost all filesystems. Apparently renaming files, creating directories, making symlinks etc are not good enough. Delta ***** XML document plus a bag of patches, expressing the difference between two revisions. May be a partial delta. * list of entries * entry * parent directory (if any) * before-name or null if new * after-name or null if deleted * uuid * type (dir, file, symlink, ...) * patch type (patch, full-text, xdelta, ...) * patch filename (?) Inventory ********* XML document; series of entries. (Quite similar to the svn ``entries`` file; perhaps should even have that name.) Stored identified by its hash. An inventory is stored for recorded revisions, also a ``pending-inventory`` for a working directory. Revision ******** XML document. Stored identified by its hash. committer RFC-2822-style name of the committer. Should match the key used to sign the revision. comment multi-line free-form text; whitespace and line breaks preserved timestamp As floating-point seconds since epoch. branch name Name of the branch to which this was originally committed. (I'm not totally satisfied that this is the right way to do it; the results will be a bit weird when a series of revisions pass through variously named branches.) inventory_hash Acts as a pointer to the inventory for this revision. parents Zero, one, or more references to parent revisions. For each the revision-id and the revision file's hash are given. The first parent is by convention the revision in whose working tree the new revision was created. precursor Must be equal to the first parent, if any are given. For compatibility with bzr 0.0.5 and earlier; eventually will be removed. merged-branches Revision ids of complete branches merged into this revision. If a revision is listed, that revision and transitively its predecessor and all other merged-branches are merged. This is empty except where cherry-picks have occurred. merged-patches Revision ids of cherry-picked patches. Patches whose branches are merged need not be listed here. Listing a revision ID implies that only the change of that particular revision from its predecessor has been merged in. This is empty except where cherry-picks have occurred. The transitive closure avoids Arch's problem of needing to list a large number of previous revisions. As ddaa writes: Continuation revisions (created by tla tag or baz branch) are associated to a patchlog whose New-patches header lists the revisions associated to all the patchlogs present in the tree. That was introduced as an optimisation so the set of patchlogs in any revision could be determined solely by examining the patchlogs of ancestor revisions in the same branch. This behaves well as long as the total count of patchlog is reasonably small or new branches are not very frequent. A continuation revision on $tree currently creates a patchlog of about 500K. This patchlog is present in all descendent of the revision, and all revisions that merges it. It may be useful at some times to keep a cache of all the branches, or all the revisions, present in the history of a branch, so that we do need to walk the whole history of the branch to build this list. ---- Proposed changes **************** * Don't store parent-id in all revisions, but rather have nodes that contain entries for children? * Assign an id to the root of the tree, perhaps listed in the top of the inventory? commit refs/heads/master mark :694 committer Martin Pool 1118993333 +1000 data 67 - weed out all remaining calls to bailout() and remove the function from :693 M 644 inline bzrlib/commands.py data 49487 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _find_plugins(): """Find all python files which are plugins, and load their commands to add to the list of "all commands" The environment variable BZRPATH is considered a delimited set of paths to look through. Each entry is searched for *.py files. If a directory is found, it is also searched, but they are not searched recursively. This allows you to revctl the plugins. Inside the plugin should be a series of cmd_* function, which inherit from the bzrlib.commands.Command class. """ bzrpath = os.environ.get('BZRPLUGINPATH', '') plugin_cmds = {} if not bzrpath: return plugin_cmds _platform_extensions = { 'win32':'.pyd', 'cygwin':'.dll', 'darwin':'.dylib', 'linux2':'.so' } if _platform_extensions.has_key(sys.platform): platform_extension = _platform_extensions[sys.platform] else: platform_extension = None for d in bzrpath.split(os.pathsep): plugin_names = {} # This should really be a set rather than a dict for f in os.listdir(d): if f.endswith('.py'): f = f[:-3] elif f.endswith('.pyc') or f.endswith('.pyo'): f = f[:-4] elif platform_extension and f.endswith(platform_extension): f = f[:-len(platform_extension)] if f.endswidth('module'): f = f[:-len('module')] else: continue if not plugin_names.has_key(f): plugin_names[f] = True plugin_names = plugin_names.keys() plugin_names.sort() try: sys.path.insert(0, d) for name in plugin_names: try: old_module = None try: if sys.modules.has_key(name): old_module = sys.modules[name] del sys.modules[name] plugin = __import__(name, locals()) for k in dir(plugin): if k.startswith('cmd_'): k_unsquished = _unsquish_command_name(k) if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = getattr(plugin, k) else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r in dir %r' % (name, d)) finally: if old_module: sys.modules[name] = old_module except ImportError, e: log_error('Unable to load plugin: %r from %r\n%s' % (name, d, e)) finally: sys.path.pop(0) return plugin_cmds def _get_cmd_dict(include_plugins=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v if include_plugins: d.update(_find_plugins()) return d def get_all_cmds(include_plugins=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(include_plugins=include_plugins).iteritems(): yield k,v def get_cmd_class(cmd,include_plugins=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(include_plugins=include_plugins) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() for opt in self.takes_options: if not opt in OPTIONS: raise BzrError("Unknown option '%s' returned by external command %s" % (opt, path)) # TODO: Is there any way to check takes_args is valid here? self.takes_args = pipe.readline().split() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-usage'" % path) pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-help'" % path) def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: optname = name.replace('_','-') value = kargs[name] if OPTIONS.has_key(optname): # it's an option opts.append('--%s' % optname) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import errno br_to = Branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if errno == errno.ENOENT: raise if location is None: location = stored_loc if location is None: raise BzrCommandError("No pull location known or specified.") from branch import find_branch, DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged. Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. To retrieve the branch as of a particular revision, supply the --revision parameter, as in "branch foo/bar -r 5". """ takes_args = ['from_location', 'to_location?'] takes_options = ['revision'] def run(self, from_location, to_location=None, revision=None): import errno from bzrlib.merge import merge from branch import find_branch, DivergedBranches, NoSuchRevision from shutil import rmtree try: br_from = find_branch(from_location) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location.rstrip("/\\")) try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) try: br_to.update_revisions(br_from, stop_revision=revision) except NoSuchRevision: rmtree(to_location) msg = "The branch %s has no revision %d." % (from_location, revision) raise BzrCommandError(msg) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False) from_location = pull_loc(br_from) br_to.controlfile("x-pull", "wb").write(from_location + "\n") def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: raise BzrError("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: raise BzrError("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, exports to a directory (equivalent to --format=dir).""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format'] def run(self, dest, revision=None, format='dir'): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest, format) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] takes_options = ['update'] def run(self, dir='.', update=False): import bzrlib.check bzrlib.check.check(Branch(dir), update=update) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest if selftest(): return 0 else: return 1 class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'update': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) >>> parse_args('log -r 500'.split()) (['log'], {'revision': 500}) >>> parse_args('log -r500:600'.split()) (['log'], {'revision': [500, 600]}) >>> parse_args('log -vr500:600'.split()) (['log'], {'verbose': True, 'revision': [500, 600]}) >>> parse_args('log -rv500:600'.split()) #the r takes an argument Traceback (most recent call last): ... ValueError: invalid literal for int(): v500 """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: raise BzrError('unknown long option %r' % a) else: shortopt = a[1:] if shortopt in SHORT_OPTIONS: # Multi-character options must have a space to delimit # their value optname = SHORT_OPTIONS[shortopt] else: # Single character short options, can be chained, # and have their value appended to their name shortopt = a[1:2] if shortopt not in SHORT_OPTIONS: # We didn't find the multi-character name, and we # didn't find the single char name raise BzrError('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if a[2:]: # There are extra things on this option # see if it is the value, or if it is another # short option optargfn = OPTIONS[optname] if optargfn is None: # This option does not take an argument, so the # next entry is another short option, pack it back # into the list argv.insert(0, '-' + a[2:]) else: # This option takes an argument, so pack it # into the array optarg = a[2:] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? raise BzrError('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: raise BzrError('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: raise BzrError('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] include_plugins=True try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd,include_plugins=include_plugins) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/errors.py data 1848 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " ###################################################################### # exceptions class BzrError(StandardError): pass class BzrCheckError(BzrError): pass class BzrCommandError(BzrError): # Error from malformed user command pass class NotBranchError(BzrError): """Specified path is not in a branch""" pass class BadFileKindError(BzrError): """Specified file is of a kind that cannot be added. (For example a symlink or device file.)""" pass class ForbiddenFileError(BzrError): """Cannot operate on a file because it is a control file.""" pass class LockError(Exception): """All exceptions from the lock/unlock functions should be from this exception class. They will be translated as necessary. The original exception is available as e.original_error """ def __init__(self, e=None): self.original_error = e if e: Exception.__init__(self, e) else: Exception.__init__(self) M 644 inline bzrlib/inventory.py data 19269 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # This should really be an id randomly assigned when the tree is # created, but it's not for now. ROOT_ID = "TREE_ROOT" import sys, os.path, types, re try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from bzrlib.xml import XMLMixin from bzrlib.errors import BzrError, BzrCheckError import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') 'TREE_ROOT' >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID)) >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123')) Traceback (most recent call last): ... BzrError: inventory already contains entry with id {2323} >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123')) >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' TODO: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ # TODO: split InventoryEntry into subclasses for files, # directories, etc etc. text_sha1 = None text_size = None def __init__(self, file_id, name, kind, parent_id, text_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID) >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID) Traceback (most recent call last): BzrCheckError: InventoryEntry name 'src/hello.c' is invalid """ if '/' in name or '\\' in name: raise BzrCheckError('InventoryEntry name %r is invalid' % name) self.file_id = file_id self.name = name self.kind = kind self.text_id = text_id self.parent_id = parent_id if kind == 'directory': self.children = {} elif kind == 'file': pass else: raise BzrError("unhandled entry kind %r" % kind) def sorted_children(self): l = self.children.items() l.sort() return l def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.parent_id, text_id=self.text_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size # note that children are *not* copied; they're pulled across when # others are added return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size != None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1']: v = getattr(self, f) if v != None: e.set(f, v) # to be conservative, we don't externalize the root pointers # for now, leaving them as null in the xml form. in a future # version it will be implied by nested elements. if self.parent_id != ROOT_ID: assert isinstance(self.parent_id, basestring) e.set('parent_id', self.parent_id) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' ## original format inventories don't have a parent_id for ## nodes in the root directory, but it's cleaner to use one ## internally. parent_id = elt.get('parent_id') if parent_id == None: parent_id = ROOT_ID self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __eq__(self, other): if not isinstance(other, InventoryEntry): return NotImplemented return (self.file_id == other.file_id) \ and (self.name == other.name) \ and (self.text_sha1 == other.text_sha1) \ and (self.text_size == other.text_size) \ and (self.text_id == other.text_id) \ and (self.parent_id == other.parent_id) \ and (self.kind == other.kind) def __ne__(self, other): return not (self == other) def __hash__(self): raise ValueError('not hashable') class RootEntry(InventoryEntry): def __init__(self, file_id): self.file_id = file_id self.children = {} self.kind = 'root_directory' self.parent_id = None self.name = '' def __eq__(self, other): if not isinstance(other, RootEntry): return NotImplemented return (self.file_id == other.file_id) \ and (self.children == other.children) class Inventory(XMLMixin): """Inventory of versioned files in a tree. This describes which file_id is present at each point in the tree, and possibly the SHA-1 or other information about the file. Entries can be looked up either by path or by file_id. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted, other than through the Inventory API. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID)) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. The inventory is created with a default root directory, with an id of None. """ self.root = RootEntry(ROOT_ID) self._byid = {self.root.file_id: self.root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, from_dir=None): """Return (path, entry) pairs, in order by name.""" if from_dir == None: assert self.root from_dir = self.root elif isinstance(from_dir, basestring): from_dir = self._byid[from_dir] kids = from_dir.children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(from_dir=ie.file_id): yield os.path.join(name, cn), cie def entries(self): """Return list of (path, ie) for all entries except the root. This may be faster than iter_entries. """ accum = [] def descend(dir_ie, dir_path): kids = dir_ie.children.items() kids.sort() for name, ie in kids: child_path = os.path.join(dir_path, name) accum.append((child_path, ie)) if ie.kind == 'directory': descend(ie, child_path) descend(self.root, '') return accum def directories(self): """Return (path, entry) pairs for all directories, including the root. """ accum = [] def descend(parent_ie, parent_path): accum.append((parent_path, parent_ie)) kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory'] kids.sort() for name, child_ie in kids: child_path = os.path.join(parent_path, name) descend(child_ie, child_path) descend(self.root, '') return accum def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID)) >>> inv['123123'].name 'hello.c' """ try: return self._byid[file_id] except KeyError: if file_id == None: raise BzrError("can't look up file_id None") else: raise BzrError("file_id {%s} not in inventory" % file_id) def get_file_kind(self, file_id): return self._byid[file_id].kind def get_child(self, parent_id, filename): return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self._byid: raise BzrError("inventory already contains entry with id {%s}" % entry.file_id) try: parent = self._byid[entry.parent_id] except KeyError: raise BzrError("parent_id {%s} not in inventory" % entry.parent_id) if parent.children.has_key(entry.name): raise BzrError("%s is already versioned" % appendpath(self.id2path(parent.file_id), entry.name)) self._byid[entry.file_id] = entry parent.children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: raise BzrError("cannot re-add root of inventory") if file_id == None: file_id = bzrlib.branch.gen_file_id(relpath) parent_id = self.path2id(parts[:-1]) assert parent_id != None ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID)) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __eq__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 True """ if not isinstance(other, Inventory): return NotImplemented if len(self._byid) != len(other._byid): # shortcut: obviously not the same return False return self._byid == other._byid def __ne__(self, other): return not (self == other) def __hash__(self): raise ValueError('not hashable') def get_idpath(self, file_id): """Return a list of file_ids for the path to an entry. The list contains one element for each directory followed by the id of the file itself. So the length of the returned list is equal to the depth of the file in the tree, counting the root directory as depth 1. """ p = [] while file_id != None: try: ie = self._byid[file_id] except KeyError: raise BzrError("file_id {%s} not found in inventory" % file_id) p.insert(0, ie.file_id) file_id = ie.parent_id return p def id2path(self, file_id): """Return as a list the path to file_id.""" # get all names, skipping root p = [self[fid].name for fid in self.get_idpath(file_id)[1:]] return os.sep.join(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. Returns None iff the path is not found. """ if isinstance(name, types.StringTypes): name = splitpath(name) mutter("lookup path %r" % name) parent = self.root for f in name: try: cie = parent.children[f] assert cie.name == f assert cie.parent_id == parent.file_id parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): return self._byid.has_key(file_id) def rename(self, file_id, new_parent_id, new_name): """Move a file within the inventory. This can change either the name, or the parent, or both. This does not move the working file.""" if not is_valid_name(new_name): raise BzrError("not an acceptable filename: %r" % new_name) new_parent = self._byid[new_parent_id] if new_name in new_parent.children: raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id))) new_parent_idpath = self.get_idpath(new_parent_id) if file_id in new_parent_idpath: raise BzrError("cannot move directory %r into a subdirectory of itself, %r" % (self.id2path(file_id), self.id2path(new_parent_id))) file_ie = self._byid[file_id] old_parent = self._byid[file_ie.parent_id] # TODO: Don't leave things messed up if this fails del old_parent.children[file_ie.name] new_parent.children[new_name] = file_ie file_ie.name = new_name file_ie.parent_id = new_parent_id _NAME_RE = re.compile(r'^[^/\\]+$') def is_valid_name(name): return bool(_NAME_RE.match(name)) M 644 inline bzrlib/newinventory.py data 4484 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from cElementTree import Element, ElementTree, SubElement def write_inventory(inv, f): el = Element('inventory', {'version': '2'}) el.text = '\n' root = Element('root_directory', {'id': inv.root.file_id}) root.tail = root.text = '\n' el.append(root) def descend(parent_el, ie): kind = ie.kind el = Element(kind, {'name': ie.name, 'id': ie.file_id,}) if kind == 'file': if ie.text_id: el.set('text_id', ie.text_id) if ie.text_sha1: el.set('text_sha1', ie.text_sha1) if ie.text_size != None: el.set('text_size', ('%d' % ie.text_size)) elif kind != 'directory': raise BzrError('unknown InventoryEntry kind %r' % kind) el.tail = '\n' parent_el.append(el) if kind == 'directory': el.text = '\n' # break before having children l = ie.children.items() l.sort() for child_name, child_ie in l: descend(el, child_ie) # walk down through inventory, adding all directories l = inv.root.children.items() l.sort() for entry_name, ie in l: descend(root, ie) ElementTree(el).write(f, 'utf-8') f.write('\n') def escape_attr(text): return text.replace("&", "&") \ .replace("'", "'") \ .replace('"', """) \ .replace("<", "<") \ .replace(">", ">") # This writes out an inventory without building an XML tree first, # just to see if it's faster. Not currently used. def write_slacker_inventory(inv, f): def descend(ie): kind = ie.kind f.write('<%s name="%s" id="%s" ' % (kind, escape_attr(ie.name), escape_attr(ie.file_id))) if kind == 'file': if ie.text_id: f.write('text_id="%s" ' % ie.text_id) if ie.text_sha1: f.write('text_sha1="%s" ' % ie.text_sha1) if ie.text_size != None: f.write('text_size="%d" ' % ie.text_size) f.write('/>\n') elif kind == 'directory': f.write('>\n') l = ie.children.items() l.sort() for child_name, child_ie in l: descend(child_ie) f.write('\n') else: raise BzrError('unknown InventoryEntry kind %r' % kind) f.write('\n') f.write('\n' % escape_attr(inv.root.file_id)) l = inv.root.children.items() l.sort() for entry_name, ie in l: descend(ie) f.write('\n') f.write('\n') def read_new_inventory(f): from inventory import Inventory, InventoryEntry def descend(parent_ie, el): kind = el.tag name = el.get('name') file_id = el.get('id') ie = InventoryEntry(file_id, name, el.tag) parent_ie.children[name] = ie inv._byid[file_id] = ie if kind == 'directory': for child_el in el: descend(ie, child_el) elif kind == 'file': assert len(el) == 0 ie.text_id = el.get('text_id') v = el.get('text_size') ie.text_size = v and int(v) ie.text_sha1 = el.get('text_sha1') else: raise BzrError("unknown inventory entry %r" % kind) inv_el = ElementTree().parse(f) assert inv_el.tag == 'inventory' root_el = inv_el[0] assert root_el.tag == 'root_directory' inv = Inventory() for el in root_el: descend(inv.root, el) M 644 inline bzrlib/osutils.py data 9458 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, types, re, time, errno, sys from stat import S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE from bzrlib.errors import BzrError from bzrlib.trace import mutter import bzrlib def make_readonly(filename): """Make a filename read-only.""" # TODO: probably needs to be fixed for windows mod = os.stat(filename).st_mode mod = mod & 0777555 os.chmod(filename, mod) def make_writable(filename): mod = os.stat(filename).st_mode mod = mod | 0200 os.chmod(filename, mod) _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])') def quotefn(f): """Return shell-quoted filename""" ## We could be a bit more terse by using double-quotes etc f = _QUOTE_RE.sub(r'\\\1', f) if f[0] == '~': f[0:1] = r'\~' return f def file_kind(f): mode = os.lstat(f)[ST_MODE] if S_ISREG(mode): return 'file' elif S_ISDIR(mode): return 'directory' elif S_ISLNK(mode): return 'symlink' else: raise BzrError("can't handle file kind with mode %o of %r" % (mode, f)) def kind_marker(kind): if kind == 'file': return '' elif kind == 'directory': return '/' elif kind == 'symlink': return '@' else: raise BzrError('invalid file kind %r' % kind) def isdir(f): """True if f is an accessible directory.""" try: return S_ISDIR(os.lstat(f)[ST_MODE]) except OSError: return False def isfile(f): """True if f is a regular file.""" try: return S_ISREG(os.lstat(f)[ST_MODE]) except OSError: return False def is_inside(dir, fname): """True if fname is inside dir. """ return os.path.commonprefix([dir, fname]) == dir def is_inside_any(dir_list, fname): """True if fname is inside any of given dirs.""" # quick scan for perfect match if fname in dir_list: return True for dirname in dir_list: if is_inside(dirname, fname): return True else: return False def pumpfile(fromfile, tofile): """Copy contents of one file to another.""" tofile.write(fromfile.read()) def uuid(): """Return a new UUID""" try: return file('/proc/sys/kernel/random/uuid').readline().rstrip('\n') except IOError: return chomp(os.popen('uuidgen').readline()) def sha_file(f): import sha if hasattr(f, 'tell'): assert f.tell() == 0 s = sha.new() BUFSIZE = 128<<10 while True: b = f.read(BUFSIZE) if not b: break s.update(b) return s.hexdigest() def sha_string(f): import sha s = sha.new() s.update(f) return s.hexdigest() def fingerprint_file(f): import sha s = sha.new() b = f.read() s.update(b) size = len(b) return {'size': size, 'sha1': s.hexdigest()} def config_dir(): """Return per-user configuration directory. By default this is ~/.bzr.conf/ TODO: Global option --config-dir to override this. """ return os.path.expanduser("~/.bzr.conf") def _auto_user_id(): """Calculate automatic user identification. Returns (realname, email). Only used when none is set in the environment or the id file. This previously used the FQDN as the default domain, but that can be very slow on machines where DNS is broken. So now we simply use the hostname. """ import socket # XXX: Any good way to get real user name on win32? try: import pwd uid = os.getuid() w = pwd.getpwuid(uid) gecos = w.pw_gecos.decode(bzrlib.user_encoding) username = w.pw_name.decode(bzrlib.user_encoding) comma = gecos.find(',') if comma == -1: realname = gecos else: realname = gecos[:comma] if not realname: realname = username except ImportError: import getpass realname = username = getpass.getuser().decode(bzrlib.user_encoding) return realname, (username + '@' + socket.gethostname()) def _get_user_id(): """Return the full user id from a file or environment variable. TODO: Allow taking this from a file in the branch directory too for per-branch ids.""" v = os.environ.get('BZREMAIL') if v: return v.decode(bzrlib.user_encoding) try: return (open(os.path.join(config_dir(), "email")) .read() .decode(bzrlib.user_encoding) .rstrip("\r\n")) except IOError, e: if e.errno != errno.ENOENT: raise e v = os.environ.get('EMAIL') if v: return v.decode(bzrlib.user_encoding) else: return None def username(): """Return email-style username. Something similar to 'Martin Pool ' TODO: Check it's reasonably well-formed. """ v = _get_user_id() if v: return v name, email = _auto_user_id() if name: return '%s <%s>' % (name, email) else: return email _EMAIL_RE = re.compile(r'[\w+.-]+@[\w+.-]+') def user_email(): """Return just the email component of a username.""" e = _get_user_id() if e: m = _EMAIL_RE.search(e) if not m: raise BzrError("%r doesn't seem to contain a reasonable email address" % e) return m.group(0) return _auto_user_id()[1] def compare_files(a, b): """Returns true if equal in contents""" BUFSIZE = 4096 while True: ai = a.read(BUFSIZE) bi = b.read(BUFSIZE) if ai != bi: return False if ai == '': return True def local_time_offset(t=None): """Return offset of local zone from GMT, either at present or at time t.""" # python2.3 localtime() can't take None if t == None: t = time.time() if time.localtime(t).tm_isdst and time.daylight: return -time.altzone else: return -time.timezone def format_date(t, offset=0, timezone='original'): ## TODO: Perhaps a global option to use either universal or local time? ## Or perhaps just let people set $TZ? assert isinstance(t, float) if timezone == 'utc': tt = time.gmtime(t) offset = 0 elif timezone == 'original': if offset == None: offset = 0 tt = time.gmtime(t + offset) elif timezone == 'local': tt = time.localtime(t) offset = local_time_offset(t) else: raise BzrError("unsupported timezone format %r", ['options are "utc", "original", "local"']) return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt) + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)) def compact_date(when): return time.strftime('%Y%m%d%H%M%S', time.gmtime(when)) def filesize(f): """Return size of given open file.""" return os.fstat(f.fileno())[ST_SIZE] if hasattr(os, 'urandom'): # python 2.4 and later rand_bytes = os.urandom elif sys.platform == 'linux2': rand_bytes = file('/dev/urandom', 'rb').read else: # not well seeded, but better than nothing def rand_bytes(n): import random s = '' while n: s += chr(random.randint(0, 255)) n -= 1 return s ## TODO: We could later have path objects that remember their list ## decomposition (might be too tricksy though.) def splitpath(p): """Turn string into list of parts. >>> splitpath('a') ['a'] >>> splitpath('a/b') ['a', 'b'] >>> splitpath('a/./b') ['a', 'b'] >>> splitpath('a/.b') ['a', '.b'] >>> splitpath('a/../b') Traceback (most recent call last): ... BzrError: sorry, '..' not allowed in path """ assert isinstance(p, types.StringTypes) # split on either delimiter because people might use either on # Windows ps = re.split(r'[\\/]', p) rps = [] for f in ps: if f == '..': raise BzrError("sorry, %r not allowed in path" % f) elif (f == '.') or (f == ''): pass else: rps.append(f) return rps def joinpath(p): assert isinstance(p, list) for f in p: if (f == '..') or (f == None) or (f == ''): raise BzrError("sorry, %r not allowed in path" % f) return os.path.join(*p) def appendpath(p1, p2): if p1 == '': return p2 else: return os.path.join(p1, p2) def extern_command(cmd, ignore_errors = False): mutter('external command: %s' % `cmd`) if os.system(cmd): if not ignore_errors: raise BzrError('command failed') M 644 inline bzrlib/store.py data 6040 # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Stores are the main data-storage mechanism for Bazaar-NG. A store is a simple write-once container indexed by a universally unique ID, which is typically the SHA-1 of the content.""" __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " import os, tempfile, types, osutils, gzip, errno from stat import ST_SIZE from StringIO import StringIO from trace import mutter ###################################################################### # stores class StoreError(Exception): pass class ImmutableStore(object): """Store that holds files indexed by unique names. Files can be added, but not modified once they are in. Typically the hash is used as the name, or something else known to be unique, such as a UUID. >>> st = ImmutableScratchStore() >>> st.add(StringIO('hello'), 'aa') >>> 'aa' in st True >>> 'foo' in st False You are not allowed to add an id that is already present. Entries can be retrieved as files, which may then be read. >>> st.add(StringIO('goodbye'), '123123') >>> st['123123'].read() 'goodbye' TODO: Atomic add by writing to a temporary file and renaming. TODO: Perhaps automatically transform to/from XML in a method? Would just need to tell the constructor what class to use... TODO: Even within a simple disk store like this, we could gzip the files. But since many are less than one disk block, that might not help a lot. """ def __init__(self, basedir): """ImmutableStore constructor.""" self._basedir = basedir def _path(self, id): assert '/' not in id return os.path.join(self._basedir, id) def __repr__(self): return "%s(%r)" % (self.__class__.__name__, self._basedir) def add(self, f, fileid, compressed=True): """Add contents of a file into the store. f -- An open file, or file-like object.""" # FIXME: Only works on smallish files # TODO: Can be optimized by copying at the same time as # computing the sum. mutter("add store entry %r" % (fileid)) if isinstance(f, types.StringTypes): content = f else: content = f.read() p = self._path(fileid) if os.access(p, os.F_OK) or os.access(p + '.gz', os.F_OK): raise BzrError("store %r already contains id %r" % (self._basedir, fileid)) if compressed: f = gzip.GzipFile(p + '.gz', 'wb') os.chmod(p + '.gz', 0444) else: f = file(p, 'wb') os.chmod(p, 0444) f.write(content) f.close() def copy_multi(self, other, ids): """Copy texts for ids from other into self. If an id is present in self, it is skipped. A count of copied ids is returned, which may be less than len(ids). """ from bzrlib.progress import ProgressBar pb = ProgressBar() pb.update('preparing to copy') to_copy = [id for id in ids if id not in self] count = 0 for id in to_copy: count += 1 pb.update('copy', count, len(to_copy)) self.add(other[id], id) assert count == len(to_copy) pb.clear() return count def __contains__(self, fileid): """""" p = self._path(fileid) return (os.access(p, os.R_OK) or os.access(p + '.gz', os.R_OK)) # TODO: Guard against the same thing being stored twice, compressed and uncompresse def __iter__(self): for f in os.listdir(self._basedir): if f[-3:] == '.gz': # TODO: case-insensitive? yield f[:-3] else: yield f def __len__(self): return len(os.listdir(self._basedir)) def __getitem__(self, fileid): """Returns a file reading from a particular entry.""" p = self._path(fileid) try: return gzip.GzipFile(p + '.gz', 'rb') except IOError, e: if e.errno == errno.ENOENT: return file(p, 'rb') else: raise e def total_size(self): """Return (count, bytes) This is the (compressed) size stored on disk, not the size of the content.""" total = 0 count = 0 for fid in self: count += 1 p = self._path(fid) try: total += os.stat(p)[ST_SIZE] except OSError: total += os.stat(p + '.gz')[ST_SIZE] return count, total class ImmutableScratchStore(ImmutableStore): """Self-destructing test subclass of ImmutableStore. The Store only exists for the lifetime of the Python object. Obviously you should not put anything precious in it. """ def __init__(self): ImmutableStore.__init__(self, tempfile.mkdtemp()) def __del__(self): for f in os.listdir(self._basedir): fpath = os.path.join(self._basedir, f) # needed on windows, and maybe some other filesystems os.chmod(fpath, 0600) os.remove(fpath) os.rmdir(self._basedir) mutter("%r destroyed" % self) M 644 inline bzrlib/tree.py data 10349 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tree classes, representing directory at point in time. """ from sets import Set import os.path, os, fnmatch, time from osutils import pumpfile, filesize, quotefn, sha_file, \ joinpath, splitpath, appendpath, isdir, isfile, file_kind, fingerprint_file import errno from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE from bzrlib.inventory import Inventory from bzrlib.trace import mutter, note from bzrlib.errors import BzrError import branch import bzrlib exporters = {} class Tree(object): """Abstract file tree. There are several subclasses: * `WorkingTree` exists as files on disk editable by the user. * `RevisionTree` is a tree as recorded at some point in the past. * `EmptyTree` Trees contain an `Inventory` object, and also know how to retrieve file texts mentioned in the inventory, either from a working directory or from a store. It is possible for trees to contain files that are not described in their inventory or vice versa; for this use `filenames()`. Trees can be compared, etc, regardless of whether they are working trees or versioned trees. """ def has_filename(self, filename): """True if the tree has given filename.""" raise NotImplementedError() def has_id(self, file_id): return self.inventory.has_id(file_id) __contains__ = has_id def __iter__(self): return iter(self.inventory) def id2path(self, file_id): return self.inventory.id2path(file_id) def _get_inventory(self): return self._inventory inventory = property(_get_inventory, doc="Inventory of this Tree") def _check_retrieved(self, ie, f): fp = fingerprint_file(f) f.seek(0) if ie.text_size != None: if ie.text_size != fp['size']: raise BzrError("mismatched size for file %r in %r" % (ie.file_id, self._store), ["inventory expects %d bytes" % ie.text_size, "file is actually %d bytes" % fp['size'], "store is probably damaged/corrupt"]) if ie.text_sha1 != fp['sha1']: raise BzrError("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store), ["inventory expects %s" % ie.text_sha1, "file is actually %s" % fp['sha1'], "store is probably damaged/corrupt"]) def print_file(self, fileid): """Print file with id `fileid` to stdout.""" import sys pumpfile(self.get_file(fileid), sys.stdout) def export(self, dest, format='dir'): """Export this tree.""" try: exporter = exporters[format] except KeyError: raise BzrCommandError("export format %r not supported" % format) exporter(self, dest) class RevisionTree(Tree): """Tree viewing a previous revision. File text can be retrieved from the text store. TODO: Some kind of `__repr__` method, but a good one probably means knowing the branch and revision number, or at least passing a description to the constructor. """ def __init__(self, store, inv): self._store = store self._inventory = inv def get_file(self, file_id): ie = self._inventory[file_id] f = self._store[ie.text_id] mutter(" get fileid{%s} from %r" % (file_id, self)) self._check_retrieved(ie, f) return f def get_file_size(self, file_id): return self._inventory[file_id].text_size def get_file_sha1(self, file_id): ie = self._inventory[file_id] return ie.text_sha1 def has_filename(self, filename): return bool(self.inventory.path2id(filename)) def list_files(self): # The only files returned by this are those from the version for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id class EmptyTree(Tree): def __init__(self): self._inventory = Inventory() def has_filename(self, filename): return False def list_files(self): if False: # just to make it a generator yield None ###################################################################### # diff # TODO: Merge these two functions into a single one that can operate # on either a whole tree or a set of files. # TODO: Return the diff in order by filename, not by category or in # random order. Can probably be done by lock-stepping through the # filenames from both trees. def file_status(filename, old_tree, new_tree): """Return single-letter status, old and new names for a file. The complexity here is in deciding how to represent renames; many complex cases are possible. """ old_inv = old_tree.inventory new_inv = new_tree.inventory new_id = new_inv.path2id(filename) old_id = old_inv.path2id(filename) if not new_id and not old_id: # easy: doesn't exist in either; not versioned at all if new_tree.is_ignored(filename): return 'I', None, None else: return '?', None, None elif new_id: # There is now a file of this name, great. pass else: # There is no longer a file of this name, but we can describe # what happened to the file that used to have # this name. There are two possibilities: either it was # deleted entirely, or renamed. assert old_id if new_inv.has_id(old_id): return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id) else: return 'D', old_inv.id2path(old_id), None # if the file_id is new in this revision, it is added if new_id and not old_inv.has_id(new_id): return 'A' # if there used to be a file of this name, but that ID has now # disappeared, it is deleted if old_id and not new_inv.has_id(old_id): return 'D' return 'wtf?' def find_renames(old_inv, new_inv): for file_id in old_inv: if file_id not in new_inv: continue old_name = old_inv.id2path(file_id) new_name = new_inv.id2path(file_id) if old_name != new_name: yield (old_name, new_name) ###################################################################### # export def dir_exporter(tree, dest): """Export this tree to a new directory. `dest` should not exist, and will be created holding the contents of this tree. TODO: To handle subdirectories we need to create the directories first. :note: If the export fails, the destination directory will be left in a half-assed state. """ os.mkdir(dest) mutter('export version %r' % tree) inv = tree.inventory for dp, ie in inv.iter_entries(): kind = ie.kind fullpath = appendpath(dest, dp) if kind == 'directory': os.mkdir(fullpath) elif kind == 'file': pumpfile(tree.get_file(ie.file_id), file(fullpath, 'wb')) else: raise BzrError("don't know how to export {%s} of kind %r" % (ie.file_id, kind)) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) exporters['dir'] = dir_exporter try: import tarfile except ImportError: pass else: def tar_exporter(tree, dest, compression=None): """Export this tree to a new tar file. `dest` will be created holding the contents of this tree; if it already exists, it will be clobbered, like with "tar -c". """ now = time.time() compression = str(compression or '') try: ball = tarfile.open(dest, 'w:' + compression) except tarfile.CompressionError, e: raise BzrError(str(e)) mutter('export version %r' % tree) inv = tree.inventory for dp, ie in inv.iter_entries(): mutter(" export {%s} kind %s to %s" % (ie.file_id, ie.kind, dest)) item = tarfile.TarInfo(dp) # TODO: would be cool to actually set it to the timestamp of the # revision it was last changed item.mtime = now if ie.kind == 'directory': item.type = tarfile.DIRTYPE fileobj = None item.name += '/' item.size = 0 item.mode = 0755 elif ie.kind == 'file': item.type = tarfile.REGTYPE fileobj = tree.get_file(ie.file_id) item.size = _find_file_size(fileobj) item.mode = 0644 else: raise BzrError("don't know how to export {%s} of kind %r" % (ie.file_id, ie.kind)) ball.addfile(item, fileobj) ball.close() exporters['tar'] = tar_exporter def tgz_exporter(tree, dest): tar_exporter(tree, dest, compression='gz') exporters['tgz'] = tgz_exporter def tbz_exporter(tree, dest): tar_exporter(tree, dest, compression='bz2') exporters['tbz2'] = tbz_exporter def _find_file_size(fileobj): offset = fileobj.tell() try: fileobj.seek(0, 2) size = fileobj.tell() except TypeError: # gzip doesn't accept second argument to seek() fileobj.seek(0) size = 0 while True: nread = len(fileobj.read()) if nread == 0: break size += nread fileobj.seek(offset) return size commit refs/heads/master mark :695 committer Martin Pool 1118993644 +1000 data 54 - don't display progress bars on really dumb terminals from :694 M 644 inline bzrlib/progress.py data 7813 # Copyright (C) 2005 Aaron Bentley # Copyright (C) 2005 Canonical # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ Simple text-mode progress indicator. Everyone loves ascii art! To display an indicator, create a ProgressBar object. Call it, passing Progress objects indicating the current state. When done, call clear(). Progress is suppressed when output is not sent to a terminal, so as not to clutter log files. """ # TODO: remove functions in favour of keeping everything in one class # TODO: should be a global option e.g. --silent that disables progress # indicators, preferably without needing to adjust all code that # potentially calls them. # TODO: Perhaps don't write updates faster than a certain rate, say # 5/second. import sys import time def _width(): """Return estimated terminal width. TODO: Do something smart on Windows? TODO: Is there anything that gets a better update when the window is resized while the program is running? """ import os try: return int(os.environ['COLUMNS']) except (IndexError, KeyError, ValueError): return 80 def _supports_progress(f): if not hasattr(f, 'isatty'): return False if not f.isatty(): return False import os if os.environ.get('TERM') == 'dumb': # e.g. emacs compile window return False return True class ProgressBar(object): """Progress bar display object. Several options are available to control the display. These can be passed as parameters to the constructor or assigned at any time: show_pct Show percentage complete. show_spinner Show rotating baton. This ticks over on every update even if the values don't change. show_eta Show predicted time-to-completion. show_bar Show bar graph. show_count Show numerical counts. The output file should be in line-buffered or unbuffered mode. """ SPIN_CHARS = r'/-\|' MIN_PAUSE = 0.1 # seconds start_time = None last_update = None def __init__(self, to_file=sys.stderr, show_pct=False, show_spinner=False, show_eta=True, show_bar=True, show_count=True): object.__init__(self) self.to_file = to_file self.suppressed = not _supports_progress(self.to_file) self.spin_pos = 0 self.last_msg = None self.last_cnt = None self.last_total = None self.show_pct = show_pct self.show_spinner = show_spinner self.show_eta = show_eta self.show_bar = show_bar self.show_count = show_count def tick(self): self.update(self.last_msg, self.last_cnt, self.last_total) def update(self, msg, current_cnt=None, total_cnt=None): """Update and redraw progress bar.""" if self.suppressed: return # save these for the tick() function self.last_msg = msg self.last_cnt = current_cnt self.last_total = total_cnt now = time.time() if self.start_time is None: self.start_time = now else: interval = now - self.last_update if interval > 0 and interval < self.MIN_PAUSE: return self.last_update = now width = _width() if total_cnt: assert current_cnt <= total_cnt if current_cnt: assert current_cnt >= 0 if self.show_eta and self.start_time and total_cnt: eta = get_eta(self.start_time, current_cnt, total_cnt) eta_str = " " + str_tdelta(eta) else: eta_str = "" if self.show_spinner: spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' ' else: spin_str = '' # always update this; it's also used for the bar self.spin_pos += 1 if self.show_pct and total_cnt and current_cnt: pct = 100.0 * current_cnt / total_cnt pct_str = ' (%5.1f%%)' % pct else: pct_str = '' if not self.show_count: count_str = '' elif current_cnt is None: count_str = '' elif total_cnt is None: count_str = ' %i' % (current_cnt) else: # make both fields the same size t = '%i' % (total_cnt) c = '%*i' % (len(t), current_cnt) count_str = ' ' + c + '/' + t if self.show_bar: # progress bar, if present, soaks up all remaining space cols = width - 1 - len(msg) - len(spin_str) - len(pct_str) \ - len(eta_str) - len(count_str) - 3 if total_cnt: # number of markers highlighted in bar markers = int(round(float(cols) * current_cnt / total_cnt)) bar_str = '[' + ('=' * markers).ljust(cols) + '] ' elif False: # don't know total, so can't show completion. # so just show an expanded spinning thingy m = self.spin_pos % cols ms = (' ' * m + '*').ljust(cols) bar_str = '[' + ms + '] ' else: bar_str = '' else: bar_str = '' m = spin_str + bar_str + msg + count_str + pct_str + eta_str assert len(m) < width self.to_file.write('\r' + m.ljust(width - 1)) #self.to_file.flush() def clear(self): if self.suppressed: return self.to_file.write('\r%s\r' % (' ' * (_width() - 1))) #self.to_file.flush() def str_tdelta(delt): if delt is None: return "-:--:--" delt = int(round(delt)) return '%d:%02d:%02d' % (delt/3600, (delt/60) % 60, delt % 60) def get_eta(start_time, current, total, enough_samples=3): if start_time is None: return None if not total: return None if current < enough_samples: return None if current > total: return None # wtf? elapsed = time.time() - start_time if elapsed < 2.0: # not enough time to estimate return None total_duration = float(elapsed) * float(total) / float(current) assert total_duration >= elapsed return total_duration - elapsed def run_tests(): import doctest result = doctest.testmod() if result[1] > 0: if result[0] == 0: print "All tests passed" else: print "No tests to run" def demo(): from time import sleep pb = ProgressBar(show_pct=True, show_bar=True, show_spinner=False) for i in range(100): pb.update('Elephanten', i, 99) sleep(0.1) sleep(2) pb.clear() sleep(1) print 'done!' if __name__ == "__main__": demo() commit refs/heads/master mark :696 committer Martin Pool 1119001001 +1000 data 144 - Refactor revision deserialization code - Allow for multiple parents on a revision (not used yet) - Smarter default values for Revision objects from :695 M 644 inline bzrlib/revision.py data 5158 # (C) 2005 Canonical # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from xml import XMLMixin try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from errors import BzrError class RevisionReference: """ Reference to a stored revision. Includes the revision_id and revision_sha1. """ revision_id = None revision_sha1 = None def __init__(self, revision_id, revision_sha1): if revision_id == None \ or isinstance(revision_id, basestring): self.revision_id = revision_id else: raise ValueError('bad revision_id %r' % revision_id) if revision_sha1 != None: if isinstance(revision_sha1, basestring) \ and len(revision_sha1) == 40: self.revision_sha1 = revision_sha1 else: raise ValueError('bad revision_sha1 %r' % revision_sha1) class Revision(XMLMixin): """Single revision on a branch. Revisions may know their revision_hash, but only once they've been written out. This is not stored because you cannot write the hash into the file it describes. TODO: Perhaps make precursor be a child element, not an attribute? parents List of parent revisions; """ inventory_id = None inventory_sha1 = None revision_id = None timestamp = None message = None timezone = None committer = None def __init__(self, **args): self.__dict__.update(args) self.parents = [] def _get_precursor(self): ##from warnings import warn ##warn("Revision.precursor is deprecated") if self.parents: return self.parents[0].revision_id else: return None def _get_precursor_sha1(self): ##from warnings import warn ##warn("Revision.precursor_sha1 is deprecated") if self.parents: return self.parents[0].revision_sha1 else: return None precursor = property(_get_precursor) precursor_sha1 = property(_get_precursor_sha1) def __repr__(self): return "" % self.revision_id def to_element(self): root = Element('revision', committer = self.committer, timestamp = '%.9f' % self.timestamp, revision_id = self.revision_id, inventory_id = self.inventory_id, inventory_sha1 = self.inventory_sha1, ) if self.timezone: root.set('timezone', str(self.timezone)) if self.precursor: root.set('precursor', self.precursor) if self.precursor_sha1: root.set('precursor_sha1', self.precursor_sha1) root.text = '\n' msg = SubElement(root, 'message') msg.text = self.message msg.tail = '\n' return root def from_element(cls, elt): return unpack_revision(elt) from_element = classmethod(from_element) def unpack_revision(elt): """Convert XML element into Revision object.""" # is deprecated... if elt.tag not in ('revision', 'changeset'): raise BzrError("unexpected tag in revision file: %r" % elt) rev = Revision(committer = elt.get('committer'), timestamp = float(elt.get('timestamp')), revision_id = elt.get('revision_id'), inventory_id = elt.get('inventory_id'), inventory_sha1 = elt.get('inventory_sha1') ) precursor = elt.get('precursor') precursor_sha1 = elt.get('precursor_sha1') pelts = elt.find('parents') if precursor: # revisions written prior to 0.0.5 have a single precursor # give as an attribute rev_ref = RevisionReference(precursor, precursor_sha1) rev.parents.append(rev_ref) elif pelts: for p in pelts: assert p.tag == 'revisionref', \ "bad parent node tag %r" % p.tag rev_ref = RevisionReference(p.get('revision_id'), p.get('revision_sha1')) rev.parents.append(rev_ref) v = elt.get('timezone') rev.timezone = v and int(v) rev.message = elt.findtext('message') # text of return rev commit refs/heads/master mark :697 committer Martin Pool 1119003009 +1000 data 228 - write out parent list for new revisions - don't assign to the Revision.precursor property in commit but rather store parents This is done in a way that should support old clients; the precursor property is still present from :696 M 644 inline bzrlib/commit.py data 10518 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def commit(branch, message, timestamp=None, timezone=None, committer=None, verbose=True, specific_files=None, rev_id=None): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. specific_files If true, commit only those files. rev_id If set, use this as the new revision id. Useful for test or import commands that need to tightly control what revisions are assigned. If you duplicate a revision id that exists elsewhere it is your own fault. If null (default), a time/random revision id is generated. """ import time, tempfile from bzrlib.osutils import local_time_offset, username from bzrlib.branch import gen_file_id from bzrlib.errors import BzrError from bzrlib.revision import Revision, RevisionReference from bzrlib.trace import mutter, note branch.lock_write() try: # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_tree = branch.working_tree() work_inv = work_tree.inventory basis = branch.basis_tree() basis_inv = basis.inventory if verbose: note('looking for changes...') missing_ids, new_inv = _gather_commit(branch, work_tree, work_inv, basis_inv, specific_files, verbose) for file_id in missing_ids: # Any files that have been deleted are now removed from the # working inventory. Files that were not selected for commit # are left as they were in the working inventory and ommitted # from the revision inventory. # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itbranch. if work_inv.has_id(file_id): del work_inv[file_id] if rev_id is None: rev_id = _gen_revision_id(time.time()) inv_id = rev_id inv_tmp = tempfile.TemporaryFile() new_inv.write_xml(inv_tmp) inv_tmp.seek(0) branch.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) # We could also just sha hash the inv_tmp file # however, in the case that branch.inventory_store.add() # ever actually does anything special inv_sha1 = branch.get_inventory_sha1(inv_id) precursor_id = branch.last_patch() if precursor_id: precursor_sha1 = branch.get_revision_sha1(precursor_id) else: precursor_sha1 = None parent = RevisionReference(precursor_id, precursor_sha1) branch._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, message = message, inventory_id=inv_id, inventory_sha1=inv_sha1, revision_id=rev_id) rev.parents = [parent] rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) branch.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (branch.revno() + 1)) branch.append_revision(rev_id) if verbose: note("commited r%d" % branch.revno()) finally: branch.unlock() def _gen_revision_id(when): """Return new revision-id.""" from binascii import hexlify from osutils import rand_bytes, compact_date, user_email s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def _gather_commit(branch, work_tree, work_inv, basis_inv, specific_files, verbose): """Build inventory preparatory to commit. This adds any changed files into the text store, and sets their test-id, sha and size in the returned inventory appropriately. missing_ids Modified to hold a list of files that have been deleted from the working directory; these should be removed from the working inventory. """ from bzrlib.inventory import Inventory from osutils import isdir, isfile, sha_string, quotefn, \ local_time_offset, username, kind_marker, is_inside_any from branch import gen_file_id from errors import BzrError from revision import Revision from bzrlib.trace import mutter, note inv = Inventory() missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). p = branch.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if specific_files and not is_inside_any(specific_files, path): if basis_inv.has_id(file_id): # carry over with previous state inv.add(basis_inv[file_id].copy()) else: # omit this from committed inventory pass continue if not work_tree.has_id(file_id): if verbose: print('deleted %s%s' % (path, kind_marker(entry.kind))) mutter(" file is missing, removing from inventory") missing_ids.append(file_id) continue # this is present in the new inventory; may be new, modified or # unchanged. old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] entry = entry.copy() inv.add(entry) if old_ie: old_kind = old_ie.kind if old_kind != entry.kind: raise BzrError("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): raise BzrError("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): raise BzrError("%s is entered as file but is not a file" % quotefn(p)) new_sha1 = work_tree.get_file_sha1(file_id) if (old_ie and old_ie.text_sha1 == new_sha1): ## assert content == basis.get_file(file_id).read() entry.text_id = old_ie.text_id entry.text_sha1 = new_sha1 entry.text_size = old_ie.text_size mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: content = file(p, 'rb').read() # calculate the sha again, just in case the file contents # changed since we updated the cache entry.text_sha1 = sha_string(content) entry.text_size = len(content) entry.text_id = gen_file_id(entry.name) branch.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: marked = path + kind_marker(entry.kind) if not old_ie: print 'added', marked elif old_ie == entry: pass # unchanged elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): print 'modified', marked else: print 'renamed', marked return missing_ids, inv M 644 inline bzrlib/revision.py data 5798 # (C) 2005 Canonical # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from xml import XMLMixin try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from errors import BzrError class RevisionReference: """ Reference to a stored revision. Includes the revision_id and revision_sha1. """ revision_id = None revision_sha1 = None def __init__(self, revision_id, revision_sha1): if revision_id == None \ or isinstance(revision_id, basestring): self.revision_id = revision_id else: raise ValueError('bad revision_id %r' % revision_id) if revision_sha1 != None: if isinstance(revision_sha1, basestring) \ and len(revision_sha1) == 40: self.revision_sha1 = revision_sha1 else: raise ValueError('bad revision_sha1 %r' % revision_sha1) class Revision(XMLMixin): """Single revision on a branch. Revisions may know their revision_hash, but only once they've been written out. This is not stored because you cannot write the hash into the file it describes. After bzr 0.0.5 revisions are allowed to have multiple parents. To support old clients this is written out in a slightly redundant form: the first parent as the predecessor. This will eventually be dropped. parents List of parent revisions; """ inventory_id = None inventory_sha1 = None revision_id = None timestamp = None message = None timezone = None committer = None def __init__(self, **args): self.__dict__.update(args) self.parents = [] def _get_precursor(self): ##from warnings import warn ##warn("Revision.precursor is deprecated") if self.parents: return self.parents[0].revision_id else: return None def _get_precursor_sha1(self): ##from warnings import warn ##warn("Revision.precursor_sha1 is deprecated") if self.parents: return self.parents[0].revision_sha1 else: return None def _fail(self): raise Exception("can't assign to precursor anymore") precursor = property(_get_precursor, _fail, _fail) precursor_sha1 = property(_get_precursor_sha1, _fail, _fail) def __repr__(self): return "" % self.revision_id def to_element(self): root = Element('revision', committer = self.committer, timestamp = '%.9f' % self.timestamp, revision_id = self.revision_id, inventory_id = self.inventory_id, inventory_sha1 = self.inventory_sha1, ) if self.timezone: root.set('timezone', str(self.timezone)) if self.precursor: root.set('precursor', self.precursor) if self.precursor_sha1: root.set('precursor_sha1', self.precursor_sha1) root.text = '\n' msg = SubElement(root, 'message') msg.text = self.message msg.tail = '\n' if self.parents: pelts = SubElement(root, 'parents') for rr in self.parents: assert isinstance(rr, RevisionReference) p = SubElement(pelts, 'revision_ref') p.set('revision_id', rr.revision_id) if rr.revision_sha1: p.set('revision_sha1', rr.revision_sha1) return root def from_element(cls, elt): return unpack_revision(elt) from_element = classmethod(from_element) def unpack_revision(elt): """Convert XML element into Revision object.""" # is deprecated... if elt.tag not in ('revision', 'changeset'): raise BzrError("unexpected tag in revision file: %r" % elt) rev = Revision(committer = elt.get('committer'), timestamp = float(elt.get('timestamp')), revision_id = elt.get('revision_id'), inventory_id = elt.get('inventory_id'), inventory_sha1 = elt.get('inventory_sha1') ) precursor = elt.get('precursor') precursor_sha1 = elt.get('precursor_sha1') pelts = elt.find('parents') if precursor: # revisions written prior to 0.0.5 have a single precursor # give as an attribute rev_ref = RevisionReference(precursor, precursor_sha1) rev.parents.append(rev_ref) elif pelts: for p in pelts: assert p.tag == 'revision_ref', \ "bad parent node tag %r" % p.tag rev_ref = RevisionReference(p.get('revision_id'), p.get('revision_sha1')) rev.parents.append(rev_ref) v = elt.get('timezone') rev.timezone = v and int(v) rev.message = elt.findtext('message') # text of return rev commit refs/heads/master mark :698 committer Martin Pool 1119231162 +1000 data 40 - bzr branch shouldn't say "0 conflicts" from :697 M 644 inline bzrlib/commands.py data 49505 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _find_plugins(): """Find all python files which are plugins, and load their commands to add to the list of "all commands" The environment variable BZRPATH is considered a delimited set of paths to look through. Each entry is searched for *.py files. If a directory is found, it is also searched, but they are not searched recursively. This allows you to revctl the plugins. Inside the plugin should be a series of cmd_* function, which inherit from the bzrlib.commands.Command class. """ bzrpath = os.environ.get('BZRPLUGINPATH', '') plugin_cmds = {} if not bzrpath: return plugin_cmds _platform_extensions = { 'win32':'.pyd', 'cygwin':'.dll', 'darwin':'.dylib', 'linux2':'.so' } if _platform_extensions.has_key(sys.platform): platform_extension = _platform_extensions[sys.platform] else: platform_extension = None for d in bzrpath.split(os.pathsep): plugin_names = {} # This should really be a set rather than a dict for f in os.listdir(d): if f.endswith('.py'): f = f[:-3] elif f.endswith('.pyc') or f.endswith('.pyo'): f = f[:-4] elif platform_extension and f.endswith(platform_extension): f = f[:-len(platform_extension)] if f.endswidth('module'): f = f[:-len('module')] else: continue if not plugin_names.has_key(f): plugin_names[f] = True plugin_names = plugin_names.keys() plugin_names.sort() try: sys.path.insert(0, d) for name in plugin_names: try: old_module = None try: if sys.modules.has_key(name): old_module = sys.modules[name] del sys.modules[name] plugin = __import__(name, locals()) for k in dir(plugin): if k.startswith('cmd_'): k_unsquished = _unsquish_command_name(k) if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = getattr(plugin, k) else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r in dir %r' % (name, d)) finally: if old_module: sys.modules[name] = old_module except ImportError, e: log_error('Unable to load plugin: %r from %r\n%s' % (name, d, e)) finally: sys.path.pop(0) return plugin_cmds def _get_cmd_dict(include_plugins=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v if include_plugins: d.update(_find_plugins()) return d def get_all_cmds(include_plugins=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(include_plugins=include_plugins).iteritems(): yield k,v def get_cmd_class(cmd,include_plugins=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(include_plugins=include_plugins) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() for opt in self.takes_options: if not opt in OPTIONS: raise BzrError("Unknown option '%s' returned by external command %s" % (opt, path)) # TODO: Is there any way to check takes_args is valid here? self.takes_args = pipe.readline().split() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-usage'" % path) pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-help'" % path) def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: optname = name.replace('_','-') value = kargs[name] if OPTIONS.has_key(optname): # it's an option opts.append('--%s' % optname) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import errno br_to = Branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if errno == errno.ENOENT: raise if location is None: location = stored_loc if location is None: raise BzrCommandError("No pull location known or specified.") from branch import find_branch, DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged. Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. To retrieve the branch as of a particular revision, supply the --revision parameter, as in "branch foo/bar -r 5". """ takes_args = ['from_location', 'to_location?'] takes_options = ['revision'] def run(self, from_location, to_location=None, revision=None): import errno from bzrlib.merge import merge from branch import find_branch, DivergedBranches, NoSuchRevision from shutil import rmtree try: br_from = find_branch(from_location) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location.rstrip("/\\")) try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) try: br_to.update_revisions(br_from, stop_revision=revision) except NoSuchRevision: rmtree(to_location) msg = "The branch %s has no revision %d." % (from_location, revision) raise BzrCommandError(msg) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False, ignore_zero=True) from_location = pull_loc(br_from) br_to.controlfile("x-pull", "wb").write(from_location + "\n") def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: raise BzrError("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: raise BzrError("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, exports to a directory (equivalent to --format=dir).""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format'] def run(self, dest, revision=None, format='dir'): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest, format) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] takes_options = ['update'] def run(self, dir='.', update=False): import bzrlib.check bzrlib.check.check(Branch(dir), update=update) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest if selftest(): return 0 else: return 1 class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'update': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) >>> parse_args('log -r 500'.split()) (['log'], {'revision': 500}) >>> parse_args('log -r500:600'.split()) (['log'], {'revision': [500, 600]}) >>> parse_args('log -vr500:600'.split()) (['log'], {'verbose': True, 'revision': [500, 600]}) >>> parse_args('log -rv500:600'.split()) #the r takes an argument Traceback (most recent call last): ... ValueError: invalid literal for int(): v500 """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: raise BzrError('unknown long option %r' % a) else: shortopt = a[1:] if shortopt in SHORT_OPTIONS: # Multi-character options must have a space to delimit # their value optname = SHORT_OPTIONS[shortopt] else: # Single character short options, can be chained, # and have their value appended to their name shortopt = a[1:2] if shortopt not in SHORT_OPTIONS: # We didn't find the multi-character name, and we # didn't find the single char name raise BzrError('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if a[2:]: # There are extra things on this option # see if it is the value, or if it is another # short option optargfn = OPTIONS[optname] if optargfn is None: # This option does not take an argument, so the # next entry is another short option, pack it back # into the list argv.insert(0, '-' + a[2:]) else: # This option takes an argument, so pack it # into the array optarg = a[2:] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? raise BzrError('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: raise BzrError('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: raise BzrError('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] include_plugins=True try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd,include_plugins=include_plugins) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :699 committer Martin Pool 1119232261 +1000 data 40 - simpleminded patchwork client in shell from :698 M 644 inline contrib/pwk data 481 #! /bin/sh -pe # take patches from patchwork into bzr # authentication must be in ~/.netrc PWK_ROOT='http://patchwork.ozlabs.org/bazaar-ng' PWK_AUTH_ROOT='https://patchwork.ozlabs.org/bazaar-ng' usage() { echo "usage: pwk cat PATCH-ID" >&2 } catpatch() { curl --get -d id=$1 $PWK_ROOT/patchcontent } if [ $# -ne 2 ] then usage exit 1 fi case "$1" in cat) catpatch $2 ;; try) catpatch $2 | patch -p0 --dry-run ;; *) usage exit 1 esac commit refs/heads/master mark :700 committer Martin Pool 1119232749 +1000 data 3 doc from :699 M 644 inline bzrlib/revision.py data 5826 # (C) 2005 Canonical # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from xml import XMLMixin try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from errors import BzrError class RevisionReference: """ Reference to a stored revision. Includes the revision_id and revision_sha1. """ revision_id = None revision_sha1 = None def __init__(self, revision_id, revision_sha1): if revision_id == None \ or isinstance(revision_id, basestring): self.revision_id = revision_id else: raise ValueError('bad revision_id %r' % revision_id) if revision_sha1 != None: if isinstance(revision_sha1, basestring) \ and len(revision_sha1) == 40: self.revision_sha1 = revision_sha1 else: raise ValueError('bad revision_sha1 %r' % revision_sha1) class Revision(XMLMixin): """Single revision on a branch. Revisions may know their revision_hash, but only once they've been written out. This is not stored because you cannot write the hash into the file it describes. After bzr 0.0.5 revisions are allowed to have multiple parents. To support old clients this is written out in a slightly redundant form: the first parent as the predecessor. This will eventually be dropped. parents List of parent revisions, each is a RevisionReference. """ inventory_id = None inventory_sha1 = None revision_id = None timestamp = None message = None timezone = None committer = None def __init__(self, **args): self.__dict__.update(args) self.parents = [] def _get_precursor(self): ##from warnings import warn ##warn("Revision.precursor is deprecated") if self.parents: return self.parents[0].revision_id else: return None def _get_precursor_sha1(self): ##from warnings import warn ##warn("Revision.precursor_sha1 is deprecated") if self.parents: return self.parents[0].revision_sha1 else: return None def _fail(self): raise Exception("can't assign to precursor anymore") precursor = property(_get_precursor, _fail, _fail) precursor_sha1 = property(_get_precursor_sha1, _fail, _fail) def __repr__(self): return "" % self.revision_id def to_element(self): root = Element('revision', committer = self.committer, timestamp = '%.9f' % self.timestamp, revision_id = self.revision_id, inventory_id = self.inventory_id, inventory_sha1 = self.inventory_sha1, ) if self.timezone: root.set('timezone', str(self.timezone)) if self.precursor: root.set('precursor', self.precursor) if self.precursor_sha1: root.set('precursor_sha1', self.precursor_sha1) root.text = '\n' msg = SubElement(root, 'message') msg.text = self.message msg.tail = '\n' if self.parents: pelts = SubElement(root, 'parents') for rr in self.parents: assert isinstance(rr, RevisionReference) p = SubElement(pelts, 'revision_ref') p.set('revision_id', rr.revision_id) if rr.revision_sha1: p.set('revision_sha1', rr.revision_sha1) return root def from_element(cls, elt): return unpack_revision(elt) from_element = classmethod(from_element) def unpack_revision(elt): """Convert XML element into Revision object.""" # is deprecated... if elt.tag not in ('revision', 'changeset'): raise BzrError("unexpected tag in revision file: %r" % elt) rev = Revision(committer = elt.get('committer'), timestamp = float(elt.get('timestamp')), revision_id = elt.get('revision_id'), inventory_id = elt.get('inventory_id'), inventory_sha1 = elt.get('inventory_sha1') ) precursor = elt.get('precursor') precursor_sha1 = elt.get('precursor_sha1') pelts = elt.find('parents') if precursor: # revisions written prior to 0.0.5 have a single precursor # give as an attribute rev_ref = RevisionReference(precursor, precursor_sha1) rev.parents.append(rev_ref) elif pelts: for p in pelts: assert p.tag == 'revision_ref', \ "bad parent node tag %r" % p.tag rev_ref = RevisionReference(p.get('revision_id'), p.get('revision_sha1')) rev.parents.append(rev_ref) v = elt.get('timezone') rev.timezone = v and int(v) rev.message = elt.findtext('message') # text of return rev commit refs/heads/master mark :701 committer Martin Pool 1119232766 +1000 data 18 - delete dead code from :700 M 644 inline bzrlib/log.py data 6938 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Code to show logs of changes. Various flavors of log can be produced: * for one file, or the whole tree, and (not done yet) for files in a given directory * in "verbose" mode with a description of what changed from one version to the next * with file-ids and revision-ids shown * from last to first or (not anymore) from first to last; the default is "reversed" because it shows the likely most relevant and interesting information first * (not yet) in XML format """ from trace import mutter def find_touching_revisions(branch, file_id): """Yield a description of revisions which affect the file_id. Each returned element is (revno, revision_id, description) This is the list of revisions where the file is either added, modified, renamed or deleted. TODO: Perhaps some way to limit this to only particular revisions, or to traverse a non-mainline set of revisions? """ last_ie = None last_path = None revno = 1 for revision_id in branch.revision_history(): this_inv = branch.get_revision_inventory(revision_id) if file_id in this_inv: this_ie = this_inv[file_id] this_path = this_inv.id2path(file_id) else: this_ie = this_path = None # now we know how it was last time, and how it is in this revision. # are those two states effectively the same or not? if not this_ie and not last_ie: # not present in either pass elif this_ie and not last_ie: yield revno, revision_id, "added " + this_path elif not this_ie and last_ie: # deleted here yield revno, revision_id, "deleted " + last_path elif this_path != last_path: yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path)) elif (this_ie.text_size != last_ie.text_size or this_ie.text_sha1 != last_ie.text_sha1): yield revno, revision_id, "modified " + this_path last_ie = this_ie last_path = this_path revno += 1 def show_log(branch, specific_fileid=None, show_timezone='original', verbose=False, show_ids=False, to_file=None, direction='reverse', start_revision=None, end_revision=None): """Write out human-readable log of commits to this branch. specific_fileid If true, list only the commits affecting the specified file, rather than all commits. show_timezone 'original' (committer's timezone), 'utc' (universal time), or 'local' (local user's timezone) verbose If true show added/changed/deleted/renamed files. show_ids If true, show revision and file ids. to_file File to send log to; by default stdout. direction 'reverse' (default) is latest to earliest; 'forward' is earliest to latest. start_revision If not None, only show revisions >= start_revision end_revision If not None, only show revisions <= end_revision """ from osutils import format_date from errors import BzrCheckError from textui import show_status if specific_fileid: mutter('get log for file_id %r' % specific_fileid) if to_file == None: import sys to_file = sys.stdout which_revs = branch.enum_history(direction) if not (verbose or specific_fileid): # no need to know what changed between revisions with_deltas = deltas_for_log_dummy(branch, which_revs) elif direction == 'reverse': with_deltas = deltas_for_log_reverse(branch, which_revs) else: raise NotImplementedError("sorry, verbose forward logs not done yet") for revno, rev, delta in with_deltas: if specific_fileid: if not delta.touches_file_id(specific_fileid): continue if start_revision is not None and revno < start_revision: continue if end_revision is not None and revno > end_revision: continue if not verbose: # although we calculated it, throw it away without display delta = None show_one_log(revno, rev, delta, show_ids, to_file, show_timezone) def deltas_for_log_dummy(branch, which_revs): for revno, revision_id in which_revs: yield revno, branch.get_revision(revision_id), None def deltas_for_log_reverse(branch, which_revs): """Compute deltas for display in reverse log. Given a sequence of (revno, revision_id) pairs, return (revno, rev, delta). The delta is from the given revision to the next one in the sequence, which makes sense if the log is being displayed from newest to oldest. """ from tree import EmptyTree from diff import compare_trees last_revno = last_revision_id = last_tree = None for revno, revision_id in which_revs: this_tree = branch.revision_tree(revision_id) this_revision = branch.get_revision(revision_id) if last_revno: yield last_revno, last_revision, compare_trees(this_tree, last_tree, False) last_revno = revno last_revision = this_revision last_tree = this_tree if last_revno: this_tree = EmptyTree() yield last_revno, last_revision, compare_trees(this_tree, last_tree, False) def show_one_log(revno, rev, delta, show_ids, to_file, show_timezone): from osutils import format_date print >>to_file, '-' * 60 print >>to_file, 'revno:', revno if show_ids: print >>to_file, 'revision-id:', rev.revision_id print >>to_file, 'committer:', rev.committer print >>to_file, 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, show_timezone)) print >>to_file, 'message:' if not rev.message: print >>to_file, ' (no message)' else: for l in rev.message.split('\n'): print >>to_file, ' ' + l if delta != None: delta.show(to_file, show_ids) commit refs/heads/master mark :702 committer Martin Pool 1119233012 +1000 data 17 todo: bzr upgrade from :701 M 644 inline TODO data 13414 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * ``bzr log DIR`` should give changes to any files within DIR. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. Perhaps ~/.bzr/http-cache. Baz has a fairly simple cache under ~/.arch-cache, containing revision information encoded almost as a bunch of archives. Perhaps we could simply store full paths. * Maybe also store directories in the statcache so that we can quickly identify that they still exist. * Diff should show timestamps; for files from the working directory we can use the file itself; for files from a revision we should use the commit time of the revision. * Perhaps split command infrastructure from the actual command definitions. * Cleaner support for negative boolean options like --no-recurse. * Statcache should possibly map all file paths to / separators * quotefn doubles all backslashes on Windows; this is probably not the best thing to do. What would be a better way to safely represent filenames? Perhaps we could doublequote things containing spaces, on the principle that filenames containing quotes are unlikely? Nice for humans; less good for machine parsing. * Patches should probably use only forward slashes, even on Windows, otherwise Unix patch can't apply them. (?) * Branch.update_revisions() inefficiently fetches revisions from the remote server twice; once to find out what text and inventory they need and then again to actually get the thing. This is a bit inefficient. One complicating factor here is that we don't really want to have revisions present in the revision-store until all their constituent parts are also stored. The basic problem is that RemoteBranch.get_revision() and similar methods return object, but what we really want is the raw XML, which can be popped into our own store. That needs to be refactored. * ``bzr status FOO`` where foo is ignored should say so. Medium things ------------- * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. We should be able to just get the id for the selected files, look up their location and diff just those files. No need to traverse the entire inventories. * ``bzr status DIR`` or ``bzr diff DIR`` should report on all changes under that directory. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from ElementTree to an object when it is read in, but rather wait until the program actually wants to know about that node. * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. - Selected-file commit - Impossible selected-file commit: adding things in non-versioned directories, crossing renames, etc. * Write a reproducible benchmark, perhaps importing various kernel versions. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Paranoid mode where we never trust SHA-1 matches. * Don't commit if there are no changes unless forced. * --dry-run mode for commit? (Or maybe just run with check-command=false?) * Generally, be a bit more verbose unless --silent is specified. * Function that finds all changes to files under a given directory; perhaps log should use this if a directory is given. * XML attributes might have trouble with filenames containing \n and \r. Do we really want to support this? I think perhaps not. * Remember execute bits, so that exports will work OK. * Unify smart_add and plain Branch.add(); perhaps smart_add should just build a list of files to add and pass that to the regular add function. * Function to list a directory, saying in which revision each file was last modified. Useful for web and gui interfaces, and slow to compute one file at a time. * unittest is standard, but the results are kind of ugly; would be nice to make it cleaner. * Check locking is correct during merge-related operations. * Perhaps attempts to get locks should timeout after some period of time, or at least display a progress message. * Split out upgrade functionality from check command into a separate ``bzr upgrade``. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. Possibly this should be done by splitting the commit function into several parts (under a single interface). It is already rather large. Decomposition: - find tree modifications and prepare in-memory inventory - export that inventory to a temporary directory - run the test in that temporary directory - if that succeeded, continue to actually finish the commit What should be done with the text of modified files while this is underway? I don't think we want to count on holding them in memory and we can't trust the working files to stay in one place so I suppose we need to move them into the text store, or otherwise into a temporary directory. If the commit does not actually complete, we would rather the content was not left behind in the stores. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :703 committer Martin Pool 1119235808 +1000 data 120 - split out a new 'bzr upgrade' command separate from (but based on) 'bzr check', so that the code in each is simpler from :702 M 644 inline bzrlib/upgrade.py data 6093 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def upgrade(branch): """ Upgrade branch to current format. This causes objects to be rewritten into the current format. If they change, their SHA-1 will of course change, which might break any later signatures, or backreferences that check the SHA-1. """ import sys from bzrlib.trace import mutter from bzrlib.errors import BzrCheckError from bzrlib.progress import ProgressBar branch.lock_write() try: pb = ProgressBar(show_spinner=True) last_ptr = None checked_revs = {} history = branch.revision_history() revno = 0 revcount = len(history) updated_revisions = [] # Set to True in the case that the previous revision entry # was updated, since this will affect future revision entries updated_previous_revision = False for rid in history: revno += 1 pb.update('upgrading revision', revno, revcount) mutter(' revision {%s}' % rid) rev = branch.get_revision(rid) if rev.revision_id != rid: raise BzrCheckError('wrong internal revision id in revision {%s}' % rid) if rev.precursor != last_ptr: raise BzrCheckError('mismatched precursor in revision {%s}' % rid) last_ptr = rid if rid in checked_revs: raise BzrCheckError('repeated revision {%s}' % rid) checked_revs[rid] = True ## TODO: Check all the required fields are present on the revision. updated = False if rev.inventory_sha1: #mutter(' checking inventory hash {%s}' % rev.inventory_sha1) inv_sha1 = branch.get_inventory_sha1(rev.inventory_id) if inv_sha1 != rev.inventory_sha1: raise BzrCheckError('Inventory sha1 hash doesn\'t match' ' value in revision {%s}' % rid) else: inv_sha1 = branch.get_inventory_sha1(rev.inventory_id) rev.inventory_sha1 = inv_sha1 updated = True if rev.precursor: if rev.precursor_sha1: precursor_sha1 = branch.get_revision_sha1(rev.precursor) if updated_previous_revision: # we don't expect the hashes to match, because # we had to modify the previous revision_history entry. rev.precursor_sha1 = precursor_sha1 updated = True else: #mutter(' checking precursor hash {%s}' % rev.precursor_sha1) if rev.precursor_sha1 != precursor_sha1: raise BzrCheckError('Precursor sha1 hash doesn\'t match' ' value in revision {%s}' % rid) else: precursor_sha1 = branch.get_revision_sha1(rev.precursor) rev.precursor_sha1 = precursor_sha1 updated = True if updated: updated_previous_revision = True # We had to update this revision entries hashes # Now we need to write out a new value # This is a little bit invasive, since we are *rewriting* a # revision entry. I'm not supremely happy about it, but # there should be *some* way of making old entries have # the full meta information. import tempfile, os, errno rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) tmpfd, tmp_path = tempfile.mkstemp(prefix=rid, suffix='.gz', dir=branch.controlfilename('revision-store')) os.close(tmpfd) def special_rename(p1, p2): if sys.platform == 'win32': try: os.remove(p2) except OSError, e: if e.errno != errno.ENOENT: raise os.rename(p1, p2) try: # TODO: We may need to handle the case where the old revision # entry was not compressed (and thus did not end with .gz) # Remove the old revision entry out of the way rev_path = branch.controlfilename(['revision-store', rid+'.gz']) special_rename(rev_path, tmp_path) branch.revision_store.add(rev_tmp, rid) # Add the new one os.remove(tmp_path) # Remove the old name mutter(' Updated revision entry {%s}' % rid) except: # On any exception, restore the old entry special_rename(tmp_path, rev_path) raise rev_tmp.close() updated_revisions.append(rid) else: updated_previous_revision = False finally: branch.unlock() pb.clear() if updated_revisions: print '%d revisions updated to current format' % len(updated_revisions) M 644 inline NEWS data 8451 DEVELOPMENT HEAD CHANGES: * New ``bzr upgrade`` command to upgrade the format of a branch, replacing ``bzr check --update``. bzr-0.0.5 2005-06-15 CHANGES: * ``bzr`` with no command now shows help rather than giving an error. Suggested by Michael Ellerman. * ``bzr status`` output format changed, because svn-style output doesn't really match the model of bzr. Now files are grouped by status and can be shown with their IDs. ``bzr status --all`` shows all versioned files and unknown files but not ignored files. * ``bzr log`` runs from most-recent to least-recent, the reverse of the previous order. The previous behaviour can be obtained with the ``--forward`` option. * ``bzr inventory`` by default shows only filenames, and also ids if ``--show-ids`` is given, in which case the id is the second field. ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New ``bzr ignore PATTERN`` command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.sw[nop] .git .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. File may be in a branch other than the working directory. * ``bzr log`` and ``bzr root`` can be given an http URL instead of a filename. * Commands can now be defined by external programs or scripts in a directory on $BZRPATH. * New "stat cache" avoids reading the contents of files if they haven't changed since the previous time. * If the Python interpreter is too old, try to find a better one or give an error. Based on a patch from Fredrik Lundh. * New optional parameter ``bzr info [BRANCH]``. * New form ``bzr commit SELECTED`` to commit only selected files. * New form ``bzr log -r FROM:TO`` shows changes in selected range; contributed by John A Meinel. * New option ``bzr diff --diff-options 'OPTS'`` allows passing options through to an external GNU diff. * New option ``bzr add --no-recurse`` to add a directory but not their contents. * ``bzr --version`` now shows more information if bzr is being run from a branch. BUG FIXES: * Fixed diff format so that added and removed files will be handled properly by patch. Fix from Lalo Martins. * Various fixes for files whose names contain spaces or other metacharacters. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. * testbzr requires python2.4, but can be used to test bzr running under a different version. * Tests added for many other changes in this release. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. * New developer commands ``added``, ``modified``. PORTABILITY: * Cope on Windows on python2.3 by using the weaker random seed. 2.4 is now only recommended. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/check.py data 5505 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def check(branch): """Run consistency checks on a branch. """ from bzrlib.trace import mutter from bzrlib.errors import BzrCheckError from bzrlib.osutils import fingerprint_file from bzrlib.progress import ProgressBar branch.lock_read() try: pb = ProgressBar(show_spinner=True) last_ptr = None checked_revs = {} missing_inventory_sha_cnt = 0 history = branch.revision_history() revno = 0 revcount = len(history) checked_texts = {} for rev_id in history: revno += 1 pb.update('checking revision', revno, revcount) mutter(' revision {%s}' % rev_id) rev = branch.get_revision(rev_id) if rev.revision_id != rev_id: raise BzrCheckError('wrong internal revision id in revision {%s}' % rev_id) if rev.precursor != last_ptr: raise BzrCheckError('mismatched precursor in revision {%s}' % rev_id) last_ptr = rev_id if rev_id in checked_revs: raise BzrCheckError('repeated revision {%s}' % rev_id) checked_revs[rev_id] = True ## TODO: Check all the required fields are present on the revision. if rev.inventory_sha1: inv_sha1 = branch.get_inventory_sha1(rev.inventory_id) if inv_sha1 != rev.inventory_sha1: raise BzrCheckError('Inventory sha1 hash doesn\'t match' ' value in revision {%s}' % rev_id) else: missing_inventory_sha_cnt += 1 mutter("no inventory_sha1 on revision {%s}" % rev_id) if rev.precursor: if rev.precursor_sha1: precursor_sha1 = branch.get_revision_sha1(rev.precursor) #mutter(' checking precursor hash {%s}' % rev.precursor_sha1) if rev.precursor_sha1 != precursor_sha1: raise BzrCheckError('Precursor sha1 hash doesn\'t match' ' value in revision {%s}' % rev_id) inv = branch.get_inventory(rev.inventory_id) seen_ids = {} seen_names = {} ## p('revision %d/%d file ids' % (revno, revcount)) for file_id in inv: if file_id in seen_ids: raise BzrCheckError('duplicated file_id {%s} ' 'in inventory for revision {%s}' % (file_id, rev_id)) seen_ids[file_id] = True i = 0 for file_id in inv: i += 1 if i & 31 == 0: pb.tick() ie = inv[file_id] if ie.parent_id != None: if ie.parent_id not in seen_ids: raise BzrCheckError('missing parent {%s} in inventory for revision {%s}' % (ie.parent_id, rev_id)) if ie.kind == 'file': if ie.text_id in checked_texts: fp = checked_texts[ie.text_id] else: if not ie.text_id in branch.text_store: raise BzrCheckError('text {%s} not in text_store' % ie.text_id) tf = branch.text_store[ie.text_id] fp = fingerprint_file(tf) checked_texts[ie.text_id] = fp if ie.text_size != fp['size']: raise BzrCheckError('text {%s} wrong size' % ie.text_id) if ie.text_sha1 != fp['sha1']: raise BzrCheckError('text {%s} wrong sha1' % ie.text_id) elif ie.kind == 'directory': if ie.text_sha1 != None or ie.text_size != None or ie.text_id != None: raise BzrCheckError('directory {%s} has text in revision {%s}' % (file_id, rev_id)) pb.tick() for path, ie in inv.iter_entries(): if path in seen_names: raise BzrCheckError('duplicated path %s ' 'in inventory for revision {%s}' % (path, rev_id)) seen_names[path] = True finally: branch.unlock() pb.clear() print 'checked %d revisions, %d file texts' % (revcount, len(checked_texts)) if missing_inventory_sha_cnt: print '%d revisions are missing inventory_sha1' % missing_inventory_sha_cnt print ' (use "bzr upgrade" to fix them)' M 644 inline bzrlib/commands.py data 49748 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _find_plugins(): """Find all python files which are plugins, and load their commands to add to the list of "all commands" The environment variable BZRPATH is considered a delimited set of paths to look through. Each entry is searched for *.py files. If a directory is found, it is also searched, but they are not searched recursively. This allows you to revctl the plugins. Inside the plugin should be a series of cmd_* function, which inherit from the bzrlib.commands.Command class. """ bzrpath = os.environ.get('BZRPLUGINPATH', '') plugin_cmds = {} if not bzrpath: return plugin_cmds _platform_extensions = { 'win32':'.pyd', 'cygwin':'.dll', 'darwin':'.dylib', 'linux2':'.so' } if _platform_extensions.has_key(sys.platform): platform_extension = _platform_extensions[sys.platform] else: platform_extension = None for d in bzrpath.split(os.pathsep): plugin_names = {} # This should really be a set rather than a dict for f in os.listdir(d): if f.endswith('.py'): f = f[:-3] elif f.endswith('.pyc') or f.endswith('.pyo'): f = f[:-4] elif platform_extension and f.endswith(platform_extension): f = f[:-len(platform_extension)] if f.endswidth('module'): f = f[:-len('module')] else: continue if not plugin_names.has_key(f): plugin_names[f] = True plugin_names = plugin_names.keys() plugin_names.sort() try: sys.path.insert(0, d) for name in plugin_names: try: old_module = None try: if sys.modules.has_key(name): old_module = sys.modules[name] del sys.modules[name] plugin = __import__(name, locals()) for k in dir(plugin): if k.startswith('cmd_'): k_unsquished = _unsquish_command_name(k) if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = getattr(plugin, k) else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r in dir %r' % (name, d)) finally: if old_module: sys.modules[name] = old_module except ImportError, e: log_error('Unable to load plugin: %r from %r\n%s' % (name, d, e)) finally: sys.path.pop(0) return plugin_cmds def _get_cmd_dict(include_plugins=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v if include_plugins: d.update(_find_plugins()) return d def get_all_cmds(include_plugins=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(include_plugins=include_plugins).iteritems(): yield k,v def get_cmd_class(cmd,include_plugins=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(include_plugins=include_plugins) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() for opt in self.takes_options: if not opt in OPTIONS: raise BzrError("Unknown option '%s' returned by external command %s" % (opt, path)) # TODO: Is there any way to check takes_args is valid here? self.takes_args = pipe.readline().split() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-usage'" % path) pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-help'" % path) def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: optname = name.replace('_','-') value = kargs[name] if OPTIONS.has_key(optname): # it's an option opts.append('--%s' % optname) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import errno br_to = Branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if errno == errno.ENOENT: raise if location is None: location = stored_loc if location is None: raise BzrCommandError("No pull location known or specified.") from branch import find_branch, DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged. Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. To retrieve the branch as of a particular revision, supply the --revision parameter, as in "branch foo/bar -r 5". """ takes_args = ['from_location', 'to_location?'] takes_options = ['revision'] def run(self, from_location, to_location=None, revision=None): import errno from bzrlib.merge import merge from branch import find_branch, DivergedBranches, NoSuchRevision from shutil import rmtree try: br_from = find_branch(from_location) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location.rstrip("/\\")) try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) try: br_to.update_revisions(br_from, stop_revision=revision) except NoSuchRevision: rmtree(to_location) msg = "The branch %s has no revision %d." % (from_location, revision) raise BzrCommandError(msg) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False, ignore_zero=True) from_location = pull_loc(br_from) br_to.controlfile("x-pull", "wb").write(from_location + "\n") def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: raise BzrError("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: raise BzrError("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, exports to a directory (equivalent to --format=dir).""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format'] def run(self, dest, revision=None, format='dir'): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest, format) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_upgrade(Command): """Upgrade branch storage to current format. This should normally be used only after the check command tells you to run it. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.upgrade import upgrade upgrade(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest if selftest(): return 0 else: return 1 class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'update': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) >>> parse_args('log -r 500'.split()) (['log'], {'revision': 500}) >>> parse_args('log -r500:600'.split()) (['log'], {'revision': [500, 600]}) >>> parse_args('log -vr500:600'.split()) (['log'], {'verbose': True, 'revision': [500, 600]}) >>> parse_args('log -rv500:600'.split()) #the r takes an argument Traceback (most recent call last): ... ValueError: invalid literal for int(): v500 """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: raise BzrError('unknown long option %r' % a) else: shortopt = a[1:] if shortopt in SHORT_OPTIONS: # Multi-character options must have a space to delimit # their value optname = SHORT_OPTIONS[shortopt] else: # Single character short options, can be chained, # and have their value appended to their name shortopt = a[1:2] if shortopt not in SHORT_OPTIONS: # We didn't find the multi-character name, and we # didn't find the single char name raise BzrError('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if a[2:]: # There are extra things on this option # see if it is the value, or if it is another # short option optargfn = OPTIONS[optname] if optargfn is None: # This option does not take an argument, so the # next entry is another short option, pack it back # into the list argv.insert(0, '-' + a[2:]) else: # This option takes an argument, so pack it # into the array optarg = a[2:] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? raise BzrError('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: raise BzrError('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: raise BzrError('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] include_plugins=True try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd,include_plugins=include_plugins) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :704 committer Martin Pool 1119235910 +1000 data 37 - more whitespace in XML for Revision from :703 M 644 inline bzrlib/revision.py data 5899 # (C) 2005 Canonical # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from xml import XMLMixin try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from errors import BzrError class RevisionReference: """ Reference to a stored revision. Includes the revision_id and revision_sha1. """ revision_id = None revision_sha1 = None def __init__(self, revision_id, revision_sha1): if revision_id == None \ or isinstance(revision_id, basestring): self.revision_id = revision_id else: raise ValueError('bad revision_id %r' % revision_id) if revision_sha1 != None: if isinstance(revision_sha1, basestring) \ and len(revision_sha1) == 40: self.revision_sha1 = revision_sha1 else: raise ValueError('bad revision_sha1 %r' % revision_sha1) class Revision(XMLMixin): """Single revision on a branch. Revisions may know their revision_hash, but only once they've been written out. This is not stored because you cannot write the hash into the file it describes. After bzr 0.0.5 revisions are allowed to have multiple parents. To support old clients this is written out in a slightly redundant form: the first parent as the predecessor. This will eventually be dropped. parents List of parent revisions, each is a RevisionReference. """ inventory_id = None inventory_sha1 = None revision_id = None timestamp = None message = None timezone = None committer = None def __init__(self, **args): self.__dict__.update(args) self.parents = [] def _get_precursor(self): ##from warnings import warn ##warn("Revision.precursor is deprecated") if self.parents: return self.parents[0].revision_id else: return None def _get_precursor_sha1(self): ##from warnings import warn ##warn("Revision.precursor_sha1 is deprecated") if self.parents: return self.parents[0].revision_sha1 else: return None def _fail(self): raise Exception("can't assign to precursor anymore") precursor = property(_get_precursor, _fail, _fail) precursor_sha1 = property(_get_precursor_sha1, _fail, _fail) def __repr__(self): return "" % self.revision_id def to_element(self): root = Element('revision', committer = self.committer, timestamp = '%.9f' % self.timestamp, revision_id = self.revision_id, inventory_id = self.inventory_id, inventory_sha1 = self.inventory_sha1, ) if self.timezone: root.set('timezone', str(self.timezone)) if self.precursor: root.set('precursor', self.precursor) if self.precursor_sha1: root.set('precursor_sha1', self.precursor_sha1) root.text = '\n' msg = SubElement(root, 'message') msg.text = self.message msg.tail = '\n' if self.parents: pelts = SubElement(root, 'parents') pelts.tail = pelts.text = '\n' for rr in self.parents: assert isinstance(rr, RevisionReference) p = SubElement(pelts, 'revision_ref') p.tail = '\n' p.set('revision_id', rr.revision_id) if rr.revision_sha1: p.set('revision_sha1', rr.revision_sha1) return root def from_element(cls, elt): return unpack_revision(elt) from_element = classmethod(from_element) def unpack_revision(elt): """Convert XML element into Revision object.""" # is deprecated... if elt.tag not in ('revision', 'changeset'): raise BzrError("unexpected tag in revision file: %r" % elt) rev = Revision(committer = elt.get('committer'), timestamp = float(elt.get('timestamp')), revision_id = elt.get('revision_id'), inventory_id = elt.get('inventory_id'), inventory_sha1 = elt.get('inventory_sha1') ) precursor = elt.get('precursor') precursor_sha1 = elt.get('precursor_sha1') pelts = elt.find('parents') if precursor: # revisions written prior to 0.0.5 have a single precursor # give as an attribute rev_ref = RevisionReference(precursor, precursor_sha1) rev.parents.append(rev_ref) elif pelts: for p in pelts: assert p.tag == 'revision_ref', \ "bad parent node tag %r" % p.tag rev_ref = RevisionReference(p.get('revision_id'), p.get('revision_sha1')) rev.parents.append(rev_ref) v = elt.get('timezone') rev.timezone = v and int(v) rev.message = elt.findtext('message') # text of return rev commit refs/heads/master mark :705 committer Martin Pool 1119238113 +1000 data 45 - updated check for revision parents and sha1 from :704 M 644 inline bzrlib/check.py data 7161 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def check(branch): """Run consistency checks on a branch. TODO: Also check non-mailine revisions mentioned as parents. """ from bzrlib.trace import mutter from bzrlib.errors import BzrCheckError from bzrlib.osutils import fingerprint_file from bzrlib.progress import ProgressBar branch.lock_read() try: pb = ProgressBar(show_spinner=True) last_rev_id = None missing_inventory_sha_cnt = 0 missing_revision_sha_cnt = 0 history = branch.revision_history() revno = 0 revcount = len(history) # for all texts checked, text_id -> sha1 checked_texts = {} for rev_id in history: revno += 1 pb.update('checking revision', revno, revcount) mutter(' revision {%s}' % rev_id) rev = branch.get_revision(rev_id) if rev.revision_id != rev_id: raise BzrCheckError('wrong internal revision id in revision {%s}' % rev_id) # check the previous history entry is a parent of this entry if rev.parents: if last_rev_id is None: raise BzrCheckError("revision {%s} has %d parents, but is the " "start of the branch" % (rev_id, len(rev.parents))) for prr in rev.parents: if prr.revision_id == last_rev_id: break else: raise BzrCheckError("previous revision {%s} not listed among " "parents of {%s}" % (last_rev_id, rev_id)) for prr in rev.parents: if prr.revision_sha1 is None: missing_revision_sha_cnt += 1 continue prid = prr.revision_id actual_sha = branch.get_revision_sha1(prid) if prr.revision_sha1 != actual_sha: raise BzrCheckError("mismatched revision sha1 for " "parent {%s} of {%s}: %s vs %s" % (prid, rev_id, prr.revision_sha1, actual_sha)) elif last_rev_id: raise BzrCheckError("revision {%s} has no parents listed but preceded " "by {%s}" % (rev_id, last_rev_id)) ## TODO: Check all the required fields are present on the revision. if rev.inventory_sha1: inv_sha1 = branch.get_inventory_sha1(rev.inventory_id) if inv_sha1 != rev.inventory_sha1: raise BzrCheckError('Inventory sha1 hash doesn\'t match' ' value in revision {%s}' % rev_id) else: missing_inventory_sha_cnt += 1 mutter("no inventory_sha1 on revision {%s}" % rev_id) if rev.precursor: if rev.precursor_sha1: precursor_sha1 = branch.get_revision_sha1(rev.precursor) #mutter(' checking precursor hash {%s}' % rev.precursor_sha1) if rev.precursor_sha1 != precursor_sha1: raise BzrCheckError('Precursor sha1 hash doesn\'t match' ' value in revision {%s}' % rev_id) inv = branch.get_inventory(rev.inventory_id) seen_ids = {} seen_names = {} ## p('revision %d/%d file ids' % (revno, revcount)) for file_id in inv: if file_id in seen_ids: raise BzrCheckError('duplicated file_id {%s} ' 'in inventory for revision {%s}' % (file_id, rev_id)) seen_ids[file_id] = True i = 0 for file_id in inv: i += 1 if i & 31 == 0: pb.tick() ie = inv[file_id] if ie.parent_id != None: if ie.parent_id not in seen_ids: raise BzrCheckError('missing parent {%s} in inventory for revision {%s}' % (ie.parent_id, rev_id)) if ie.kind == 'file': if ie.text_id in checked_texts: fp = checked_texts[ie.text_id] else: if not ie.text_id in branch.text_store: raise BzrCheckError('text {%s} not in text_store' % ie.text_id) tf = branch.text_store[ie.text_id] fp = fingerprint_file(tf) checked_texts[ie.text_id] = fp if ie.text_size != fp['size']: raise BzrCheckError('text {%s} wrong size' % ie.text_id) if ie.text_sha1 != fp['sha1']: raise BzrCheckError('text {%s} wrong sha1' % ie.text_id) elif ie.kind == 'directory': if ie.text_sha1 != None or ie.text_size != None or ie.text_id != None: raise BzrCheckError('directory {%s} has text in revision {%s}' % (file_id, rev_id)) pb.tick() for path, ie in inv.iter_entries(): if path in seen_names: raise BzrCheckError('duplicated path %s ' 'in inventory for revision {%s}' % (path, rev_id)) seen_names[path] = True last_rev_id = rev_id finally: branch.unlock() pb.clear() print 'checked %d revisions, %d file texts' % (revcount, len(checked_texts)) if missing_inventory_sha_cnt: print '%d revisions are missing inventory_sha1' % missing_inventory_sha_cnt if missing_revision_sha_cnt: print '%d parent links are missing revision_sha1' % missing_revision_sha_cnt if (missing_inventory_sha_cnt or missing_revision_sha_cnt): print ' (use "bzr upgrade" to fix them)' commit refs/heads/master mark :706 committer Martin Pool 1119238161 +1000 data 34 - remove redundant precursor check from :705 M 644 inline bzrlib/check.py data 6723 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def check(branch): """Run consistency checks on a branch. TODO: Also check non-mailine revisions mentioned as parents. """ from bzrlib.trace import mutter from bzrlib.errors import BzrCheckError from bzrlib.osutils import fingerprint_file from bzrlib.progress import ProgressBar branch.lock_read() try: pb = ProgressBar(show_spinner=True) last_rev_id = None missing_inventory_sha_cnt = 0 missing_revision_sha_cnt = 0 history = branch.revision_history() revno = 0 revcount = len(history) # for all texts checked, text_id -> sha1 checked_texts = {} for rev_id in history: revno += 1 pb.update('checking revision', revno, revcount) mutter(' revision {%s}' % rev_id) rev = branch.get_revision(rev_id) if rev.revision_id != rev_id: raise BzrCheckError('wrong internal revision id in revision {%s}' % rev_id) # check the previous history entry is a parent of this entry if rev.parents: if last_rev_id is None: raise BzrCheckError("revision {%s} has %d parents, but is the " "start of the branch" % (rev_id, len(rev.parents))) for prr in rev.parents: if prr.revision_id == last_rev_id: break else: raise BzrCheckError("previous revision {%s} not listed among " "parents of {%s}" % (last_rev_id, rev_id)) for prr in rev.parents: if prr.revision_sha1 is None: missing_revision_sha_cnt += 1 continue prid = prr.revision_id actual_sha = branch.get_revision_sha1(prid) if prr.revision_sha1 != actual_sha: raise BzrCheckError("mismatched revision sha1 for " "parent {%s} of {%s}: %s vs %s" % (prid, rev_id, prr.revision_sha1, actual_sha)) elif last_rev_id: raise BzrCheckError("revision {%s} has no parents listed but preceded " "by {%s}" % (rev_id, last_rev_id)) ## TODO: Check all the required fields are present on the revision. if rev.inventory_sha1: inv_sha1 = branch.get_inventory_sha1(rev.inventory_id) if inv_sha1 != rev.inventory_sha1: raise BzrCheckError('Inventory sha1 hash doesn\'t match' ' value in revision {%s}' % rev_id) else: missing_inventory_sha_cnt += 1 mutter("no inventory_sha1 on revision {%s}" % rev_id) inv = branch.get_inventory(rev.inventory_id) seen_ids = {} seen_names = {} ## p('revision %d/%d file ids' % (revno, revcount)) for file_id in inv: if file_id in seen_ids: raise BzrCheckError('duplicated file_id {%s} ' 'in inventory for revision {%s}' % (file_id, rev_id)) seen_ids[file_id] = True i = 0 for file_id in inv: i += 1 if i & 31 == 0: pb.tick() ie = inv[file_id] if ie.parent_id != None: if ie.parent_id not in seen_ids: raise BzrCheckError('missing parent {%s} in inventory for revision {%s}' % (ie.parent_id, rev_id)) if ie.kind == 'file': if ie.text_id in checked_texts: fp = checked_texts[ie.text_id] else: if not ie.text_id in branch.text_store: raise BzrCheckError('text {%s} not in text_store' % ie.text_id) tf = branch.text_store[ie.text_id] fp = fingerprint_file(tf) checked_texts[ie.text_id] = fp if ie.text_size != fp['size']: raise BzrCheckError('text {%s} wrong size' % ie.text_id) if ie.text_sha1 != fp['sha1']: raise BzrCheckError('text {%s} wrong sha1' % ie.text_id) elif ie.kind == 'directory': if ie.text_sha1 != None or ie.text_size != None or ie.text_id != None: raise BzrCheckError('directory {%s} has text in revision {%s}' % (file_id, rev_id)) pb.tick() for path, ie in inv.iter_entries(): if path in seen_names: raise BzrCheckError('duplicated path %s ' 'in inventory for revision {%s}' % (path, rev_id)) seen_names[path] = True last_rev_id = rev_id finally: branch.unlock() pb.clear() print 'checked %d revisions, %d file texts' % (revcount, len(checked_texts)) if missing_inventory_sha_cnt: print '%d revisions are missing inventory_sha1' % missing_inventory_sha_cnt if missing_revision_sha_cnt: print '%d parent links are missing revision_sha1' % missing_revision_sha_cnt if (missing_inventory_sha_cnt or missing_revision_sha_cnt): print ' (use "bzr upgrade" to fix them)' commit refs/heads/master mark :707 committer Martin Pool 1119238230 +1000 data 55 - give warning if the precursor attributes are accessed from :706 M 644 inline bzrlib/revision.py data 5891 # (C) 2005 Canonical # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from xml import XMLMixin try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from errors import BzrError class RevisionReference: """ Reference to a stored revision. Includes the revision_id and revision_sha1. """ revision_id = None revision_sha1 = None def __init__(self, revision_id, revision_sha1): if revision_id == None \ or isinstance(revision_id, basestring): self.revision_id = revision_id else: raise ValueError('bad revision_id %r' % revision_id) if revision_sha1 != None: if isinstance(revision_sha1, basestring) \ and len(revision_sha1) == 40: self.revision_sha1 = revision_sha1 else: raise ValueError('bad revision_sha1 %r' % revision_sha1) class Revision(XMLMixin): """Single revision on a branch. Revisions may know their revision_hash, but only once they've been written out. This is not stored because you cannot write the hash into the file it describes. After bzr 0.0.5 revisions are allowed to have multiple parents. To support old clients this is written out in a slightly redundant form: the first parent as the predecessor. This will eventually be dropped. parents List of parent revisions, each is a RevisionReference. """ inventory_id = None inventory_sha1 = None revision_id = None timestamp = None message = None timezone = None committer = None def __init__(self, **args): self.__dict__.update(args) self.parents = [] def _get_precursor(self): from warnings import warn warn("Revision.precursor is deprecated") if self.parents: return self.parents[0].revision_id else: return None def _get_precursor_sha1(self): from warnings import warn warn("Revision.precursor_sha1 is deprecated") if self.parents: return self.parents[0].revision_sha1 else: return None def _fail(self): raise Exception("can't assign to precursor anymore") precursor = property(_get_precursor, _fail, _fail) precursor_sha1 = property(_get_precursor_sha1, _fail, _fail) def __repr__(self): return "" % self.revision_id def to_element(self): root = Element('revision', committer = self.committer, timestamp = '%.9f' % self.timestamp, revision_id = self.revision_id, inventory_id = self.inventory_id, inventory_sha1 = self.inventory_sha1, ) if self.timezone: root.set('timezone', str(self.timezone)) if self.precursor: root.set('precursor', self.precursor) if self.precursor_sha1: root.set('precursor_sha1', self.precursor_sha1) root.text = '\n' msg = SubElement(root, 'message') msg.text = self.message msg.tail = '\n' if self.parents: pelts = SubElement(root, 'parents') pelts.tail = pelts.text = '\n' for rr in self.parents: assert isinstance(rr, RevisionReference) p = SubElement(pelts, 'revision_ref') p.tail = '\n' p.set('revision_id', rr.revision_id) if rr.revision_sha1: p.set('revision_sha1', rr.revision_sha1) return root def from_element(cls, elt): return unpack_revision(elt) from_element = classmethod(from_element) def unpack_revision(elt): """Convert XML element into Revision object.""" # is deprecated... if elt.tag not in ('revision', 'changeset'): raise BzrError("unexpected tag in revision file: %r" % elt) rev = Revision(committer = elt.get('committer'), timestamp = float(elt.get('timestamp')), revision_id = elt.get('revision_id'), inventory_id = elt.get('inventory_id'), inventory_sha1 = elt.get('inventory_sha1') ) precursor = elt.get('precursor') precursor_sha1 = elt.get('precursor_sha1') pelts = elt.find('parents') if precursor: # revisions written prior to 0.0.5 have a single precursor # give as an attribute rev_ref = RevisionReference(precursor, precursor_sha1) rev.parents.append(rev_ref) elif pelts: for p in pelts: assert p.tag == 'revision_ref', \ "bad parent node tag %r" % p.tag rev_ref = RevisionReference(p.get('revision_id'), p.get('revision_sha1')) rev.parents.append(rev_ref) v = elt.get('timezone') rev.timezone = v and int(v) rev.message = elt.findtext('message') # text of return rev commit refs/heads/master mark :708 committer Martin Pool 1119238269 +1000 data 16 - better warning from :707 M 644 inline bzrlib/revision.py data 5919 # (C) 2005 Canonical # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from xml import XMLMixin try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from errors import BzrError class RevisionReference: """ Reference to a stored revision. Includes the revision_id and revision_sha1. """ revision_id = None revision_sha1 = None def __init__(self, revision_id, revision_sha1): if revision_id == None \ or isinstance(revision_id, basestring): self.revision_id = revision_id else: raise ValueError('bad revision_id %r' % revision_id) if revision_sha1 != None: if isinstance(revision_sha1, basestring) \ and len(revision_sha1) == 40: self.revision_sha1 = revision_sha1 else: raise ValueError('bad revision_sha1 %r' % revision_sha1) class Revision(XMLMixin): """Single revision on a branch. Revisions may know their revision_hash, but only once they've been written out. This is not stored because you cannot write the hash into the file it describes. After bzr 0.0.5 revisions are allowed to have multiple parents. To support old clients this is written out in a slightly redundant form: the first parent as the predecessor. This will eventually be dropped. parents List of parent revisions, each is a RevisionReference. """ inventory_id = None inventory_sha1 = None revision_id = None timestamp = None message = None timezone = None committer = None def __init__(self, **args): self.__dict__.update(args) self.parents = [] def _get_precursor(self): from warnings import warn warn("Revision.precursor is deprecated", stacklevel=2) if self.parents: return self.parents[0].revision_id else: return None def _get_precursor_sha1(self): from warnings import warn warn("Revision.precursor_sha1 is deprecated", stacklevel=2) if self.parents: return self.parents[0].revision_sha1 else: return None def _fail(self): raise Exception("can't assign to precursor anymore") precursor = property(_get_precursor, _fail, _fail) precursor_sha1 = property(_get_precursor_sha1, _fail, _fail) def __repr__(self): return "" % self.revision_id def to_element(self): root = Element('revision', committer = self.committer, timestamp = '%.9f' % self.timestamp, revision_id = self.revision_id, inventory_id = self.inventory_id, inventory_sha1 = self.inventory_sha1, ) if self.timezone: root.set('timezone', str(self.timezone)) if self.precursor: root.set('precursor', self.precursor) if self.precursor_sha1: root.set('precursor_sha1', self.precursor_sha1) root.text = '\n' msg = SubElement(root, 'message') msg.text = self.message msg.tail = '\n' if self.parents: pelts = SubElement(root, 'parents') pelts.tail = pelts.text = '\n' for rr in self.parents: assert isinstance(rr, RevisionReference) p = SubElement(pelts, 'revision_ref') p.tail = '\n' p.set('revision_id', rr.revision_id) if rr.revision_sha1: p.set('revision_sha1', rr.revision_sha1) return root def from_element(cls, elt): return unpack_revision(elt) from_element = classmethod(from_element) def unpack_revision(elt): """Convert XML element into Revision object.""" # is deprecated... if elt.tag not in ('revision', 'changeset'): raise BzrError("unexpected tag in revision file: %r" % elt) rev = Revision(committer = elt.get('committer'), timestamp = float(elt.get('timestamp')), revision_id = elt.get('revision_id'), inventory_id = elt.get('inventory_id'), inventory_sha1 = elt.get('inventory_sha1') ) precursor = elt.get('precursor') precursor_sha1 = elt.get('precursor_sha1') pelts = elt.find('parents') if precursor: # revisions written prior to 0.0.5 have a single precursor # give as an attribute rev_ref = RevisionReference(precursor, precursor_sha1) rev.parents.append(rev_ref) elif pelts: for p in pelts: assert p.tag == 'revision_ref', \ "bad parent node tag %r" % p.tag rev_ref = RevisionReference(p.get('revision_id'), p.get('revision_sha1')) rev.parents.append(rev_ref) v = elt.get('timezone') rev.timezone = v and int(v) rev.message = elt.findtext('message') # text of return rev commit refs/heads/master mark :709 committer Martin Pool 1119238404 +1000 data 91 - when writing a revision, store the first parent as the precursor to support old readers from :708 M 644 inline bzrlib/revision.py data 6063 # (C) 2005 Canonical # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from xml import XMLMixin try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from errors import BzrError class RevisionReference: """ Reference to a stored revision. Includes the revision_id and revision_sha1. """ revision_id = None revision_sha1 = None def __init__(self, revision_id, revision_sha1): if revision_id == None \ or isinstance(revision_id, basestring): self.revision_id = revision_id else: raise ValueError('bad revision_id %r' % revision_id) if revision_sha1 != None: if isinstance(revision_sha1, basestring) \ and len(revision_sha1) == 40: self.revision_sha1 = revision_sha1 else: raise ValueError('bad revision_sha1 %r' % revision_sha1) class Revision(XMLMixin): """Single revision on a branch. Revisions may know their revision_hash, but only once they've been written out. This is not stored because you cannot write the hash into the file it describes. After bzr 0.0.5 revisions are allowed to have multiple parents. To support old clients this is written out in a slightly redundant form: the first parent as the predecessor. This will eventually be dropped. parents List of parent revisions, each is a RevisionReference. """ inventory_id = None inventory_sha1 = None revision_id = None timestamp = None message = None timezone = None committer = None def __init__(self, **args): self.__dict__.update(args) self.parents = [] def _get_precursor(self): from warnings import warn warn("Revision.precursor is deprecated", stacklevel=2) if self.parents: return self.parents[0].revision_id else: return None def _get_precursor_sha1(self): from warnings import warn warn("Revision.precursor_sha1 is deprecated", stacklevel=2) if self.parents: return self.parents[0].revision_sha1 else: return None def _fail(self): raise Exception("can't assign to precursor anymore") precursor = property(_get_precursor, _fail, _fail) precursor_sha1 = property(_get_precursor_sha1, _fail, _fail) def __repr__(self): return "" % self.revision_id def to_element(self): root = Element('revision', committer = self.committer, timestamp = '%.9f' % self.timestamp, revision_id = self.revision_id, inventory_id = self.inventory_id, inventory_sha1 = self.inventory_sha1, ) if self.timezone: root.set('timezone', str(self.timezone)) root.text = '\n' msg = SubElement(root, 'message') msg.text = self.message msg.tail = '\n' if self.parents: # first parent stored as precursor for compatability with 0.0.5 and # earlier pr = self.parents[0] root.set('precursor', pr.revision_id) if pr.revision_sha1: root.set('precursor_sha1', pr.revision_sha1) if self.parents: pelts = SubElement(root, 'parents') pelts.tail = pelts.text = '\n' for rr in self.parents: assert isinstance(rr, RevisionReference) p = SubElement(pelts, 'revision_ref') p.tail = '\n' p.set('revision_id', rr.revision_id) if rr.revision_sha1: p.set('revision_sha1', rr.revision_sha1) return root def from_element(cls, elt): return unpack_revision(elt) from_element = classmethod(from_element) def unpack_revision(elt): """Convert XML element into Revision object.""" # is deprecated... if elt.tag not in ('revision', 'changeset'): raise BzrError("unexpected tag in revision file: %r" % elt) rev = Revision(committer = elt.get('committer'), timestamp = float(elt.get('timestamp')), revision_id = elt.get('revision_id'), inventory_id = elt.get('inventory_id'), inventory_sha1 = elt.get('inventory_sha1') ) precursor = elt.get('precursor') precursor_sha1 = elt.get('precursor_sha1') pelts = elt.find('parents') if precursor: # revisions written prior to 0.0.5 have a single precursor # give as an attribute rev_ref = RevisionReference(precursor, precursor_sha1) rev.parents.append(rev_ref) elif pelts: for p in pelts: assert p.tag == 'revision_ref', \ "bad parent node tag %r" % p.tag rev_ref = RevisionReference(p.get('revision_id'), p.get('revision_sha1')) rev.parents.append(rev_ref) v = elt.get('timezone') rev.timezone = v and int(v) rev.message = elt.findtext('message') # text of return rev commit refs/heads/master mark :710 committer Martin Pool 1119239102 +1000 data 65 - bzr upgrade updates or checks SHA1 on all predecessor revisions from :709 M 644 inline bzrlib/upgrade.py data 5247 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def upgrade(branch): """ Upgrade branch to current format. This causes objects to be rewritten into the current format. If they change, their SHA-1 will of course change, which might break any later signatures, or backreferences that check the SHA-1. TODO: Check non-mainline revisions. """ import sys from bzrlib.trace import mutter from bzrlib.errors import BzrCheckError from bzrlib.progress import ProgressBar branch.lock_write() try: pb = ProgressBar(show_spinner=True) last_rev_id = None history = branch.revision_history() revno = 0 revcount = len(history) updated_revisions = [] # Set to True in the case that the previous revision entry # was updated, since this will affect future revision entries updated_previous_revision = False for rev_id in history: revno += 1 pb.update('upgrading revision', revno, revcount) rev = branch.get_revision(rev_id) if rev.revision_id != rev_id: raise BzrCheckError('wrong internal revision id in revision {%s}' % rev_id) last_rev_id = rev_id # if set to true, revision must be written out updated = False if rev.inventory_sha1 is None: rev.inventory_sha1 = branch.get_inventory_sha1(rev.inventory_id) updated = True mutter(" set inventory_sha1 on {%s}" % rev_id) for prr in rev.parents: actual_sha1 = branch.get_revision_sha1(prr.revision_id) if (updated_previous_revision or prr.revision_sha1 is None): if prr.revision_sha1 != actual_sha1: prr.revision_sha1 = actual_sha1 updated = True elif actual_sha1 != prr.revision_sha1: raise BzrCheckError("parent {%s} of {%s} sha1 mismatch: " "%s vs %s" % (prr.revision_id, rev_id, actual_sha1, prr.revision_sha1)) if updated: updated_previous_revision = True # We had to update this revision entries hashes # Now we need to write out a new value # This is a little bit invasive, since we are *rewriting* a # revision entry. I'm not supremely happy about it, but # there should be *some* way of making old entries have # the full meta information. import tempfile, os, errno rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) tmpfd, tmp_path = tempfile.mkstemp(prefix=rev_id, suffix='.gz', dir=branch.controlfilename('revision-store')) os.close(tmpfd) def special_rename(p1, p2): if sys.platform == 'win32': try: os.remove(p2) except OSError, e: if e.errno != errno.ENOENT: raise os.rename(p1, p2) try: # TODO: We may need to handle the case where the old revision # entry was not compressed (and thus did not end with .gz) # Remove the old revision entry out of the way rev_path = branch.controlfilename(['revision-store', rev_id+'.gz']) special_rename(rev_path, tmp_path) branch.revision_store.add(rev_tmp, rev_id) # Add the new one os.remove(tmp_path) # Remove the old name mutter(' Updated revision entry {%s}' % rev_id) except: # On any exception, restore the old entry special_rename(tmp_path, rev_path) raise rev_tmp.close() updated_revisions.append(rev_id) else: updated_previous_revision = False finally: branch.unlock() pb.clear() if updated_revisions: print '%d revisions updated to current format' % len(updated_revisions) commit refs/heads/master mark :711 committer Martin Pool 1119239222 +1000 data 12 - store docs from :710 M 644 inline bzrlib/store.py data 5777 # Copyright (C) 2005 by Canonical Development Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ Stores are the main data-storage mechanism for Bazaar-NG. A store is a simple write-once container indexed by a universally unique ID. """ import os, tempfile, types, osutils, gzip, errno from stat import ST_SIZE from StringIO import StringIO from trace import mutter ###################################################################### # stores class StoreError(Exception): pass class ImmutableStore(object): """Store that holds files indexed by unique names. Files can be added, but not modified once they are in. Typically the hash is used as the name, or something else known to be unique, such as a UUID. >>> st = ImmutableScratchStore() >>> st.add(StringIO('hello'), 'aa') >>> 'aa' in st True >>> 'foo' in st False You are not allowed to add an id that is already present. Entries can be retrieved as files, which may then be read. >>> st.add(StringIO('goodbye'), '123123') >>> st['123123'].read() 'goodbye' TODO: Atomic add by writing to a temporary file and renaming. In bzr 0.0.5 and earlier, files within the store were marked readonly on disk. This is no longer done but existing stores need to be accomodated. """ def __init__(self, basedir): """ImmutableStore constructor.""" self._basedir = basedir def _path(self, id): assert '/' not in id return os.path.join(self._basedir, id) def __repr__(self): return "%s(%r)" % (self.__class__.__name__, self._basedir) def add(self, f, fileid, compressed=True): """Add contents of a file into the store. f -- An open file, or file-like object.""" # FIXME: Only works on smallish files # TODO: Can be optimized by copying at the same time as # computing the sum. mutter("add store entry %r" % (fileid)) if isinstance(f, types.StringTypes): content = f else: content = f.read() p = self._path(fileid) if os.access(p, os.F_OK) or os.access(p + '.gz', os.F_OK): raise BzrError("store %r already contains id %r" % (self._basedir, fileid)) if compressed: f = gzip.GzipFile(p + '.gz', 'wb') os.chmod(p + '.gz', 0444) else: f = file(p, 'wb') os.chmod(p, 0444) f.write(content) f.close() def copy_multi(self, other, ids): """Copy texts for ids from other into self. If an id is present in self, it is skipped. A count of copied ids is returned, which may be less than len(ids). """ from bzrlib.progress import ProgressBar pb = ProgressBar() pb.update('preparing to copy') to_copy = [id for id in ids if id not in self] count = 0 for id in to_copy: count += 1 pb.update('copy', count, len(to_copy)) self.add(other[id], id) assert count == len(to_copy) pb.clear() return count def __contains__(self, fileid): """""" p = self._path(fileid) return (os.access(p, os.R_OK) or os.access(p + '.gz', os.R_OK)) # TODO: Guard against the same thing being stored twice, compressed and uncompresse def __iter__(self): for f in os.listdir(self._basedir): if f[-3:] == '.gz': # TODO: case-insensitive? yield f[:-3] else: yield f def __len__(self): return len(os.listdir(self._basedir)) def __getitem__(self, fileid): """Returns a file reading from a particular entry.""" p = self._path(fileid) try: return gzip.GzipFile(p + '.gz', 'rb') except IOError, e: if e.errno == errno.ENOENT: return file(p, 'rb') else: raise e def total_size(self): """Return (count, bytes) This is the (compressed) size stored on disk, not the size of the content.""" total = 0 count = 0 for fid in self: count += 1 p = self._path(fid) try: total += os.stat(p)[ST_SIZE] except OSError: total += os.stat(p + '.gz')[ST_SIZE] return count, total class ImmutableScratchStore(ImmutableStore): """Self-destructing test subclass of ImmutableStore. The Store only exists for the lifetime of the Python object. Obviously you should not put anything precious in it. """ def __init__(self): ImmutableStore.__init__(self, tempfile.mkdtemp()) def __del__(self): for f in os.listdir(self._basedir): fpath = os.path.join(self._basedir, f) # needed on windows, and maybe some other filesystems os.chmod(fpath, 0600) os.remove(fpath) os.rmdir(self._basedir) mutter("%r destroyed" % self) commit refs/heads/master mark :712 committer Martin Pool 1119239266 +1000 data 36 - better check for invalid store ids from :711 M 644 inline bzrlib/store.py data 5841 # Copyright (C) 2005 by Canonical Development Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ Stores are the main data-storage mechanism for Bazaar-NG. A store is a simple write-once container indexed by a universally unique ID. """ import os, tempfile, types, osutils, gzip, errno from stat import ST_SIZE from StringIO import StringIO from trace import mutter ###################################################################### # stores class StoreError(Exception): pass class ImmutableStore(object): """Store that holds files indexed by unique names. Files can be added, but not modified once they are in. Typically the hash is used as the name, or something else known to be unique, such as a UUID. >>> st = ImmutableScratchStore() >>> st.add(StringIO('hello'), 'aa') >>> 'aa' in st True >>> 'foo' in st False You are not allowed to add an id that is already present. Entries can be retrieved as files, which may then be read. >>> st.add(StringIO('goodbye'), '123123') >>> st['123123'].read() 'goodbye' TODO: Atomic add by writing to a temporary file and renaming. In bzr 0.0.5 and earlier, files within the store were marked readonly on disk. This is no longer done but existing stores need to be accomodated. """ def __init__(self, basedir): """ImmutableStore constructor.""" self._basedir = basedir def _path(self, id): if '\\' in id or '/' in id: raise ValueError("invalid store id %r" % id) return os.path.join(self._basedir, id) def __repr__(self): return "%s(%r)" % (self.__class__.__name__, self._basedir) def add(self, f, fileid, compressed=True): """Add contents of a file into the store. f -- An open file, or file-like object.""" # FIXME: Only works on smallish files # TODO: Can be optimized by copying at the same time as # computing the sum. mutter("add store entry %r" % (fileid)) if isinstance(f, types.StringTypes): content = f else: content = f.read() p = self._path(fileid) if os.access(p, os.F_OK) or os.access(p + '.gz', os.F_OK): raise BzrError("store %r already contains id %r" % (self._basedir, fileid)) if compressed: f = gzip.GzipFile(p + '.gz', 'wb') os.chmod(p + '.gz', 0444) else: f = file(p, 'wb') os.chmod(p, 0444) f.write(content) f.close() def copy_multi(self, other, ids): """Copy texts for ids from other into self. If an id is present in self, it is skipped. A count of copied ids is returned, which may be less than len(ids). """ from bzrlib.progress import ProgressBar pb = ProgressBar() pb.update('preparing to copy') to_copy = [id for id in ids if id not in self] count = 0 for id in to_copy: count += 1 pb.update('copy', count, len(to_copy)) self.add(other[id], id) assert count == len(to_copy) pb.clear() return count def __contains__(self, fileid): """""" p = self._path(fileid) return (os.access(p, os.R_OK) or os.access(p + '.gz', os.R_OK)) # TODO: Guard against the same thing being stored twice, compressed and uncompresse def __iter__(self): for f in os.listdir(self._basedir): if f[-3:] == '.gz': # TODO: case-insensitive? yield f[:-3] else: yield f def __len__(self): return len(os.listdir(self._basedir)) def __getitem__(self, fileid): """Returns a file reading from a particular entry.""" p = self._path(fileid) try: return gzip.GzipFile(p + '.gz', 'rb') except IOError, e: if e.errno == errno.ENOENT: return file(p, 'rb') else: raise e def total_size(self): """Return (count, bytes) This is the (compressed) size stored on disk, not the size of the content.""" total = 0 count = 0 for fid in self: count += 1 p = self._path(fid) try: total += os.stat(p)[ST_SIZE] except OSError: total += os.stat(p + '.gz')[ST_SIZE] return count, total class ImmutableScratchStore(ImmutableStore): """Self-destructing test subclass of ImmutableStore. The Store only exists for the lifetime of the Python object. Obviously you should not put anything precious in it. """ def __init__(self): ImmutableStore.__init__(self, tempfile.mkdtemp()) def __del__(self): for f in os.listdir(self._basedir): fpath = os.path.join(self._basedir, f) # needed on windows, and maybe some other filesystems os.chmod(fpath, 0600) os.remove(fpath) os.rmdir(self._basedir) mutter("%r destroyed" % self) commit refs/heads/master mark :713 committer Martin Pool 1119239277 +1000 data 12 - store docs from :712 M 644 inline bzrlib/store.py data 5799 # Copyright (C) 2005 by Canonical Development Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ Stores are the main data-storage mechanism for Bazaar-NG. A store is a simple write-once container indexed by a universally unique ID. """ import os, tempfile, types, osutils, gzip, errno from stat import ST_SIZE from StringIO import StringIO from trace import mutter ###################################################################### # stores class StoreError(Exception): pass class ImmutableStore(object): """Store that holds files indexed by unique names. Files can be added, but not modified once they are in. Typically the hash is used as the name, or something else known to be unique, such as a UUID. >>> st = ImmutableScratchStore() >>> st.add(StringIO('hello'), 'aa') >>> 'aa' in st True >>> 'foo' in st False You are not allowed to add an id that is already present. Entries can be retrieved as files, which may then be read. >>> st.add(StringIO('goodbye'), '123123') >>> st['123123'].read() 'goodbye' TODO: Atomic add by writing to a temporary file and renaming. In bzr 0.0.5 and earlier, files within the store were marked readonly on disk. This is no longer done but existing stores need to be accomodated. """ def __init__(self, basedir): self._basedir = basedir def _path(self, id): if '\\' in id or '/' in id: raise ValueError("invalid store id %r" % id) return os.path.join(self._basedir, id) def __repr__(self): return "%s(%r)" % (self.__class__.__name__, self._basedir) def add(self, f, fileid, compressed=True): """Add contents of a file into the store. f -- An open file, or file-like object.""" # FIXME: Only works on smallish files # TODO: Can be optimized by copying at the same time as # computing the sum. mutter("add store entry %r" % (fileid)) if isinstance(f, types.StringTypes): content = f else: content = f.read() p = self._path(fileid) if os.access(p, os.F_OK) or os.access(p + '.gz', os.F_OK): raise BzrError("store %r already contains id %r" % (self._basedir, fileid)) if compressed: f = gzip.GzipFile(p + '.gz', 'wb') os.chmod(p + '.gz', 0444) else: f = file(p, 'wb') os.chmod(p, 0444) f.write(content) f.close() def copy_multi(self, other, ids): """Copy texts for ids from other into self. If an id is present in self, it is skipped. A count of copied ids is returned, which may be less than len(ids). """ from bzrlib.progress import ProgressBar pb = ProgressBar() pb.update('preparing to copy') to_copy = [id for id in ids if id not in self] count = 0 for id in to_copy: count += 1 pb.update('copy', count, len(to_copy)) self.add(other[id], id) assert count == len(to_copy) pb.clear() return count def __contains__(self, fileid): """""" p = self._path(fileid) return (os.access(p, os.R_OK) or os.access(p + '.gz', os.R_OK)) # TODO: Guard against the same thing being stored twice, compressed and uncompresse def __iter__(self): for f in os.listdir(self._basedir): if f[-3:] == '.gz': # TODO: case-insensitive? yield f[:-3] else: yield f def __len__(self): return len(os.listdir(self._basedir)) def __getitem__(self, fileid): """Returns a file reading from a particular entry.""" p = self._path(fileid) try: return gzip.GzipFile(p + '.gz', 'rb') except IOError, e: if e.errno == errno.ENOENT: return file(p, 'rb') else: raise e def total_size(self): """Return (count, bytes) This is the (compressed) size stored on disk, not the size of the content.""" total = 0 count = 0 for fid in self: count += 1 p = self._path(fid) try: total += os.stat(p)[ST_SIZE] except OSError: total += os.stat(p + '.gz')[ST_SIZE] return count, total class ImmutableScratchStore(ImmutableStore): """Self-destructing test subclass of ImmutableStore. The Store only exists for the lifetime of the Python object. Obviously you should not put anything precious in it. """ def __init__(self): ImmutableStore.__init__(self, tempfile.mkdtemp()) def __del__(self): for f in os.listdir(self._basedir): fpath = os.path.join(self._basedir, f) # needed on windows, and maybe some other filesystems os.chmod(fpath, 0600) os.remove(fpath) os.rmdir(self._basedir) mutter("%r destroyed" % self) commit refs/heads/master mark :714 committer Martin Pool 1119239663 +1000 data 49 - files within stores are no longer made readonly from :713 M 644 inline NEWS data 8533 DEVELOPMENT HEAD CHANGES: * New ``bzr upgrade`` command to upgrade the format of a branch, replacing ``bzr check --update``. * Files within store directories are no longer marked readonly on disk. bzr-0.0.5 2005-06-15 CHANGES: * ``bzr`` with no command now shows help rather than giving an error. Suggested by Michael Ellerman. * ``bzr status`` output format changed, because svn-style output doesn't really match the model of bzr. Now files are grouped by status and can be shown with their IDs. ``bzr status --all`` shows all versioned files and unknown files but not ignored files. * ``bzr log`` runs from most-recent to least-recent, the reverse of the previous order. The previous behaviour can be obtained with the ``--forward`` option. * ``bzr inventory`` by default shows only filenames, and also ids if ``--show-ids`` is given, in which case the id is the second field. ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New ``bzr ignore PATTERN`` command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.sw[nop] .git .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. File may be in a branch other than the working directory. * ``bzr log`` and ``bzr root`` can be given an http URL instead of a filename. * Commands can now be defined by external programs or scripts in a directory on $BZRPATH. * New "stat cache" avoids reading the contents of files if they haven't changed since the previous time. * If the Python interpreter is too old, try to find a better one or give an error. Based on a patch from Fredrik Lundh. * New optional parameter ``bzr info [BRANCH]``. * New form ``bzr commit SELECTED`` to commit only selected files. * New form ``bzr log -r FROM:TO`` shows changes in selected range; contributed by John A Meinel. * New option ``bzr diff --diff-options 'OPTS'`` allows passing options through to an external GNU diff. * New option ``bzr add --no-recurse`` to add a directory but not their contents. * ``bzr --version`` now shows more information if bzr is being run from a branch. BUG FIXES: * Fixed diff format so that added and removed files will be handled properly by patch. Fix from Lalo Martins. * Various fixes for files whose names contain spaces or other metacharacters. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. * testbzr requires python2.4, but can be used to test bzr running under a different version. * Tests added for many other changes in this release. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. * New developer commands ``added``, ``modified``. PORTABILITY: * Cope on Windows on python2.3 by using the weaker random seed. 2.4 is now only recommended. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/store.py data 5731 # Copyright (C) 2005 by Canonical Development Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ Stores are the main data-storage mechanism for Bazaar-NG. A store is a simple write-once container indexed by a universally unique ID. """ import os, tempfile, types, osutils, gzip, errno from stat import ST_SIZE from StringIO import StringIO from trace import mutter ###################################################################### # stores class StoreError(Exception): pass class ImmutableStore(object): """Store that holds files indexed by unique names. Files can be added, but not modified once they are in. Typically the hash is used as the name, or something else known to be unique, such as a UUID. >>> st = ImmutableScratchStore() >>> st.add(StringIO('hello'), 'aa') >>> 'aa' in st True >>> 'foo' in st False You are not allowed to add an id that is already present. Entries can be retrieved as files, which may then be read. >>> st.add(StringIO('goodbye'), '123123') >>> st['123123'].read() 'goodbye' TODO: Atomic add by writing to a temporary file and renaming. In bzr 0.0.5 and earlier, files within the store were marked readonly on disk. This is no longer done but existing stores need to be accomodated. """ def __init__(self, basedir): self._basedir = basedir def _path(self, id): if '\\' in id or '/' in id: raise ValueError("invalid store id %r" % id) return os.path.join(self._basedir, id) def __repr__(self): return "%s(%r)" % (self.__class__.__name__, self._basedir) def add(self, f, fileid, compressed=True): """Add contents of a file into the store. f -- An open file, or file-like object.""" # FIXME: Only works on smallish files # TODO: Can be optimized by copying at the same time as # computing the sum. mutter("add store entry %r" % (fileid)) if isinstance(f, types.StringTypes): content = f else: content = f.read() p = self._path(fileid) if os.access(p, os.F_OK) or os.access(p + '.gz', os.F_OK): raise BzrError("store %r already contains id %r" % (self._basedir, fileid)) if compressed: f = gzip.GzipFile(p + '.gz', 'wb') else: f = file(p, 'wb') f.write(content) f.close() def copy_multi(self, other, ids): """Copy texts for ids from other into self. If an id is present in self, it is skipped. A count of copied ids is returned, which may be less than len(ids). """ from bzrlib.progress import ProgressBar pb = ProgressBar() pb.update('preparing to copy') to_copy = [id for id in ids if id not in self] count = 0 for id in to_copy: count += 1 pb.update('copy', count, len(to_copy)) self.add(other[id], id) assert count == len(to_copy) pb.clear() return count def __contains__(self, fileid): """""" p = self._path(fileid) return (os.access(p, os.R_OK) or os.access(p + '.gz', os.R_OK)) # TODO: Guard against the same thing being stored twice, compressed and uncompresse def __iter__(self): for f in os.listdir(self._basedir): if f[-3:] == '.gz': # TODO: case-insensitive? yield f[:-3] else: yield f def __len__(self): return len(os.listdir(self._basedir)) def __getitem__(self, fileid): """Returns a file reading from a particular entry.""" p = self._path(fileid) try: return gzip.GzipFile(p + '.gz', 'rb') except IOError, e: if e.errno == errno.ENOENT: return file(p, 'rb') else: raise e def total_size(self): """Return (count, bytes) This is the (compressed) size stored on disk, not the size of the content.""" total = 0 count = 0 for fid in self: count += 1 p = self._path(fid) try: total += os.stat(p)[ST_SIZE] except OSError: total += os.stat(p + '.gz')[ST_SIZE] return count, total class ImmutableScratchStore(ImmutableStore): """Self-destructing test subclass of ImmutableStore. The Store only exists for the lifetime of the Python object. Obviously you should not put anything precious in it. """ def __init__(self): ImmutableStore.__init__(self, tempfile.mkdtemp()) def __del__(self): for f in os.listdir(self._basedir): fpath = os.path.join(self._basedir, f) # needed on windows, and maybe some other filesystems os.chmod(fpath, 0600) os.remove(fpath) os.rmdir(self._basedir) mutter("%r destroyed" % self) commit refs/heads/master mark :715 committer Martin Pool 1119241475 +1000 data 27 - add jk's patchwork client from :714 M 644 inline contrib/pwclient.full data 13377 #!/usr/bin/perl -w # # Patchwork - automated patch tracking system # Copyright (C) 2005 Jeremy Kerr # # This file is part of the Patchwork package. # # Patchwork is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # Patchwork is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Patchwork; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA use strict; use lib '../lib'; use SOAP::Lite; use Getopt::Std; my $uri = 'urn:SOAPInterface'; # this URI has the address of the soap.pl script, followed by the project name my $proxy = 'http://patchwork.ozlabs.org/soap.pl/bazaar-ng'; my $soap; my ($rows, $cols); my %actions = ( list => 'List all patches (restrict to a state with -s )', view => 'View a patch', get => 'Download a patch and save it locally', apply => 'Apply a patch (in the current dir, using -p1)', search => 'Search for patches (by name)' ); sub page($@) { my $str = shift; my $lines; if (@_) { ($lines) = @_; } else { my @l = split(/\n/, $str); $lines = $#l; } if ($rows && $lines >= $rows) { my $pager = $ENV{PAGER} || 'more'; open(FH, "|-", $pager) || die "Couldn't run pager '$pager': $!"; print FH $str; close(FH); } else { print $str; } } sub patch_list(@) { my @patches = @_; my $states = return "No patches\n" unless @patches; my $str = list_header(); my $max = $cols - 9; $max = 10 if $max < 10; foreach my $patch (@patches) { my $name = $patch->name(); if ($cols && length($name) > $max) { $name = substr($name, 0, $max - 1).'$'; } $str .= sprintf "%4d %3s %s\n", $patch->id(), substr(states($patch->state()), 0, 3), $name; } return $str; } sub _get_patch($) { my ($id) = @_; unless ($id) { print STDERR "No id given to retrieve a patch\n"; exit 1; } unless ($id =~ m/^[0-9]+$/) { print STDERR "Invalid patch id '$id'\n'"; exit 1; } my $res = $soap->get_patch($id); die "SOAP fault: ".$res->faultstring if $res->fault; my $patch = $res->result; unless ($patch) { print STDERR "Patch not found\n"; exit 1; } return $patch; } sub list() { my %opts; my $res; getopts('s:', \%opts); if ($opts{s}) { $res = $soap->get_patches_by_state(state_from_name($opts{s})); } else { $res = $soap->get_patches(); } die "SOAP fault: ".$res->faultstring if $res->fault; my $patches = $res->result; page(patch_list(@$patches), $#{$patches} + 2); return 1; } sub search() { my $query = join(' ', map { '"'.$_.'"' } @ARGV); my $res = $soap->search($query); die "SOAP fault: ".$res->faultstring if $res->fault; my $patches = $res->result; my $str = ''; unless ($patches && @{$patches}) { print "No patches found\n"; return 1; } $str .= list_header(); page(patch_list(@$patches), $#{$patches}); return 1; } sub view() { my ($id) = @ARGV; my $patch = _get_patch($id); page($patch->content()); return 1; } sub get() { my ($id) = @ARGV; my $patch = _get_patch($id); if (-e $patch->filename()) { printf STDERR "Patch file:\n\t%s\nalready exists\n", $patch->filename(); exit 1; } open(FH, ">", $patch->filename()) or die "Couldn't open ".$patch->filename()." for writing: $!"; print FH $patch->content; close(FH); printf "Saved '%s'\n\tto: %s\n", $patch->name, $patch->filename(); return 1; } sub apply() { my ($id) = @ARGV; my $patch = _get_patch($id); open(FH, "|-", "patch", "-p1") or die "Couldn't execute 'patch -p1'"; print FH $patch->content; close(FH); return 1; } sub usage() { printf STDERR "Usage: %s [options]\n", $0; printf STDERR "Where is one of:\n"; printf STDERR "\t%-6s : %s\n", $_, $actions{$_} for sort keys %actions; } sub list_header() { return sprintf "%4s %3s %s\n", 'ID', 'Sta', 'Name'; } my %_states; sub states(@) { my $state = @_ ? shift : undef; unless (%_states) { my $res = $soap->get_states(); die "SOAP fault: ".$res->faultstring if $res->fault; my $stateref = $res->result; %_states = %$stateref; } return $state ? $_states{$state} : %_states; } sub state_from_name($) { my ($name) = @_; my @matches; my %states = states(); foreach my $id (keys(%states)) { push(@matches, $id) if ($states{$id} =~ m/^$name/i); } if ($#matches < 0) { print STDERR "No such state '$name'\n"; exit 1; } elsif ($#matches > 0) { printf STDERR "Multiple states match '$name':\n"; printf STDERR "\t%s\n", $states{$_} for @matches; exit 1; } return $matches[0]; } my $action = shift; unless ($action) { usage(); exit 1; } if (eval "require Term::Size") { ($cols, $rows) = Term::Size::chars(*STDOUT); } else { ($cols, $rows) = (0,0); } $soap = new SOAP::Lite(uri => $uri, proxy => $proxy); foreach (sort(keys(%actions))) { if ($_ eq $action) { eval "return &$action()" or die $@; exit 0; } } printf STDERR "No such action '%s'\n", $action; usage(); exit 1; # Patchwork - automated patch tracking system # Copyright (C) 2005 Jeremy Kerr # # This file is part of the Patchwork package. # # Patchwork is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # Patchwork is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Patchwork; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA package PatchWork::Comment; use strict; # internal variables # id # msgid # submitter # content # date # @refs sub new($) { my ($cls) = @_; my $obj = {}; bless($obj, $cls); return $obj; } sub id(@) { my ($obj) = shift; if (@_) { $obj->{id} = shift } return $obj->{id}; } sub submitter(@) { my ($obj) = shift; if (@_) { $obj->{submitter} = shift } return $obj->{submitter}; } sub msgid(@) { my ($obj) = shift; if (@_) { $obj->{msgid} = shift } return $obj->{msgid}; } sub date(@) { my ($obj) = shift; if (@_) { $obj->{date} = shift } return $obj->{date}; } sub content(@) { my ($obj) = shift; if (@_) { $obj->{content} = shift } return $obj->{content}; } sub refs(@) { my ($obj) = shift; push(@{$obj->{refs}}, @_) if @_; return $obj->{refs}; } 1; # Patchwork - automated patch tracking system # Copyright (C) 2005 Jeremy Kerr # # This file is part of the Patchwork package. # # Patchwork is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # Patchwork is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Patchwork; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA package PatchWork::Person; use strict; # internal variables # email # name sub new(@) { my $cls = shift; my $obj = {}; bless($obj, $cls); $obj->{email} = shift; $obj->{name} = shift; return $obj; } sub parse_from($$) { my ($obj, $str) = @_; if ($str =~ m/"?(.*?)"?\s*<([^>]+)>/) { $obj->{email} = $2; $obj->{name} = $1; } elsif ($str =~ m/"?(.*?)"?\s*\(([^\)]+)\)/) { $obj->{email} = $1; $obj->{name} = $2; } else { $obj->{email} = $str; } } sub id(@) { my ($obj) = shift; if (@_) { $obj->{id} = shift } return $obj->{id}; } sub email(@) { my ($obj) = shift; if (@_) { $obj->{email} = shift } return $obj->{email}; } sub name(@) { my ($obj) = shift; if (@_) { $obj->{name} = shift } return $obj->{name}; } sub username(@) { my ($obj) = shift; if (@_) { $obj->{username} = shift } return $obj->{username}; } 1; # Patchwork - automated patch tracking system # Copyright (C) 2005 Jeremy Kerr # # This file is part of the Patchwork package. # # Patchwork is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # Patchwork is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Patchwork; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA package PatchWork::Patch; use strict; # internal variables # id # msgid # date # name # content # filename # submitter # comments # @trees sub new($) { my ($cls) = @_; my $obj = {}; bless($obj, $cls); $obj->{comments} = []; $obj->{trees} = {}; $obj->{archived} = 0; $obj->{state} = 1; return $obj; } sub id(@) { my ($obj) = shift; if (@_) { $obj->{id} = shift } return $obj->{id}; } sub msgid(@) { my ($obj) = shift; if (@_) { $obj->{msgid} = shift } return $obj->{msgid}; } sub date(@) { my ($obj) = shift; if (@_) { $obj->{date} = shift } return $obj->{date}; } sub state(@) { my ($obj) = shift; if (@_) { $obj->{state} = shift } return $obj->{state}; } sub name(@) { my ($obj) = shift; if (@_) { $obj->{name} = shift } return $obj->{name}; } sub filename(@) { my ($obj) = shift; if (@_) { $obj->{filename} = shift } return $obj->{filename}; } sub submitter(@) { my ($obj) = shift; if (@_) { $obj->{submitter} = shift } return $obj->{submitter}; } sub content(@) { my ($obj) = shift; if (@_) { $obj->{content} = shift } return $obj->{content}; } sub archived(@) { my ($obj) = shift; if (@_) { $obj->{archived} = shift } return $obj->{archived}; } sub add_comment($$) { my ($obj, $comment) = @_; push(@{$obj->{comments}}, $comment); } sub comments($) { my ($obj) = @_; return $obj->{comments}; } sub trees(@) { my ($obj) = shift; if (@_) { $obj->{trees} = shift } return $obj->{trees}; } 1; # Patchwork - automated patch tracking system # Copyright (C) 2005 Jeremy Kerr # # This file is part of the Patchwork package. # # Patchwork is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # Patchwork is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Patchwork; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA package PatchWork::Tree; use strict; # internal variables # id # name # url sub new($$) { my ($cls, $id) = @_; my $obj = {}; bless($obj, $cls); $obj->{id} = $id; return $obj; } sub id($) { my ($obj) = @_; return $obj->{id}; } sub name(@) { my ($obj) = shift; if (@_) { $obj->{name} = shift } return $obj->{name}; } sub url(@) { my ($obj) = shift; if (@_) { $obj->{url} = shift } return $obj->{url}; } 1; # Patchwork - automated patch tracking system # Copyright (C) 2005 Jeremy Kerr # # This file is part of the Patchwork package. # # Patchwork is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # Patchwork is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Patchwork; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA package PatchWork::User; @PatchWork::User::ISA = ('PatchWork::Person'); use strict; # internal variables # username sub new($$) { my ($cls, $id) = @_; my $obj = {}; bless($obj, $cls); $obj->{id} = $id; return $obj; } sub username(@) { my ($obj) = shift; if (@_) { $obj->{username} = shift } return $obj->{username}; } 1; commit refs/heads/master mark :716 committer Martin Pool 1119242003 +1000 data 35 - write into store using AtomicFile from :715 M 644 inline bzrlib/store.py data 5950 # Copyright (C) 2005 by Canonical Development Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ Stores are the main data-storage mechanism for Bazaar-NG. A store is a simple write-once container indexed by a universally unique ID. """ import os, tempfile, types, osutils, gzip, errno from stat import ST_SIZE from StringIO import StringIO from trace import mutter ###################################################################### # stores class StoreError(Exception): pass class ImmutableStore(object): """Store that holds files indexed by unique names. Files can be added, but not modified once they are in. Typically the hash is used as the name, or something else known to be unique, such as a UUID. >>> st = ImmutableScratchStore() >>> st.add(StringIO('hello'), 'aa') >>> 'aa' in st True >>> 'foo' in st False You are not allowed to add an id that is already present. Entries can be retrieved as files, which may then be read. >>> st.add(StringIO('goodbye'), '123123') >>> st['123123'].read() 'goodbye' TODO: Atomic add by writing to a temporary file and renaming. In bzr 0.0.5 and earlier, files within the store were marked readonly on disk. This is no longer done but existing stores need to be accomodated. """ def __init__(self, basedir): self._basedir = basedir def _path(self, id): if '\\' in id or '/' in id: raise ValueError("invalid store id %r" % id) return os.path.join(self._basedir, id) def __repr__(self): return "%s(%r)" % (self.__class__.__name__, self._basedir) def add(self, f, fileid, compressed=True): """Add contents of a file into the store. f -- An open file, or file-like object.""" # FIXME: Only works on files that will fit in memory from bzrlib.atomicfile import AtomicFile mutter("add store entry %r" % (fileid)) if isinstance(f, types.StringTypes): content = f else: content = f.read() p = self._path(fileid) if os.access(p, os.F_OK) or os.access(p + '.gz', os.F_OK): raise BzrError("store %r already contains id %r" % (self._basedir, fileid)) fn = p if compressed: fn = fn + '.gz' af = AtomicFile(fn, 'wb') try: if compressed: gf = gzip.GzipFile(mode='wb', fileobj=af) gf.write(content) gf.close() else: af.write(content) af.commit() finally: af.close() def copy_multi(self, other, ids): """Copy texts for ids from other into self. If an id is present in self, it is skipped. A count of copied ids is returned, which may be less than len(ids). """ from bzrlib.progress import ProgressBar pb = ProgressBar() pb.update('preparing to copy') to_copy = [id for id in ids if id not in self] count = 0 for id in to_copy: count += 1 pb.update('copy', count, len(to_copy)) self.add(other[id], id) assert count == len(to_copy) pb.clear() return count def __contains__(self, fileid): """""" p = self._path(fileid) return (os.access(p, os.R_OK) or os.access(p + '.gz', os.R_OK)) # TODO: Guard against the same thing being stored twice, compressed and uncompresse def __iter__(self): for f in os.listdir(self._basedir): if f[-3:] == '.gz': # TODO: case-insensitive? yield f[:-3] else: yield f def __len__(self): return len(os.listdir(self._basedir)) def __getitem__(self, fileid): """Returns a file reading from a particular entry.""" p = self._path(fileid) try: return gzip.GzipFile(p + '.gz', 'rb') except IOError, e: if e.errno == errno.ENOENT: return file(p, 'rb') else: raise e def total_size(self): """Return (count, bytes) This is the (compressed) size stored on disk, not the size of the content.""" total = 0 count = 0 for fid in self: count += 1 p = self._path(fid) try: total += os.stat(p)[ST_SIZE] except OSError: total += os.stat(p + '.gz')[ST_SIZE] return count, total class ImmutableScratchStore(ImmutableStore): """Self-destructing test subclass of ImmutableStore. The Store only exists for the lifetime of the Python object. Obviously you should not put anything precious in it. """ def __init__(self): ImmutableStore.__init__(self, tempfile.mkdtemp()) def __del__(self): for f in os.listdir(self._basedir): fpath = os.path.join(self._basedir, f) # needed on windows, and maybe some other filesystems os.chmod(fpath, 0600) os.remove(fpath) os.rmdir(self._basedir) mutter("%r destroyed" % self) commit refs/heads/master mark :717 committer Martin Pool 1119243466 +1000 data 72 - correctly set parent list when committing first revision to a branch from :716 M 644 inline bzrlib/commit.py data 10450 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def commit(branch, message, timestamp=None, timezone=None, committer=None, verbose=True, specific_files=None, rev_id=None): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. specific_files If true, commit only those files. rev_id If set, use this as the new revision id. Useful for test or import commands that need to tightly control what revisions are assigned. If you duplicate a revision id that exists elsewhere it is your own fault. If null (default), a time/random revision id is generated. """ import time, tempfile from bzrlib.osutils import local_time_offset, username from bzrlib.branch import gen_file_id from bzrlib.errors import BzrError from bzrlib.revision import Revision, RevisionReference from bzrlib.trace import mutter, note branch.lock_write() try: # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_tree = branch.working_tree() work_inv = work_tree.inventory basis = branch.basis_tree() basis_inv = basis.inventory if verbose: note('looking for changes...') missing_ids, new_inv = _gather_commit(branch, work_tree, work_inv, basis_inv, specific_files, verbose) for file_id in missing_ids: # Any files that have been deleted are now removed from the # working inventory. Files that were not selected for commit # are left as they were in the working inventory and ommitted # from the revision inventory. # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itbranch. if work_inv.has_id(file_id): del work_inv[file_id] if rev_id is None: rev_id = _gen_revision_id(time.time()) inv_id = rev_id inv_tmp = tempfile.TemporaryFile() new_inv.write_xml(inv_tmp) inv_tmp.seek(0) branch.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) # We could also just sha hash the inv_tmp file # however, in the case that branch.inventory_store.add() # ever actually does anything special inv_sha1 = branch.get_inventory_sha1(inv_id) branch._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, message = message, inventory_id=inv_id, inventory_sha1=inv_sha1, revision_id=rev_id) precursor_id = branch.last_patch() if precursor_id: precursor_sha1 = branch.get_revision_sha1(precursor_id) rev.parents = [RevisionReference(precursor_id, precursor_sha1)] rev_tmp = tempfile.TemporaryFile() rev.write_xml(rev_tmp) rev_tmp.seek(0) branch.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (branch.revno() + 1)) branch.append_revision(rev_id) if verbose: note("commited r%d" % branch.revno()) finally: branch.unlock() def _gen_revision_id(when): """Return new revision-id.""" from binascii import hexlify from osutils import rand_bytes, compact_date, user_email s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def _gather_commit(branch, work_tree, work_inv, basis_inv, specific_files, verbose): """Build inventory preparatory to commit. This adds any changed files into the text store, and sets their test-id, sha and size in the returned inventory appropriately. missing_ids Modified to hold a list of files that have been deleted from the working directory; these should be removed from the working inventory. """ from bzrlib.inventory import Inventory from osutils import isdir, isfile, sha_string, quotefn, \ local_time_offset, username, kind_marker, is_inside_any from branch import gen_file_id from errors import BzrError from revision import Revision from bzrlib.trace import mutter, note inv = Inventory() missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). p = branch.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if specific_files and not is_inside_any(specific_files, path): if basis_inv.has_id(file_id): # carry over with previous state inv.add(basis_inv[file_id].copy()) else: # omit this from committed inventory pass continue if not work_tree.has_id(file_id): if verbose: print('deleted %s%s' % (path, kind_marker(entry.kind))) mutter(" file is missing, removing from inventory") missing_ids.append(file_id) continue # this is present in the new inventory; may be new, modified or # unchanged. old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] entry = entry.copy() inv.add(entry) if old_ie: old_kind = old_ie.kind if old_kind != entry.kind: raise BzrError("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): raise BzrError("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): raise BzrError("%s is entered as file but is not a file" % quotefn(p)) new_sha1 = work_tree.get_file_sha1(file_id) if (old_ie and old_ie.text_sha1 == new_sha1): ## assert content == basis.get_file(file_id).read() entry.text_id = old_ie.text_id entry.text_sha1 = new_sha1 entry.text_size = old_ie.text_size mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: content = file(p, 'rb').read() # calculate the sha again, just in case the file contents # changed since we updated the cache entry.text_sha1 = sha_string(content) entry.text_size = len(content) entry.text_id = gen_file_id(entry.name) branch.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: marked = path + kind_marker(entry.kind) if not old_ie: print 'added', marked elif old_ie == entry: pass # unchanged elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): print 'modified', marked else: print 'renamed', marked return missing_ids, inv commit refs/heads/master mark :718 committer Martin Pool 1119243492 +1000 data 50 - add consistency checks when writing out revision from :717 M 644 inline bzrlib/revision.py data 6135 # (C) 2005 Canonical # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from xml import XMLMixin try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from errors import BzrError class RevisionReference: """ Reference to a stored revision. Includes the revision_id and revision_sha1. """ revision_id = None revision_sha1 = None def __init__(self, revision_id, revision_sha1): if revision_id == None \ or isinstance(revision_id, basestring): self.revision_id = revision_id else: raise ValueError('bad revision_id %r' % revision_id) if revision_sha1 != None: if isinstance(revision_sha1, basestring) \ and len(revision_sha1) == 40: self.revision_sha1 = revision_sha1 else: raise ValueError('bad revision_sha1 %r' % revision_sha1) class Revision(XMLMixin): """Single revision on a branch. Revisions may know their revision_hash, but only once they've been written out. This is not stored because you cannot write the hash into the file it describes. After bzr 0.0.5 revisions are allowed to have multiple parents. To support old clients this is written out in a slightly redundant form: the first parent as the predecessor. This will eventually be dropped. parents List of parent revisions, each is a RevisionReference. """ inventory_id = None inventory_sha1 = None revision_id = None timestamp = None message = None timezone = None committer = None def __init__(self, **args): self.__dict__.update(args) self.parents = [] def _get_precursor(self): from warnings import warn warn("Revision.precursor is deprecated", stacklevel=2) if self.parents: return self.parents[0].revision_id else: return None def _get_precursor_sha1(self): from warnings import warn warn("Revision.precursor_sha1 is deprecated", stacklevel=2) if self.parents: return self.parents[0].revision_sha1 else: return None def _fail(self): raise Exception("can't assign to precursor anymore") precursor = property(_get_precursor, _fail, _fail) precursor_sha1 = property(_get_precursor_sha1, _fail, _fail) def __repr__(self): return "" % self.revision_id def to_element(self): root = Element('revision', committer = self.committer, timestamp = '%.9f' % self.timestamp, revision_id = self.revision_id, inventory_id = self.inventory_id, inventory_sha1 = self.inventory_sha1, ) if self.timezone: root.set('timezone', str(self.timezone)) root.text = '\n' msg = SubElement(root, 'message') msg.text = self.message msg.tail = '\n' if self.parents: # first parent stored as precursor for compatability with 0.0.5 and # earlier pr = self.parents[0] assert pr.revision_id root.set('precursor', pr.revision_id) if pr.revision_sha1: root.set('precursor_sha1', pr.revision_sha1) if self.parents: pelts = SubElement(root, 'parents') pelts.tail = pelts.text = '\n' for rr in self.parents: assert isinstance(rr, RevisionReference) p = SubElement(pelts, 'revision_ref') p.tail = '\n' assert rr.revision_id p.set('revision_id', rr.revision_id) if rr.revision_sha1: p.set('revision_sha1', rr.revision_sha1) return root def from_element(cls, elt): return unpack_revision(elt) from_element = classmethod(from_element) def unpack_revision(elt): """Convert XML element into Revision object.""" # is deprecated... if elt.tag not in ('revision', 'changeset'): raise BzrError("unexpected tag in revision file: %r" % elt) rev = Revision(committer = elt.get('committer'), timestamp = float(elt.get('timestamp')), revision_id = elt.get('revision_id'), inventory_id = elt.get('inventory_id'), inventory_sha1 = elt.get('inventory_sha1') ) precursor = elt.get('precursor') precursor_sha1 = elt.get('precursor_sha1') pelts = elt.find('parents') if precursor: # revisions written prior to 0.0.5 have a single precursor # give as an attribute rev_ref = RevisionReference(precursor, precursor_sha1) rev.parents.append(rev_ref) elif pelts: for p in pelts: assert p.tag == 'revision_ref', \ "bad parent node tag %r" % p.tag rev_ref = RevisionReference(p.get('revision_id'), p.get('revision_sha1')) rev.parents.append(rev_ref) v = elt.get('timezone') rev.timezone = v and int(v) rev.message = elt.findtext('message') # text of return rev commit refs/heads/master mark :719 committer Martin Pool 1119244140 +1000 data 26 - reorganize selftest code from :718 M 644 inline bzrlib/selftest.py data 1661 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import unittest class _MyResult(unittest.TestResult): # def startTest(self, test): # print str(test).ljust(50), # unittest.TestResult.startTest(self, test) # def stopTest(self, test): # print # unittest.TestResult.stopTest(self, test) pass def selftest(): from unittest import TestLoader, TestSuite import bzrlib import bzrlib.whitebox from doctest import DocTestSuite suite = TestSuite() suite.addTest(TestLoader().loadTestsFromModule(bzrlib.whitebox)) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands: suite.addTest(DocTestSuite(m)) result = _MyResult() suite.run(result) print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) return result.wasSuccessful() commit refs/heads/master mark :720 committer Martin Pool 1119244924 +1000 data 58 - start moving external tests into the testsuite framework from :719 M 644 inline bzrlib/blackbox.py data 1159 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Black-box tests for bzr. These check that it behaves properly when it's invoked through the regular command-line interface. """ # this code was previously in testbzr from unittest import TestCase class TestVersion(TestCase): def runTest(self): from os import system rc = system('bzr version') if rc != 0: fail("command returned status %d" % rc) M 644 inline bzrlib/selftest.py data 1736 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase class _MyResult(TestResult): # def startTest(self, test): # print str(test).ljust(50), # TestResult.startTest(self, test) # def stopTest(self, test): # print # TestResult.stopTest(self, test) pass def selftest(): from unittest import TestLoader, TestSuite import bzrlib import bzrlib.whitebox import bzrlib.blackbox from doctest import DocTestSuite suite = TestSuite() tl = TestLoader() for m in bzrlib.whitebox, bzrlib.blackbox: suite.addTest(tl.loadTestsFromModule(m)) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands: suite.addTest(DocTestSuite(m)) result = _MyResult() suite.run(result) print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) return result.wasSuccessful() commit refs/heads/master mark :721 committer Martin Pool 1119333911 +1000 data 106 - framework for running external commands from unittest suite - show better messages when some tests fail from :720 M 644 inline bzrlib/blackbox.py data 1384 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Black-box tests for bzr. These check that it behaves properly when it's invoked through the regular command-line interface. """ # this code was previously in testbzr from unittest import TestCase from bzrlib.selftest import TestBase class TestVersion(TestBase): def runTest(self): # output is intentionally passed through to stdout so that we # can see the version being tested self.runcmd(['bzr', 'version']) # class InTempBranch(TestBase): # """Base class for tests run in a temporary branch.""" # def setUp(): # def tearDown() # class InitBranch(TestBase): # def runTest(self): M 644 inline bzrlib/selftest.py data 3991 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr def runcmd(self, cmd, expected=0): self.log('$ ' + ' '.join(cmd)) from os import spawnvp, P_WAIT rc = spawnvp(P_WAIT, cmd[0], cmd) if rc != expected: self.fail("command %r returned status %d" % (cmd, rc)) def backtick(self, cmd): """Run a command and return its output""" from os import popen self.log('$ ' + ' '.join(cmd)) pipe = popen(cmd) out = '' while True: buf = pipe.read() if buf: out += buf else: break rc = pipe.close() if rc: self.fail("command %r returned status %d" % (cmd, rc)) else: return out def log(self, msg): """Log a message to a progress file""" print >>TEST_LOG, msg class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ # def startTest(self, test): # print str(test).ljust(50), # TestResult.startTest(self, test) # def stopTest(self, test): # print # TestResult.stopTest(self, test) pass def selftest(): from unittest import TestLoader, TestSuite import bzrlib import bzrlib.whitebox import bzrlib.blackbox from doctest import DocTestSuite import os import shutil import time _setup_test_log() _setup_test_dir() suite = TestSuite() tl = TestLoader() for m in bzrlib.whitebox, bzrlib.blackbox: suite.addTest(tl.loadTestsFromModule(m)) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands: suite.addTest(DocTestSuite(m)) result = _MyResult() suite.run(result) _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os global TEST_LOG log_filename = os.path.abspath('testbzr.log') TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TEST_LOG, "bzr tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil global ORIG_DIR, TEST_DIR ORIG_DIR = os.getcwdu() TEST_DIR = os.path.abspath("testbzr.tmp") print '%-30s %s' % ('running tests in', TEST_DIR) if os.path.exists(TEST_DIR): shutil.rmtree(TEST_DIR) os.mkdir(TEST_DIR) os.chdir(TEST_DIR) def _show_results(result): for case, tb in result.errors: _show_test_failure('ERROR', case, tb) for case, tb in result.failures: _show_test_failure('FAILURE', case, tb) print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, tb): print (kind + '! ').ljust(60, '-') print case print tb print ''.ljust(60, '-') commit refs/heads/master mark :722 committer Martin Pool 1119334201 +1000 data 9 - cleanup from :721 commit refs/heads/master mark :723 committer Martin Pool 1119334218 +1000 data 66 - move whitebox/blackbox modules into bzrlib.selftest subdirectory from :722 R bzrlib/blackbox.py bzrlib/selftest/blackbox.py R bzrlib/selftest.py bzrlib/selftest/__init__.py R bzrlib/whitebox.py bzrlib/selftest/whitebox.py M 644 inline bzrlib/commands.py data 49703 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _find_plugins(): """Find all python files which are plugins, and load their commands to add to the list of "all commands" The environment variable BZRPATH is considered a delimited set of paths to look through. Each entry is searched for *.py files. If a directory is found, it is also searched, but they are not searched recursively. This allows you to revctl the plugins. Inside the plugin should be a series of cmd_* function, which inherit from the bzrlib.commands.Command class. """ bzrpath = os.environ.get('BZRPLUGINPATH', '') plugin_cmds = {} if not bzrpath: return plugin_cmds _platform_extensions = { 'win32':'.pyd', 'cygwin':'.dll', 'darwin':'.dylib', 'linux2':'.so' } if _platform_extensions.has_key(sys.platform): platform_extension = _platform_extensions[sys.platform] else: platform_extension = None for d in bzrpath.split(os.pathsep): plugin_names = {} # This should really be a set rather than a dict for f in os.listdir(d): if f.endswith('.py'): f = f[:-3] elif f.endswith('.pyc') or f.endswith('.pyo'): f = f[:-4] elif platform_extension and f.endswith(platform_extension): f = f[:-len(platform_extension)] if f.endswidth('module'): f = f[:-len('module')] else: continue if not plugin_names.has_key(f): plugin_names[f] = True plugin_names = plugin_names.keys() plugin_names.sort() try: sys.path.insert(0, d) for name in plugin_names: try: old_module = None try: if sys.modules.has_key(name): old_module = sys.modules[name] del sys.modules[name] plugin = __import__(name, locals()) for k in dir(plugin): if k.startswith('cmd_'): k_unsquished = _unsquish_command_name(k) if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = getattr(plugin, k) else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r in dir %r' % (name, d)) finally: if old_module: sys.modules[name] = old_module except ImportError, e: log_error('Unable to load plugin: %r from %r\n%s' % (name, d, e)) finally: sys.path.pop(0) return plugin_cmds def _get_cmd_dict(include_plugins=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v if include_plugins: d.update(_find_plugins()) return d def get_all_cmds(include_plugins=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(include_plugins=include_plugins).iteritems(): yield k,v def get_cmd_class(cmd,include_plugins=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(include_plugins=include_plugins) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() for opt in self.takes_options: if not opt in OPTIONS: raise BzrError("Unknown option '%s' returned by external command %s" % (opt, path)) # TODO: Is there any way to check takes_args is valid here? self.takes_args = pipe.readline().split() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-usage'" % path) pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-help'" % path) def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: optname = name.replace('_','-') value = kargs[name] if OPTIONS.has_key(optname): # it's an option opts.append('--%s' % optname) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import errno br_to = Branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if errno == errno.ENOENT: raise if location is None: location = stored_loc if location is None: raise BzrCommandError("No pull location known or specified.") from branch import find_branch, DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged. Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. To retrieve the branch as of a particular revision, supply the --revision parameter, as in "branch foo/bar -r 5". """ takes_args = ['from_location', 'to_location?'] takes_options = ['revision'] def run(self, from_location, to_location=None, revision=None): import errno from bzrlib.merge import merge from branch import find_branch, DivergedBranches, NoSuchRevision from shutil import rmtree try: br_from = find_branch(from_location) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location.rstrip("/\\")) try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) try: br_to.update_revisions(br_from, stop_revision=revision) except NoSuchRevision: rmtree(to_location) msg = "The branch %s has no revision %d." % (from_location, revision) raise BzrCommandError(msg) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False, ignore_zero=True) from_location = pull_loc(br_from) br_to.controlfile("x-pull", "wb").write(from_location + "\n") def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: raise BzrError("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: raise BzrError("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, exports to a directory (equivalent to --format=dir).""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format'] def run(self, dest, revision=None, format='dir'): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest, format) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_upgrade(Command): """Upgrade branch storage to current format. This should normally be used only after the check command tells you to run it. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.upgrade import upgrade upgrade(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest return int(not selftest()) class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'update': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) >>> parse_args('log -r 500'.split()) (['log'], {'revision': 500}) >>> parse_args('log -r500:600'.split()) (['log'], {'revision': [500, 600]}) >>> parse_args('log -vr500:600'.split()) (['log'], {'verbose': True, 'revision': [500, 600]}) >>> parse_args('log -rv500:600'.split()) #the r takes an argument Traceback (most recent call last): ... ValueError: invalid literal for int(): v500 """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: raise BzrError('unknown long option %r' % a) else: shortopt = a[1:] if shortopt in SHORT_OPTIONS: # Multi-character options must have a space to delimit # their value optname = SHORT_OPTIONS[shortopt] else: # Single character short options, can be chained, # and have their value appended to their name shortopt = a[1:2] if shortopt not in SHORT_OPTIONS: # We didn't find the multi-character name, and we # didn't find the single char name raise BzrError('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if a[2:]: # There are extra things on this option # see if it is the value, or if it is another # short option optargfn = OPTIONS[optname] if optargfn is None: # This option does not take an argument, so the # next entry is another short option, pack it back # into the list argv.insert(0, '-' + a[2:]) else: # This option takes an argument, so pack it # into the array optarg = a[2:] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? raise BzrError('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: raise BzrError('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: raise BzrError('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] include_plugins=True try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd,include_plugins=include_plugins) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/selftest/__init__.py data 4027 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr def runcmd(self, cmd, expected=0): self.log('$ ' + ' '.join(cmd)) from os import spawnvp, P_WAIT rc = spawnvp(P_WAIT, cmd[0], cmd) if rc != expected: self.fail("command %r returned status %d" % (cmd, rc)) def backtick(self, cmd): """Run a command and return its output""" from os import popen self.log('$ ' + ' '.join(cmd)) pipe = popen(cmd) out = '' while True: buf = pipe.read() if buf: out += buf else: break rc = pipe.close() if rc: self.fail("command %r returned status %d" % (cmd, rc)) else: return out def log(self, msg): """Log a message to a progress file""" print >>TEST_LOG, msg class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ # def startTest(self, test): # print str(test).ljust(50), # TestResult.startTest(self, test) # def stopTest(self, test): # print # TestResult.stopTest(self, test) pass def selftest(): from unittest import TestLoader, TestSuite import bzrlib import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox from doctest import DocTestSuite import os import shutil import time _setup_test_log() _setup_test_dir() suite = TestSuite() tl = TestLoader() for m in bzrlib.selftest.whitebox, bzrlib.selftest.blackbox: suite.addTest(tl.loadTestsFromModule(m)) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands: suite.addTest(DocTestSuite(m)) result = _MyResult() suite.run(result) _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os global TEST_LOG log_filename = os.path.abspath('testbzr.log') TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TEST_LOG, "bzr tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil global ORIG_DIR, TEST_DIR ORIG_DIR = os.getcwdu() TEST_DIR = os.path.abspath("testbzr.tmp") print '%-30s %s' % ('running tests in', TEST_DIR) if os.path.exists(TEST_DIR): shutil.rmtree(TEST_DIR) os.mkdir(TEST_DIR) os.chdir(TEST_DIR) def _show_results(result): for case, tb in result.errors: _show_test_failure('ERROR', case, tb) for case, tb in result.failures: _show_test_failure('FAILURE', case, tb) print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, tb): print (kind + '! ').ljust(60, '-') print case print tb print ''.ljust(60, '-') commit refs/heads/master mark :724 committer Martin Pool 1119334254 +1000 data 17 - todo: bzr mkdir from :723 M 644 inline TODO data 13466 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * ``bzr log DIR`` should give changes to any files within DIR. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. Perhaps ~/.bzr/http-cache. Baz has a fairly simple cache under ~/.arch-cache, containing revision information encoded almost as a bunch of archives. Perhaps we could simply store full paths. * Maybe also store directories in the statcache so that we can quickly identify that they still exist. * Diff should show timestamps; for files from the working directory we can use the file itself; for files from a revision we should use the commit time of the revision. * Perhaps split command infrastructure from the actual command definitions. * Cleaner support for negative boolean options like --no-recurse. * Statcache should possibly map all file paths to / separators * quotefn doubles all backslashes on Windows; this is probably not the best thing to do. What would be a better way to safely represent filenames? Perhaps we could doublequote things containing spaces, on the principle that filenames containing quotes are unlikely? Nice for humans; less good for machine parsing. * Patches should probably use only forward slashes, even on Windows, otherwise Unix patch can't apply them. (?) * Branch.update_revisions() inefficiently fetches revisions from the remote server twice; once to find out what text and inventory they need and then again to actually get the thing. This is a bit inefficient. One complicating factor here is that we don't really want to have revisions present in the revision-store until all their constituent parts are also stored. The basic problem is that RemoteBranch.get_revision() and similar methods return object, but what we really want is the raw XML, which can be popped into our own store. That needs to be refactored. * ``bzr status FOO`` where foo is ignored should say so. * ``bzr mkdir A...`` should just create and add A. Medium things ------------- * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. We should be able to just get the id for the selected files, look up their location and diff just those files. No need to traverse the entire inventories. * ``bzr status DIR`` or ``bzr diff DIR`` should report on all changes under that directory. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from ElementTree to an object when it is read in, but rather wait until the program actually wants to know about that node. * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. - Selected-file commit - Impossible selected-file commit: adding things in non-versioned directories, crossing renames, etc. * Write a reproducible benchmark, perhaps importing various kernel versions. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Paranoid mode where we never trust SHA-1 matches. * Don't commit if there are no changes unless forced. * --dry-run mode for commit? (Or maybe just run with check-command=false?) * Generally, be a bit more verbose unless --silent is specified. * Function that finds all changes to files under a given directory; perhaps log should use this if a directory is given. * XML attributes might have trouble with filenames containing \n and \r. Do we really want to support this? I think perhaps not. * Remember execute bits, so that exports will work OK. * Unify smart_add and plain Branch.add(); perhaps smart_add should just build a list of files to add and pass that to the regular add function. * Function to list a directory, saying in which revision each file was last modified. Useful for web and gui interfaces, and slow to compute one file at a time. * unittest is standard, but the results are kind of ugly; would be nice to make it cleaner. * Check locking is correct during merge-related operations. * Perhaps attempts to get locks should timeout after some period of time, or at least display a progress message. * Split out upgrade functionality from check command into a separate ``bzr upgrade``. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. Possibly this should be done by splitting the commit function into several parts (under a single interface). It is already rather large. Decomposition: - find tree modifications and prepare in-memory inventory - export that inventory to a temporary directory - run the test in that temporary directory - if that succeeded, continue to actually finish the commit What should be done with the text of modified files while this is underway? I don't think we want to count on holding them in memory and we can't trust the working files to stay in one place so I suppose we need to move them into the text store, or otherwise into a temporary directory. If the commit does not actually complete, we would rather the content was not left behind in the stores. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :725 committer Martin Pool 1119334365 +1000 data 3 doc from :724 M 644 inline bzrlib/selftest/blackbox.py data 1550 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Black-box tests for bzr. These check that it behaves properly when it's invoked through the regular command-line interface. This always reinvokes bzr through a new Python interpreter, which is a bit inefficient but arguably tests in a way more representative of how it's normally invoked. """ # this code was previously in testbzr from unittest import TestCase from bzrlib.selftest import TestBase class TestVersion(TestBase): def runTest(self): # output is intentionally passed through to stdout so that we # can see the version being tested self.runcmd(['bzr', 'version']) # class InTempBranch(TestBase): # """Base class for tests run in a temporary branch.""" # def setUp(): # def tearDown() # class InitBranch(TestBase): # def runTest(self): commit refs/heads/master mark :726 committer Martin Pool 1119340631 +1000 data 38 - more rearrangement of blackbox tests from :725 M 644 inline bzrlib/selftest/__init__.py data 4293 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr def runcmd(self, cmd, expected=0): self.log('$ ' + ' '.join(cmd)) from os import spawnvp, P_WAIT rc = spawnvp(P_WAIT, cmd[0], cmd) if rc != expected: self.fail("command %r returned status %d" % (cmd, rc)) def backtick(self, cmd): """Run a command and return its output""" from os import popen self.log('$ ' + ' '.join(cmd)) pipe = popen(cmd) out = '' while True: buf = pipe.read() if buf: out += buf self.log(buf) else: break rc = pipe.close() if rc: self.fail("command %r returned status %d" % (cmd, rc)) else: return out def log(self, msg): """Log a message to a progress file""" print >>self.TEST_LOG, msg class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ # def startTest(self, test): # print str(test).ljust(50), # TestResult.startTest(self, test) # def stopTest(self, test): # print # TestResult.stopTest(self, test) pass def selftest(): from unittest import TestLoader, TestSuite import bzrlib import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox from doctest import DocTestSuite import os import shutil import time _setup_test_log() _setup_test_dir() suite = TestSuite() tl = TestLoader() for m in bzrlib.selftest.whitebox, : suite.addTest(tl.loadTestsFromModule(m)) suite.addTest(bzrlib.selftest.blackbox.suite()) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands: suite.addTest(DocTestSuite(m)) result = _MyResult() suite.run(result) _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os log_filename = os.path.abspath('testbzr.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "bzr tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_DIR = os.path.abspath("testbzr.tmp") print '%-30s %s' % ('running tests in', TestBase.TEST_DIR) if os.path.exists(TestBase.TEST_DIR): shutil.rmtree(TestBase.TEST_DIR) os.mkdir(TestBase.TEST_DIR) os.chdir(TestBase.TEST_DIR) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_DIR, '.bzr')) def _show_results(result): for case, tb in result.errors: _show_test_failure('ERROR', case, tb) for case, tb in result.failures: _show_test_failure('FAILURE', case, tb) print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, tb): print (kind + '! ').ljust(60, '-') print case print tb print ''.ljust(60, '-') M 644 inline bzrlib/selftest/blackbox.py data 2301 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Black-box tests for bzr. These check that it behaves properly when it's invoked through the regular command-line interface. This always reinvokes bzr through a new Python interpreter, which is a bit inefficient but arguably tests in a way more representative of how it's normally invoked. """ # this code was previously in testbzr from unittest import TestCase from bzrlib.selftest import TestBase class TestVersion(TestBase): def runTest(self): # output is intentionally passed through to stdout so that we # can see the version being tested print self.runcmd(['bzr', 'version']) print class InTempBranch(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.branch_dir = os.path.join(self.TEST_DIR, self.__class__.__name__) os.mkdir(self.branch_dir) os.chdir(self.branch_dir) def tearDown(self): import os os.chdir(self.TEST_DIR) class InitBranch(InTempBranch): def runTest(self): import os print "%s running in %s" % (self, os.getcwdu()) self.runcmd(['bzr', 'init']) # lists all tests from this module in the best order to run them. we # do it this way rather than just discovering them all because it # allows us to test more basic functions first where failures will be # easiest to understand. def suite(): from unittest import TestSuite s = TestSuite() s.addTests([TestVersion(), InitBranch()]) return s commit refs/heads/master mark :727 committer Martin Pool 1119342905 +1000 data 66 - move more code to run external commands from testbzr to selftest from :726 M 644 inline bzrlib/selftest/__init__.py data 4956 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") raise class CommandFailed(Exception): pass class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr, so that we can run it # through a specified Python intepreter OVERRIDE_PYTHON = None # to run with alternative python 'python' BZRPATH = 'bzr' def formcmd(self, cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = self.BZRPATH if self.OVERRIDE_PYTHON: cmd.insert(0, self.OVERRIDE_PYTHON) self.log('$ %r' % cmd) return cmd def runcmd(self, cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = self.formcmd(cmd) self.log('$ ' + ' '.join(cmd)) actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def log(self, msg): """Log a message to a progress file""" print >>self.TEST_LOG, msg class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ # def startTest(self, test): # print str(test).ljust(50), # TestResult.startTest(self, test) # def stopTest(self, test): # print # TestResult.stopTest(self, test) pass def selftest(): from unittest import TestLoader, TestSuite import bzrlib import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox from doctest import DocTestSuite import os import shutil import time _setup_test_log() _setup_test_dir() suite = TestSuite() tl = TestLoader() for m in bzrlib.selftest.whitebox, : suite.addTest(tl.loadTestsFromModule(m)) suite.addTest(bzrlib.selftest.blackbox.suite()) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands: suite.addTest(DocTestSuite(m)) result = _MyResult() suite.run(result) _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os log_filename = os.path.abspath('testbzr.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "bzr tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_DIR = os.path.abspath("testbzr.tmp") print '%-30s %s' % ('running tests in', TestBase.TEST_DIR) if os.path.exists(TestBase.TEST_DIR): shutil.rmtree(TestBase.TEST_DIR) os.mkdir(TestBase.TEST_DIR) os.chdir(TestBase.TEST_DIR) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_DIR, '.bzr')) def _show_results(result): for case, tb in result.errors: _show_test_failure('ERROR', case, tb) for case, tb in result.failures: _show_test_failure('FAILURE', case, tb) print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, tb): print (kind + '! ').ljust(60, '-') print case print tb print ''.ljust(60, '-') M 644 inline bzrlib/selftest/blackbox.py data 2569 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Black-box tests for bzr. These check that it behaves properly when it's invoked through the regular command-line interface. This always reinvokes bzr through a new Python interpreter, which is a bit inefficient but arguably tests in a way more representative of how it's normally invoked. """ # this code was previously in testbzr from unittest import TestCase from bzrlib.selftest import TestBase class TestVersion(TestBase): def runTest(self): # output is intentionally passed through to stdout so that we # can see the version being tested print self.runcmd(['bzr', 'version']) print class HelpCommands(TestBase): def runTest(self): self.runcmd('bzr --help') self.runcmd('bzr help') self.runcmd('bzr help commands') self.runcmd('bzr help help') self.runcmd('bzr commit -h') class InTempBranch(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.branch_dir = os.path.join(self.TEST_DIR, self.__class__.__name__) os.mkdir(self.branch_dir) os.chdir(self.branch_dir) def tearDown(self): import os os.chdir(self.TEST_DIR) class InitBranch(InTempBranch): def runTest(self): import os print "%s running in %s" % (self, os.getcwdu()) self.runcmd(['bzr', 'init']) # lists all tests from this module in the best order to run them. we # do it this way rather than just discovering them all because it # allows us to test more basic functions first where failures will be # easiest to understand. def suite(): from unittest import TestSuite s = TestSuite() s.addTests([TestVersion(), InitBranch(), HelpCommands()]) return s M 644 inline testbzr data 14775 #! /usr/bin/python # -*- coding: utf-8 -*- # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. usage: testbzr [-p PYTHON] [BZR] By default this tests the copy of bzr found in the same directory as testbzr, or the first one found on the $PATH. A copy of bzr may be given on the command line to override this, for example when applying a new test suite to an old copy of bzr or vice versa. testbzr normally invokes bzr using the same version of python as it would normally use to run -- that is, the system default python, unless that is older than 2.3. The -p option allows specification of a different Python interpreter, such as when testing that bzr still works on python2.3. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" # we always invoke bzr as 'python bzr' (or e.g. 'python2.3 bzr') # partly so as to cope if the bzr binary is not marked executable OVERRIDE_PYTHON = 'python' LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) def formcmd(cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = BZRPATH if OVERRIDE_PYTHON: cmd.insert(0, OVERRIDE_PYTHON) logfile.write('$ %r\n' % cmd) return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) start_dir = os.getcwd() logfile = open(LOGFILENAME, 'wt', buffering=1) def test_plugins(): """Run a test involving creating a plugin to load, and making sure it is seen properly. """ mkdir('plugin_test') f = open(os.path.join('plugin_test', 'myplug.py'), 'wb') f.write("""import bzrlib, bzrlib.commands class cmd_myplug(bzrlib.commands.Command): '''Just a simple test plugin.''' aliases = ['mplg'] def run(self): print 'Hello from my plugin' """) f.close() os.environ['BZRPLUGINPATH'] = os.path.abspath('plugin_test') help = backtick('bzr help commands') assert help.find('myplug') != -1 assert help.find('Just a simple test plugin.') != -1 assert backtick('bzr myplug') == 'Hello from my plugin\n' assert backtick('bzr mplg') == 'Hello from my plugin\n' f = open(os.path.join('plugin_test', 'override.py'), 'wb') f.write("""import bzrlib, bzrlib.commands class cmd_commit(bzrlib.commands.cmd_commit): '''Commit changes into a new revision.''' def run(self, *args, **kwargs): print "I'm sorry dave, you can't do that" class cmd_help(bzrlib.commands.cmd_help): '''Show help on a command or other topic.''' def run(self, *args, **kwargs): print "You have been overridden" bzrlib.commands.cmd_help.run(self, *args, **kwargs) """) f.close() newhelp = backtick('bzr help commands') assert newhelp.startswith('You have been overridden\n') # We added a line, but the rest should work assert newhelp[25:] == help assert backtick('bzr commit -m test') == "I'm sorry dave, you can't do that\n" shutil.rmtree('plugin_test') try: from getopt import getopt opts, args = getopt(sys.argv[1:], 'p:') for option, value in opts: if option == '-p': OVERRIDE_PYTHON = value mypath = os.path.abspath(sys.argv[0]) print '%-30s %s' % ('running tests from', mypath) global BZRPATH if args: BZRPATH = args[0] else: BZRPATH = os.path.join(os.path.split(mypath)[0], 'bzr') print '%-30s %s' % ('against bzr', BZRPATH) print '%-30s %s' % ('in directory', os.getcwd()) print '%-30s %s' % ('with python', (OVERRIDE_PYTHON or '(default)')) print print backtick('bzr version') runcmd(['mkdir', TESTDIR]) cd(TESTDIR) test_root = os.getcwd() progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("internal tests") runcmd("bzr selftest") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) runcmd("bzr diff --message foo", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == 'unknown:\n test.txt\n' out = backtick("bzr status --all") assert out == "unknown:\n test.txt\n" out = backtick("bzr status test.txt --all") assert out == "unknown:\n test.txt\n" f = file('test2.txt', 'wt') f.write('goodbye cruel world...\n') f.close() out = backtick("bzr status test.txt") assert out == "unknown:\n test.txt\n" out = backtick("bzr status") assert out == ("unknown:\n" " test.txt\n" " test2.txt\n") os.unlink('test2.txt') progress("command aliases") out = backtick("bzr st --all") assert out == ("unknown:\n" " test.txt\n") out = backtick("bzr stat") assert out == ("unknown:\n" " test.txt\n") progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == ("added:\n" " test.txt\n") progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == os.path.join("sub2", "hello.txt\n") assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') assert backtick('bzr relpath sub2/hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') runcmd('bzr log') runcmd('bzr log -v') progress("file with spaces in name") mkdir('sub directory') file('sub directory/file with spaces ', 'wt').write('see how this works\n') runcmd('bzr add .') runcmd('bzr diff') runcmd('bzr commit -m add-spaces') runcmd('bzr check') runcmd('bzr log') runcmd('bzr log --forward') runcmd('bzr info') cd('..') cd('..') progress('branch') # Can't create a branch if it already exists runcmd('bzr branch branch1', retcode=1) # Can't create a branch if its parent doesn't exist runcmd('bzr branch /unlikely/to/exist', retcode=1) runcmd('bzr branch branch1 branch2') progress("pull") cd('branch1') runcmd('bzr pull', retcode=1) runcmd('bzr pull ../branch2') cd('.bzr') runcmd('bzr pull') runcmd('bzr commit -m empty') runcmd('bzr pull') cd('../../branch2') runcmd('bzr pull') runcmd('bzr commit -m empty') cd('../branch1') runcmd('bzr commit -m empty') runcmd('bzr pull', retcode=1) cd ('..') progress('status after remove') mkdir('status-after-remove') # see mail from William Dodé, 2005-05-25 # $ bzr init; touch a; bzr add a; bzr commit -m "add a" # * looking for changes... # added a # * commited r1 # $ bzr remove a # $ bzr status # bzr: local variable 'kind' referenced before assignment # at /vrac/python/bazaar-ng/bzrlib/diff.py:286 in compare_trees() # see ~/.bzr.log for debug information cd('status-after-remove') runcmd('bzr init') file('a', 'w').write('foo') runcmd('bzr add a') runcmd(['bzr', 'commit', '-m', 'add a']) runcmd('bzr remove a') runcmd('bzr status') cd('..') progress('ignore patterns') mkdir('ignorebranch') cd('ignorebranch') runcmd('bzr init') assert backtick('bzr unknowns') == '' file('foo.tmp', 'wt').write('tmp files are ignored') assert backtick('bzr unknowns') == '' file('foo.c', 'wt').write('int main() {}') assert backtick('bzr unknowns') == 'foo.c\n' runcmd('bzr add foo.c') assert backtick('bzr unknowns') == '' # 'ignore' works when creating the .bzignore file file('foo.blah', 'wt').write('blah') assert backtick('bzr unknowns') == 'foo.blah\n' runcmd('bzr ignore *.blah') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\n' # 'ignore' works when then .bzrignore file already exists file('garh', 'wt').write('garh') assert backtick('bzr unknowns') == 'garh\n' runcmd('bzr ignore garh') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\ngarh\n' cd('..') progress("recursive and non-recursive add") mkdir('no-recurse') cd('no-recurse') runcmd('bzr init') mkdir('foo') fp = os.path.join('foo', 'test.txt') f = file(fp, 'w') f.write('hello!\n') f.close() runcmd('bzr add --no-recurse foo') runcmd('bzr file-id foo') runcmd('bzr file-id ' + fp, 1) # not versioned yet runcmd('bzr commit -m add-dir-only') runcmd('bzr file-id ' + fp, 1) # still not versioned runcmd('bzr add foo') runcmd('bzr file-id ' + fp) runcmd('bzr commit -m add-sub-file') cd('..') # Run any function in this g = globals() funcs = g.keys() funcs.sort() for k in funcs: if k.startswith('test_') and callable(g[k]): progress(k[5:].replace('_', ' ')) g[k]() progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) logfile.close() sys.stdout.writelines(file(os.path.join(start_dir, LOGFILENAME), 'rt').readlines()[-50:]) sys.exit(1) commit refs/heads/master mark :728 committer Martin Pool 1119343434 +1000 data 28 - warning not to use testbzr from :727 M 644 inline testbzr data 14843 #! /usr/bin/python # -*- coding: utf-8 -*- # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA print 'please use "bzr selftest" instead' import sys sys.exit(1) """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. usage: testbzr [-p PYTHON] [BZR] By default this tests the copy of bzr found in the same directory as testbzr, or the first one found on the $PATH. A copy of bzr may be given on the command line to override this, for example when applying a new test suite to an old copy of bzr or vice versa. testbzr normally invokes bzr using the same version of python as it would normally use to run -- that is, the system default python, unless that is older than 2.3. The -p option allows specification of a different Python interpreter, such as when testing that bzr still works on python2.3. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" # we always invoke bzr as 'python bzr' (or e.g. 'python2.3 bzr') # partly so as to cope if the bzr binary is not marked executable OVERRIDE_PYTHON = 'python' LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) def formcmd(cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = BZRPATH if OVERRIDE_PYTHON: cmd.insert(0, OVERRIDE_PYTHON) logfile.write('$ %r\n' % cmd) return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) start_dir = os.getcwd() logfile = open(LOGFILENAME, 'wt', buffering=1) def test_plugins(): """Run a test involving creating a plugin to load, and making sure it is seen properly. """ mkdir('plugin_test') f = open(os.path.join('plugin_test', 'myplug.py'), 'wb') f.write("""import bzrlib, bzrlib.commands class cmd_myplug(bzrlib.commands.Command): '''Just a simple test plugin.''' aliases = ['mplg'] def run(self): print 'Hello from my plugin' """) f.close() os.environ['BZRPLUGINPATH'] = os.path.abspath('plugin_test') help = backtick('bzr help commands') assert help.find('myplug') != -1 assert help.find('Just a simple test plugin.') != -1 assert backtick('bzr myplug') == 'Hello from my plugin\n' assert backtick('bzr mplg') == 'Hello from my plugin\n' f = open(os.path.join('plugin_test', 'override.py'), 'wb') f.write("""import bzrlib, bzrlib.commands class cmd_commit(bzrlib.commands.cmd_commit): '''Commit changes into a new revision.''' def run(self, *args, **kwargs): print "I'm sorry dave, you can't do that" class cmd_help(bzrlib.commands.cmd_help): '''Show help on a command or other topic.''' def run(self, *args, **kwargs): print "You have been overridden" bzrlib.commands.cmd_help.run(self, *args, **kwargs) """) f.close() newhelp = backtick('bzr help commands') assert newhelp.startswith('You have been overridden\n') # We added a line, but the rest should work assert newhelp[25:] == help assert backtick('bzr commit -m test') == "I'm sorry dave, you can't do that\n" shutil.rmtree('plugin_test') try: from getopt import getopt opts, args = getopt(sys.argv[1:], 'p:') for option, value in opts: if option == '-p': OVERRIDE_PYTHON = value mypath = os.path.abspath(sys.argv[0]) print '%-30s %s' % ('running tests from', mypath) global BZRPATH if args: BZRPATH = args[0] else: BZRPATH = os.path.join(os.path.split(mypath)[0], 'bzr') print '%-30s %s' % ('against bzr', BZRPATH) print '%-30s %s' % ('in directory', os.getcwd()) print '%-30s %s' % ('with python', (OVERRIDE_PYTHON or '(default)')) print print backtick('bzr version') runcmd(['mkdir', TESTDIR]) cd(TESTDIR) test_root = os.getcwd() progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("internal tests") runcmd("bzr selftest") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) runcmd("bzr diff --message foo", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == 'unknown:\n test.txt\n' out = backtick("bzr status --all") assert out == "unknown:\n test.txt\n" out = backtick("bzr status test.txt --all") assert out == "unknown:\n test.txt\n" f = file('test2.txt', 'wt') f.write('goodbye cruel world...\n') f.close() out = backtick("bzr status test.txt") assert out == "unknown:\n test.txt\n" out = backtick("bzr status") assert out == ("unknown:\n" " test.txt\n" " test2.txt\n") os.unlink('test2.txt') progress("command aliases") out = backtick("bzr st --all") assert out == ("unknown:\n" " test.txt\n") out = backtick("bzr stat") assert out == ("unknown:\n" " test.txt\n") progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == ("added:\n" " test.txt\n") progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == os.path.join("sub2", "hello.txt\n") assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') assert backtick('bzr relpath sub2/hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') runcmd('bzr log') runcmd('bzr log -v') progress("file with spaces in name") mkdir('sub directory') file('sub directory/file with spaces ', 'wt').write('see how this works\n') runcmd('bzr add .') runcmd('bzr diff') runcmd('bzr commit -m add-spaces') runcmd('bzr check') runcmd('bzr log') runcmd('bzr log --forward') runcmd('bzr info') cd('..') cd('..') progress('branch') # Can't create a branch if it already exists runcmd('bzr branch branch1', retcode=1) # Can't create a branch if its parent doesn't exist runcmd('bzr branch /unlikely/to/exist', retcode=1) runcmd('bzr branch branch1 branch2') progress("pull") cd('branch1') runcmd('bzr pull', retcode=1) runcmd('bzr pull ../branch2') cd('.bzr') runcmd('bzr pull') runcmd('bzr commit -m empty') runcmd('bzr pull') cd('../../branch2') runcmd('bzr pull') runcmd('bzr commit -m empty') cd('../branch1') runcmd('bzr commit -m empty') runcmd('bzr pull', retcode=1) cd ('..') progress('status after remove') mkdir('status-after-remove') # see mail from William Dodé, 2005-05-25 # $ bzr init; touch a; bzr add a; bzr commit -m "add a" # * looking for changes... # added a # * commited r1 # $ bzr remove a # $ bzr status # bzr: local variable 'kind' referenced before assignment # at /vrac/python/bazaar-ng/bzrlib/diff.py:286 in compare_trees() # see ~/.bzr.log for debug information cd('status-after-remove') runcmd('bzr init') file('a', 'w').write('foo') runcmd('bzr add a') runcmd(['bzr', 'commit', '-m', 'add a']) runcmd('bzr remove a') runcmd('bzr status') cd('..') progress('ignore patterns') mkdir('ignorebranch') cd('ignorebranch') runcmd('bzr init') assert backtick('bzr unknowns') == '' file('foo.tmp', 'wt').write('tmp files are ignored') assert backtick('bzr unknowns') == '' file('foo.c', 'wt').write('int main() {}') assert backtick('bzr unknowns') == 'foo.c\n' runcmd('bzr add foo.c') assert backtick('bzr unknowns') == '' # 'ignore' works when creating the .bzignore file file('foo.blah', 'wt').write('blah') assert backtick('bzr unknowns') == 'foo.blah\n' runcmd('bzr ignore *.blah') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\n' # 'ignore' works when then .bzrignore file already exists file('garh', 'wt').write('garh') assert backtick('bzr unknowns') == 'garh\n' runcmd('bzr ignore garh') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\ngarh\n' cd('..') progress("recursive and non-recursive add") mkdir('no-recurse') cd('no-recurse') runcmd('bzr init') mkdir('foo') fp = os.path.join('foo', 'test.txt') f = file(fp, 'w') f.write('hello!\n') f.close() runcmd('bzr add --no-recurse foo') runcmd('bzr file-id foo') runcmd('bzr file-id ' + fp, 1) # not versioned yet runcmd('bzr commit -m add-dir-only') runcmd('bzr file-id ' + fp, 1) # still not versioned runcmd('bzr add foo') runcmd('bzr file-id ' + fp) runcmd('bzr commit -m add-sub-file') cd('..') # Run any function in this g = globals() funcs = g.keys() funcs.sort() for k in funcs: if k.startswith('test_') and callable(g[k]): progress(k[5:].replace('_', ' ')) g[k]() progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) logfile.close() sys.stdout.writelines(file(os.path.join(start_dir, LOGFILENAME), 'rt').readlines()[-50:]) sys.exit(1) commit refs/heads/master mark :729 committer Martin Pool 1119401468 +1000 data 52 - pull shows location being used patch from aaron from :728 M 644 inline bzrlib/commands.py data 49796 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _find_plugins(): """Find all python files which are plugins, and load their commands to add to the list of "all commands" The environment variable BZRPATH is considered a delimited set of paths to look through. Each entry is searched for *.py files. If a directory is found, it is also searched, but they are not searched recursively. This allows you to revctl the plugins. Inside the plugin should be a series of cmd_* function, which inherit from the bzrlib.commands.Command class. """ bzrpath = os.environ.get('BZRPLUGINPATH', '') plugin_cmds = {} if not bzrpath: return plugin_cmds _platform_extensions = { 'win32':'.pyd', 'cygwin':'.dll', 'darwin':'.dylib', 'linux2':'.so' } if _platform_extensions.has_key(sys.platform): platform_extension = _platform_extensions[sys.platform] else: platform_extension = None for d in bzrpath.split(os.pathsep): plugin_names = {} # This should really be a set rather than a dict for f in os.listdir(d): if f.endswith('.py'): f = f[:-3] elif f.endswith('.pyc') or f.endswith('.pyo'): f = f[:-4] elif platform_extension and f.endswith(platform_extension): f = f[:-len(platform_extension)] if f.endswidth('module'): f = f[:-len('module')] else: continue if not plugin_names.has_key(f): plugin_names[f] = True plugin_names = plugin_names.keys() plugin_names.sort() try: sys.path.insert(0, d) for name in plugin_names: try: old_module = None try: if sys.modules.has_key(name): old_module = sys.modules[name] del sys.modules[name] plugin = __import__(name, locals()) for k in dir(plugin): if k.startswith('cmd_'): k_unsquished = _unsquish_command_name(k) if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = getattr(plugin, k) else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r in dir %r' % (name, d)) finally: if old_module: sys.modules[name] = old_module except ImportError, e: log_error('Unable to load plugin: %r from %r\n%s' % (name, d, e)) finally: sys.path.pop(0) return plugin_cmds def _get_cmd_dict(include_plugins=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v if include_plugins: d.update(_find_plugins()) return d def get_all_cmds(include_plugins=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(include_plugins=include_plugins).iteritems(): yield k,v def get_cmd_class(cmd,include_plugins=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(include_plugins=include_plugins) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() for opt in self.takes_options: if not opt in OPTIONS: raise BzrError("Unknown option '%s' returned by external command %s" % (opt, path)) # TODO: Is there any way to check takes_args is valid here? self.takes_args = pipe.readline().split() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-usage'" % path) pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-help'" % path) def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: optname = name.replace('_','-') value = kargs[name] if OPTIONS.has_key(optname): # it's an option opts.append('--%s' % optname) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import errno br_to = Branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if errno == errno.ENOENT: raise if location is None: if stored_loc is None: raise BzrCommandError("No pull location known or specified.") else: print "Using last location: %s" % stored_loc location = stored_loc from branch import find_branch, DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged. Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. To retrieve the branch as of a particular revision, supply the --revision parameter, as in "branch foo/bar -r 5". """ takes_args = ['from_location', 'to_location?'] takes_options = ['revision'] def run(self, from_location, to_location=None, revision=None): import errno from bzrlib.merge import merge from branch import find_branch, DivergedBranches, NoSuchRevision from shutil import rmtree try: br_from = find_branch(from_location) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location.rstrip("/\\")) try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) try: br_to.update_revisions(br_from, stop_revision=revision) except NoSuchRevision: rmtree(to_location) msg = "The branch %s has no revision %d." % (from_location, revision) raise BzrCommandError(msg) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False, ignore_zero=True) from_location = pull_loc(br_from) br_to.controlfile("x-pull", "wb").write(from_location + "\n") def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: raise BzrError("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: raise BzrError("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, exports to a directory (equivalent to --format=dir).""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format'] def run(self, dest, revision=None, format='dir'): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest, format) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_upgrade(Command): """Upgrade branch storage to current format. This should normally be used only after the check command tells you to run it. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.upgrade import upgrade upgrade(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest return int(not selftest()) class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'update': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) >>> parse_args('log -r 500'.split()) (['log'], {'revision': 500}) >>> parse_args('log -r500:600'.split()) (['log'], {'revision': [500, 600]}) >>> parse_args('log -vr500:600'.split()) (['log'], {'verbose': True, 'revision': [500, 600]}) >>> parse_args('log -rv500:600'.split()) #the r takes an argument Traceback (most recent call last): ... ValueError: invalid literal for int(): v500 """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: raise BzrError('unknown long option %r' % a) else: shortopt = a[1:] if shortopt in SHORT_OPTIONS: # Multi-character options must have a space to delimit # their value optname = SHORT_OPTIONS[shortopt] else: # Single character short options, can be chained, # and have their value appended to their name shortopt = a[1:2] if shortopt not in SHORT_OPTIONS: # We didn't find the multi-character name, and we # didn't find the single char name raise BzrError('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if a[2:]: # There are extra things on this option # see if it is the value, or if it is another # short option optargfn = OPTIONS[optname] if optargfn is None: # This option does not take an argument, so the # next entry is another short option, pack it back # into the list argv.insert(0, '-' + a[2:]) else: # This option takes an argument, so pack it # into the array optarg = a[2:] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? raise BzrError('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: raise BzrError('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: raise BzrError('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] include_plugins=True try: args, opts = parse_args(argv[1:]) if 'help' in opts: import help if args: help.help(args[0]) else: help.help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 canonical_cmd, cmd_class = get_cmd_class(cmd,include_plugins=include_plugins) # global option if 'profile' in opts: profile = True del opts['profile'] else: profile = False # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :730 committer Martin Pool 1119409797 +1000 data 44 - add help, try, apply options to pwk script from :729 M 644 inline contrib/pwk data 952 #! /bin/sh -pe # take patches from patchwork into bzr # authentication must be in ~/.netrc # TODO: Scan all pending patches and say which ones apply cleanly. # these should be moved into some kind of per-project configuration PWK_ROOT='http://patchwork.ozlabs.org/bazaar-ng' PWK_AUTH_ROOT='https://patchwork.ozlabs.org/bazaar-ng' # bzr uses -p0 style; others use -p1 PATCH_OPTS='-p0' usage() { cat < 1119420283 +1000 data 30 - merge plugin patch from john from :730 M 644 inline bzrlib/plugin.py data 3813 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # This module implements plug-in support. # Any python module in $BZR_PLUGIN_PATH will be imported upon initialization # of bzrlib (and then forgotten about). In the plugin's main body, it should # update any bzrlib registries it wants to extend; for example, to add new # commands, import bzrlib.commands and add your new command to the # plugin_cmds variable. import sys, os, imp try: set except NameError: from sets import Set as set from bzrlib.trace import log_error def load_plugins(): """Find all python files which are plugins, and load them The environment variable BZR_PLUGIN_PATH is considered a delimited set of paths to look through. Each entry is searched for *.py files (and whatever other extensions are used in the platform, such as *.pyd). """ bzrpath = os.environ.get('BZR_PLUGIN_PATH', os.path.expanduser('~/.bzr/plugins')) # The problem with imp.get_suffixes() is that it doesn't include # .pyo which is technically valid # It also means that "testmodule.so" will show up as both test and testmodule # though it is only valid as 'test' # but you should be careful, because "testmodule.py" loads as testmodule. suffixes = imp.get_suffixes() suffixes.append(('.pyo', 'rb', imp.PY_COMPILED)) package_entries = ['__init__.py', '__init__.pyc', '__init__.pyo'] for d in bzrpath.split(os.pathsep): # going trough them one by one allows different plugins with the same # filename in different directories in the path if not d: continue plugin_names = set() if not os.path.isdir(d): continue for f in os.listdir(d): path = os.path.join(d, f) if os.path.isdir(path): for entry in package_entries: # This directory should be a package, and thus added to # the list if os.path.isfile(os.path.join(path, entry)): break else: # This directory is not a package continue else: for suffix_info in suffixes: if f.endswith(suffix_info[0]): f = f[:-len(suffix_info[0])] if suffix_info[2] == imp.C_EXTENSION and f.endswith('module'): f = f[:-len('module')] break else: continue plugin_names.add(f) plugin_names = list(plugin_names) plugin_names.sort() for name in plugin_names: try: plugin_info = imp.find_module(name, [d]) try: plugin = imp.load_module('bzrlib.plugin.' + name, *plugin_info) finally: if plugin_info[0] is not None: plugin_info[0].close() except Exception, e: log_error('Unable to load plugin: %r from %r\n%s' % (name, d, e)) M 644 inline bzrlib/__init__.py data 2161 # (C) 2005 Canonical Development Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr library""" from inventory import Inventory, InventoryEntry from branch import Branch, ScratchBranch, find_branch from osutils import format_date from tree import Tree from diff import compare_trees from trace import mutter, warning, open_tracefile from log import show_log from plugin import load_plugins import add BZRDIR = ".bzr" DEFAULT_IGNORE = ['.bzr.log', '*~', '#*#', '*$', '.#*', '.*.sw[nop]', '.*.tmp', '*.tmp', '*.bak', '*.BAK', '*.orig', '*.o', '*.obj', '*.a', '*.py[oc]', '*.so', '*.exe', '*.elc', '{arch}', 'CVS', 'CVS.adm', '.svn', '_darcs', 'SCCS', 'RCS', '*,v', 'BitKeeper', '.git', 'TAGS', '.make.state', '.sconsign', '.tmp*', '.del-*'] IGNORE_FILENAME = ".bzrignore" import locale user_encoding = locale.getpreferredencoding() del locale __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __version__ = '0.0.5' def get_bzr_revision(): """If bzr is run from a branch, return (revno,revid) or None""" from errors import BzrError try: branch = Branch(__path__[0]) rh = branch.revision_history() if rh: return len(rh), rh[-1] else: return None except BzrError: return None M 644 inline bzrlib/commands.py data 50864 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date plugin_cmds = {} def register_plugin_command(cmd): "Utility function to help register a command" global plugin_cmds k = cmd.__name__ if k.startswith("cmd_"): k_unsquished = _unsquish_command_name(k) else: k_unsquished = k if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = cmd else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r' % sys.modules[cmd.__module__]) def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _get_cmd_dict(plugins_override=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v # If we didn't load plugins, the plugin_cmds dict will be empty if plugins_override: d.update(plugin_cmds) else: d2 = plugin_cmds.copy() d2.update(d) d = d2 return d def get_all_cmds(plugins_override=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(plugins_override=plugins_override).iteritems(): yield k,v def get_cmd_class(cmd, plugins_override=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(plugins_override=plugins_override) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() for opt in self.takes_options: if not opt in OPTIONS: raise BzrError("Unknown option '%s' returned by external command %s" % (opt, path)) # TODO: Is there any way to check takes_args is valid here? self.takes_args = pipe.readline().split() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-usage'" % path) pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-help'" % path) def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: optname = name.replace('_','-') value = kargs[name] if OPTIONS.has_key(optname): # it's an option opts.append('--%s' % optname) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import errno br_to = Branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if errno == errno.ENOENT: raise if location is None: if stored_loc is None: raise BzrCommandError("No pull location known or specified.") else: print "Using last location: %s" % stored_loc location = stored_loc from branch import find_branch, DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged. Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. To retrieve the branch as of a particular revision, supply the --revision parameter, as in "branch foo/bar -r 5". """ takes_args = ['from_location', 'to_location?'] takes_options = ['revision'] def run(self, from_location, to_location=None, revision=None): import errno from bzrlib.merge import merge from branch import find_branch, DivergedBranches, NoSuchRevision from shutil import rmtree try: br_from = find_branch(from_location) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location.rstrip("/\\")) try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) try: br_to.update_revisions(br_from, stop_revision=revision) except NoSuchRevision: rmtree(to_location) msg = "The branch %s has no revision %d." % (from_location, revision) raise BzrCommandError(msg) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False, ignore_zero=True) from_location = pull_loc(br_from) br_to.controlfile("x-pull", "wb").write(from_location + "\n") def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: raise BzrError("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: raise BzrError("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, exports to a directory (equivalent to --format=dir).""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format'] def run(self, dest, revision=None, format='dir'): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest, format) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_upgrade(Command): """Upgrade branch storage to current format. This should normally be used only after the check command tells you to run it. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.upgrade import upgrade upgrade(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest return int(not selftest()) class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'update': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) >>> parse_args('log -r 500'.split()) (['log'], {'revision': 500}) >>> parse_args('log -r500:600'.split()) (['log'], {'revision': [500, 600]}) >>> parse_args('log -vr500:600'.split()) (['log'], {'verbose': True, 'revision': [500, 600]}) >>> parse_args('log -rv500:600'.split()) #the r takes an argument Traceback (most recent call last): ... ValueError: invalid literal for int(): v500 """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: raise BzrError('unknown long option %r' % a) else: shortopt = a[1:] if shortopt in SHORT_OPTIONS: # Multi-character options must have a space to delimit # their value optname = SHORT_OPTIONS[shortopt] else: # Single character short options, can be chained, # and have their value appended to their name shortopt = a[1:2] if shortopt not in SHORT_OPTIONS: # We didn't find the multi-character name, and we # didn't find the single char name raise BzrError('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if a[2:]: # There are extra things on this option # see if it is the value, or if it is another # short option optargfn = OPTIONS[optname] if optargfn is None: # This option does not take an argument, so the # next entry is another short option, pack it back # into the list argv.insert(0, '-' + a[2:]) else: # This option takes an argument, so pack it # into the array optarg = a[2:] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? raise BzrError('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: raise BzrError('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: raise BzrError('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def _parse_master_args(argv): """Parse the arguments that always go with the original command. These are things like bzr --no-plugins, etc. There are now 2 types of option flags. Ones that come *before* the command, and ones that come *after* the command. Ones coming *before* the command are applied against all possible commands. And are generally applied before plugins are loaded. The current list are: --builtin Allow plugins to load, but don't let them override builtin commands, they will still be allowed if they do not override a builtin. --no-plugins Don't load any plugins. This lets you get back to official source behavior. --profile Enable the hotspot profile before running the command. For backwards compatibility, this is also a non-master option. --version Spit out the version of bzr that is running and exit. This is also a non-master option. --help Run help and exit, also a non-master option (I think that should stay, though) >>> argv, opts = _parse_master_args(['bzr', '--test']) Traceback (most recent call last): ... BzrCommandError: Invalid master option: 'test' >>> argv, opts = _parse_master_args(['bzr', '--version', 'command']) >>> print argv ['command'] >>> print opts['version'] True >>> argv, opts = _parse_master_args(['bzr', '--profile', 'command', '--more-options']) >>> print argv ['command', '--more-options'] >>> print opts['profile'] True >>> argv, opts = _parse_master_args(['bzr', '--no-plugins', 'command']) >>> print argv ['command'] >>> print opts['no-plugins'] True >>> print opts['profile'] False >>> argv, opts = _parse_master_args(['bzr', 'command', '--profile']) >>> print argv ['command', '--profile'] >>> print opts['profile'] False """ master_opts = {'builtin':False, 'no-plugins':False, 'version':False, 'profile':False, 'help':False } # This is the point where we could hook into argv[0] to determine # what front-end is supposed to be run # For now, we are just ignoring it. cmd_name = argv.pop(0) for arg in argv[:]: if arg[:2] != '--': # at the first non-option, we return the rest break arg = arg[2:] # Remove '--' if arg not in master_opts: # We could say that this is not an error, that we should # just let it be handled by the main section instead raise BzrCommandError('Invalid master option: %r' % arg) argv.pop(0) # We are consuming this entry master_opts[arg] = True return argv, master_opts def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: # some options like --builtin and --no-plugins have special effects argv, master_opts = _parse_master_args(argv) if 'no-plugins' not in master_opts: bzrlib.load_plugins() args, opts = parse_args(argv) if master_opts['help']: from bzrlib.help import help if argv: help(argv[0]) else: help() return 0 if 'help' in opts: from bzrlib.help import help if args: help(args[0]) else: help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 plugins_override = not (master_opts['builtin']) canonical_cmd, cmd_class = get_cmd_class(cmd, plugins_override=plugins_override) profile = master_opts['profile'] # For backwards compatibility, I would rather stick with --profile being a # master/global option if 'profile' in opts: profile = True del opts['profile'] # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline testbzr data 15042 #! /usr/bin/python # -*- coding: utf-8 -*- # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA print 'please use "bzr selftest" instead' import sys sys.exit(1) """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. usage: testbzr [-p PYTHON] [BZR] By default this tests the copy of bzr found in the same directory as testbzr, or the first one found on the $PATH. A copy of bzr may be given on the command line to override this, for example when applying a new test suite to an old copy of bzr or vice versa. testbzr normally invokes bzr using the same version of python as it would normally use to run -- that is, the system default python, unless that is older than 2.3. The -p option allows specification of a different Python interpreter, such as when testing that bzr still works on python2.3. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" # we always invoke bzr as 'python bzr' (or e.g. 'python2.3 bzr') # partly so as to cope if the bzr binary is not marked executable OVERRIDE_PYTHON = 'python' LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) def formcmd(cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = BZRPATH if OVERRIDE_PYTHON: cmd.insert(0, OVERRIDE_PYTHON) logfile.write('$ %r\n' % cmd) return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) start_dir = os.getcwd() logfile = open(LOGFILENAME, 'wt', buffering=1) def test_plugins(): """Run a test involving creating a plugin to load, and making sure it is seen properly. """ orig_help = backtick('bzr help commands') # No plugins yet mkdir('plugin_test') f = open(os.path.join('plugin_test', 'myplug.py'), 'wb') f.write("""import bzrlib, bzrlib.commands class cmd_myplug(bzrlib.commands.Command): '''Just a simple test plugin.''' aliases = ['mplg'] def run(self): print 'Hello from my plugin' """) f.close() os.environ['BZRPLUGINPATH'] = os.path.abspath('plugin_test') help = backtick('bzr help commands') assert help.find('myplug') != -1 assert help.find('Just a simple test plugin.') != -1 assert backtick('bzr myplug') == 'Hello from my plugin\n' assert backtick('bzr mplg') == 'Hello from my plugin\n' f = open(os.path.join('plugin_test', 'override.py'), 'wb') f.write("""import bzrlib, bzrlib.commands class cmd_commit(bzrlib.commands.cmd_commit): '''Commit changes into a new revision.''' def run(self, *args, **kwargs): print "I'm sorry dave, you can't do that" class cmd_help(bzrlib.commands.cmd_help): '''Show help on a command or other topic.''' def run(self, *args, **kwargs): print "You have been overridden" bzrlib.commands.cmd_help.run(self, *args, **kwargs) """) f.close() newhelp = backtick('bzr help commands') assert newhelp.startswith('You have been overridden\n') # We added a line, but the rest should work assert newhelp[25:] == help assert backtick('bzr commit -m test') == "I'm sorry dave, you can't do that\n" shutil.rmtree('plugin_test') try: from getopt import getopt opts, args = getopt(sys.argv[1:], 'p:') for option, value in opts: if option == '-p': OVERRIDE_PYTHON = value mypath = os.path.abspath(sys.argv[0]) print '%-30s %s' % ('running tests from', mypath) global BZRPATH if args: BZRPATH = args[0] else: BZRPATH = os.path.join(os.path.split(mypath)[0], 'bzr') print '%-30s %s' % ('against bzr', BZRPATH) print '%-30s %s' % ('in directory', os.getcwd()) print '%-30s %s' % ('with python', (OVERRIDE_PYTHON or '(default)')) print print backtick('bzr version') runcmd(['mkdir', TESTDIR]) cd(TESTDIR) # This means that any command that is naively run in this directory # Won't affect the parent directory. runcmd('bzr init') test_root = os.getcwd() progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("internal tests") runcmd("bzr selftest") progress("user identity") # this should always identify something, if only "john@localhost" runcmd("bzr whoami") runcmd("bzr whoami --email") assert backtick("bzr whoami --email").count('@') == 1 progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) runcmd("bzr diff --message foo", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == 'unknown:\n test.txt\n' out = backtick("bzr status --all") assert out == "unknown:\n test.txt\n" out = backtick("bzr status test.txt --all") assert out == "unknown:\n test.txt\n" f = file('test2.txt', 'wt') f.write('goodbye cruel world...\n') f.close() out = backtick("bzr status test.txt") assert out == "unknown:\n test.txt\n" out = backtick("bzr status") assert out == ("unknown:\n" " test.txt\n" " test2.txt\n") os.unlink('test2.txt') progress("command aliases") out = backtick("bzr st --all") assert out == ("unknown:\n" " test.txt\n") out = backtick("bzr stat") assert out == ("unknown:\n" " test.txt\n") progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == ("added:\n" " test.txt\n") progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == os.path.join("sub2", "hello.txt\n") assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') assert backtick('bzr relpath sub2/hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') runcmd('bzr log') runcmd('bzr log -v') progress("file with spaces in name") mkdir('sub directory') file('sub directory/file with spaces ', 'wt').write('see how this works\n') runcmd('bzr add .') runcmd('bzr diff') runcmd('bzr commit -m add-spaces') runcmd('bzr check') runcmd('bzr log') runcmd('bzr log --forward') runcmd('bzr info') cd('..') cd('..') progress('branch') # Can't create a branch if it already exists runcmd('bzr branch branch1', retcode=1) # Can't create a branch if its parent doesn't exist runcmd('bzr branch /unlikely/to/exist', retcode=1) runcmd('bzr branch branch1 branch2') progress("pull") cd('branch1') runcmd('bzr pull', retcode=1) runcmd('bzr pull ../branch2') cd('.bzr') runcmd('bzr pull') runcmd('bzr commit -m empty') runcmd('bzr pull') cd('../../branch2') runcmd('bzr pull') runcmd('bzr commit -m empty') cd('../branch1') runcmd('bzr commit -m empty') runcmd('bzr pull', retcode=1) cd ('..') progress('status after remove') mkdir('status-after-remove') # see mail from William Dodé, 2005-05-25 # $ bzr init; touch a; bzr add a; bzr commit -m "add a" # * looking for changes... # added a # * commited r1 # $ bzr remove a # $ bzr status # bzr: local variable 'kind' referenced before assignment # at /vrac/python/bazaar-ng/bzrlib/diff.py:286 in compare_trees() # see ~/.bzr.log for debug information cd('status-after-remove') runcmd('bzr init') file('a', 'w').write('foo') runcmd('bzr add a') runcmd(['bzr', 'commit', '-m', 'add a']) runcmd('bzr remove a') runcmd('bzr status') cd('..') progress('ignore patterns') mkdir('ignorebranch') cd('ignorebranch') runcmd('bzr init') assert backtick('bzr unknowns') == '' file('foo.tmp', 'wt').write('tmp files are ignored') assert backtick('bzr unknowns') == '' file('foo.c', 'wt').write('int main() {}') assert backtick('bzr unknowns') == 'foo.c\n' runcmd('bzr add foo.c') assert backtick('bzr unknowns') == '' # 'ignore' works when creating the .bzignore file file('foo.blah', 'wt').write('blah') assert backtick('bzr unknowns') == 'foo.blah\n' runcmd('bzr ignore *.blah') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\n' # 'ignore' works when then .bzrignore file already exists file('garh', 'wt').write('garh') assert backtick('bzr unknowns') == 'garh\n' runcmd('bzr ignore garh') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\ngarh\n' cd('..') progress("recursive and non-recursive add") mkdir('no-recurse') cd('no-recurse') runcmd('bzr init') mkdir('foo') fp = os.path.join('foo', 'test.txt') f = file(fp, 'w') f.write('hello!\n') f.close() runcmd('bzr add --no-recurse foo') runcmd('bzr file-id foo') runcmd('bzr file-id ' + fp, 1) # not versioned yet runcmd('bzr commit -m add-dir-only') runcmd('bzr file-id ' + fp, 1) # still not versioned runcmd('bzr add foo') runcmd('bzr file-id ' + fp) runcmd('bzr commit -m add-sub-file') cd('..') # Run any function in this g = globals() funcs = g.keys() funcs.sort() for k in funcs: if k.startswith('test_') and callable(g[k]): progress(k[5:].replace('_', ' ')) g[k]() progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) logfile.close() sys.stdout.writelines(file(os.path.join(start_dir, LOGFILENAME), 'rt').readlines()[-50:]) sys.exit(1) commit refs/heads/master mark :732 committer Martin Pool 1119421100 +1000 data 35 - move more tests into bzr selftest from :731 M 644 inline bzrlib/selftest/__init__.py data 5792 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") raise class CommandFailed(Exception): pass class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr, so that we can run it # through a specified Python intepreter OVERRIDE_PYTHON = None # to run with alternative python 'python' BZRPATH = 'bzr' def formcmd(self, cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = self.BZRPATH if self.OVERRIDE_PYTHON: cmd.insert(0, self.OVERRIDE_PYTHON) self.log('$ %r' % cmd) return cmd def runcmd(self, cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = self.formcmd(cmd) self.log('$ ' + ' '.join(cmd)) actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(self, cmd, retcode=0): cmd = self.formcmd(cmd) child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG) outd, errd = child.communicate() self.log(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def log(self, msg): """Log a message to a progress file""" print >>self.TEST_LOG, msg class InTempDir(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.branch_dir = os.path.join(self.TEST_DIR, self.__class__.__name__) os.mkdir(self.branch_dir) os.chdir(self.branch_dir) def tearDown(self): import os os.chdir(self.TEST_DIR) class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ # def startTest(self, test): # print str(test).ljust(50), # TestResult.startTest(self, test) # def stopTest(self, test): # print # TestResult.stopTest(self, test) pass def selftest(): from unittest import TestLoader, TestSuite import bzrlib import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox from doctest import DocTestSuite import os import shutil import time _setup_test_log() _setup_test_dir() suite = TestSuite() tl = TestLoader() for m in bzrlib.selftest.whitebox, : suite.addTest(tl.loadTestsFromModule(m)) suite.addTest(bzrlib.selftest.blackbox.suite()) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands: suite.addTest(DocTestSuite(m)) result = _MyResult() suite.run(result) _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os log_filename = os.path.abspath('testbzr.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "bzr tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_DIR = os.path.abspath("testbzr.tmp") print '%-30s %s' % ('running tests in', TestBase.TEST_DIR) if os.path.exists(TestBase.TEST_DIR): shutil.rmtree(TestBase.TEST_DIR) os.mkdir(TestBase.TEST_DIR) os.chdir(TestBase.TEST_DIR) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_DIR, '.bzr')) def _show_results(result): for case, tb in result.errors: _show_test_failure('ERROR', case, tb) for case, tb in result.failures: _show_test_failure('FAILURE', case, tb) print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, tb): print (kind + '! ').ljust(60, '-') print case print tb print ''.ljust(60, '-') M 644 inline bzrlib/selftest/blackbox.py data 2555 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Black-box tests for bzr. These check that it behaves properly when it's invoked through the regular command-line interface. This always reinvokes bzr through a new Python interpreter, which is a bit inefficient but arguably tests in a way more representative of how it's normally invoked. """ # this code was previously in testbzr from unittest import TestCase from bzrlib.selftest import TestBase, InTempDir class TestVersion(TestBase): def runTest(self): # output is intentionally passed through to stdout so that we # can see the version being tested print self.runcmd(['bzr', 'version']) print class HelpCommands(TestBase): def runTest(self): self.runcmd('bzr --help') self.runcmd('bzr help') self.runcmd('bzr help commands') self.runcmd('bzr help help') self.runcmd('bzr commit -h') class InitBranch(InTempDir): def runTest(self): import os print "%s running in %s" % (self, os.getcwdu()) self.runcmd(['bzr', 'init']) class UserIdentity(InTempDir): def runTest(self): # this should always identify something, if only "john@localhost" self.runcmd("bzr whoami") self.runcmd("bzr whoami --email") self.assertEquals(self.backtick("bzr whoami --email").count('@'), 1) # lists all tests from this module in the best order to run them. we # do it this way rather than just discovering them all because it # allows us to test more basic functions first where failures will be # easiest to understand. def suite(): from unittest import TestSuite s = TestSuite() s.addTests([TestVersion(), InitBranch(), HelpCommands(), UserIdentity()]) return s M 644 inline testbzr data 13154 #! /usr/bin/python # -*- coding: utf-8 -*- # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA print 'please use "bzr selftest" instead' import sys sys.exit(1) """External black-box test for bzr. This always runs bzr as an external process to try to catch bugs related to argument processing, startup, etc. usage: testbzr [-p PYTHON] [BZR] By default this tests the copy of bzr found in the same directory as testbzr, or the first one found on the $PATH. A copy of bzr may be given on the command line to override this, for example when applying a new test suite to an old copy of bzr or vice versa. testbzr normally invokes bzr using the same version of python as it would normally use to run -- that is, the system default python, unless that is older than 2.3. The -p option allows specification of a different Python interpreter, such as when testing that bzr still works on python2.3. This replaces the previous test.sh which was not very portable.""" import sys, os, traceback from os import mkdir from os.path import exists TESTDIR = "testbzr.tmp" # we always invoke bzr as 'python bzr' (or e.g. 'python2.3 bzr') # partly so as to cope if the bzr binary is not marked executable OVERRIDE_PYTHON = 'python' LOGFILENAME = 'testbzr.log' try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires modules from python2.4\n" + ' ' + str(e)) sys.exit(1) def formcmd(cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = BZRPATH if OVERRIDE_PYTHON: cmd.insert(0, OVERRIDE_PYTHON) logfile.write('$ %r\n' % cmd) return cmd def runcmd(cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = formcmd(cmd) log_linenumber() actual_retcode = call(cmd, stdout=logfile, stderr=logfile) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(cmd, retcode=0): cmd = formcmd(cmd) log_linenumber() child = Popen(cmd, stdout=PIPE, stderr=logfile) outd, errd = child.communicate() logfile.write(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def progress(msg): print '* ' + msg logfile.write('* '+ msg + '\n') log_linenumber() def cd(dirname): logfile.write('$ cd %s\n' % dirname) os.chdir(dirname) def log_linenumber(): """Log the stack frame location two things up.""" stack = traceback.extract_stack()[-3] logfile.write(' at %s:%d\n' % stack[:2]) # prepare an empty scratch directory if os.path.exists(TESTDIR): shutil.rmtree(TESTDIR) start_dir = os.getcwd() logfile = open(LOGFILENAME, 'wt', buffering=1) try: from getopt import getopt opts, args = getopt(sys.argv[1:], 'p:') for option, value in opts: if option == '-p': OVERRIDE_PYTHON = value mypath = os.path.abspath(sys.argv[0]) print '%-30s %s' % ('running tests from', mypath) global BZRPATH if args: BZRPATH = args[0] else: BZRPATH = os.path.join(os.path.split(mypath)[0], 'bzr') print '%-30s %s' % ('against bzr', BZRPATH) print '%-30s %s' % ('in directory', os.getcwd()) print '%-30s %s' % ('with python', (OVERRIDE_PYTHON or '(default)')) print print backtick('bzr version') runcmd(['mkdir', TESTDIR]) cd(TESTDIR) # This means that any command that is naively run in this directory # Won't affect the parent directory. runcmd('bzr init') test_root = os.getcwd() progress("introductory commands") runcmd("bzr version") runcmd("bzr --version") runcmd("bzr help") runcmd("bzr --help") progress("internal tests") runcmd("bzr selftest") progress("invalid commands") runcmd("bzr pants", retcode=1) runcmd("bzr --pants off", retcode=1) runcmd("bzr diff --message foo", retcode=1) progress("basic branch creation") runcmd(['mkdir', 'branch1']) cd('branch1') runcmd('bzr init') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == 'unknown:\n test.txt\n' out = backtick("bzr status --all") assert out == "unknown:\n test.txt\n" out = backtick("bzr status test.txt --all") assert out == "unknown:\n test.txt\n" f = file('test2.txt', 'wt') f.write('goodbye cruel world...\n') f.close() out = backtick("bzr status test.txt") assert out == "unknown:\n test.txt\n" out = backtick("bzr status") assert out == ("unknown:\n" " test.txt\n" " test2.txt\n") os.unlink('test2.txt') progress("command aliases") out = backtick("bzr st --all") assert out == ("unknown:\n" " test.txt\n") out = backtick("bzr stat") assert out == ("unknown:\n" " test.txt\n") progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == ("added:\n" " test.txt\n") progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == os.path.join("sub2", "hello.txt\n") assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) cd('sub1/sub2') assert backtick('bzr root')[:-1] == os.path.join(test_root, 'branch1') runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') runcmd(['bzr', 'commit', '-m', 'move to parent directory']) cd('..') assert backtick('bzr relpath sub2/hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') runcmd('bzr log') runcmd('bzr log -v') progress("file with spaces in name") mkdir('sub directory') file('sub directory/file with spaces ', 'wt').write('see how this works\n') runcmd('bzr add .') runcmd('bzr diff') runcmd('bzr commit -m add-spaces') runcmd('bzr check') runcmd('bzr log') runcmd('bzr log --forward') runcmd('bzr info') cd('..') cd('..') progress('branch') # Can't create a branch if it already exists runcmd('bzr branch branch1', retcode=1) # Can't create a branch if its parent doesn't exist runcmd('bzr branch /unlikely/to/exist', retcode=1) runcmd('bzr branch branch1 branch2') progress("pull") cd('branch1') runcmd('bzr pull', retcode=1) runcmd('bzr pull ../branch2') cd('.bzr') runcmd('bzr pull') runcmd('bzr commit -m empty') runcmd('bzr pull') cd('../../branch2') runcmd('bzr pull') runcmd('bzr commit -m empty') cd('../branch1') runcmd('bzr commit -m empty') runcmd('bzr pull', retcode=1) cd ('..') progress('status after remove') mkdir('status-after-remove') # see mail from William Dodé, 2005-05-25 # $ bzr init; touch a; bzr add a; bzr commit -m "add a" # * looking for changes... # added a # * commited r1 # $ bzr remove a # $ bzr status # bzr: local variable 'kind' referenced before assignment # at /vrac/python/bazaar-ng/bzrlib/diff.py:286 in compare_trees() # see ~/.bzr.log for debug information cd('status-after-remove') runcmd('bzr init') file('a', 'w').write('foo') runcmd('bzr add a') runcmd(['bzr', 'commit', '-m', 'add a']) runcmd('bzr remove a') runcmd('bzr status') cd('..') progress('ignore patterns') mkdir('ignorebranch') cd('ignorebranch') runcmd('bzr init') assert backtick('bzr unknowns') == '' file('foo.tmp', 'wt').write('tmp files are ignored') assert backtick('bzr unknowns') == '' file('foo.c', 'wt').write('int main() {}') assert backtick('bzr unknowns') == 'foo.c\n' runcmd('bzr add foo.c') assert backtick('bzr unknowns') == '' # 'ignore' works when creating the .bzignore file file('foo.blah', 'wt').write('blah') assert backtick('bzr unknowns') == 'foo.blah\n' runcmd('bzr ignore *.blah') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\n' # 'ignore' works when then .bzrignore file already exists file('garh', 'wt').write('garh') assert backtick('bzr unknowns') == 'garh\n' runcmd('bzr ignore garh') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\ngarh\n' cd('..') progress("recursive and non-recursive add") mkdir('no-recurse') cd('no-recurse') runcmd('bzr init') mkdir('foo') fp = os.path.join('foo', 'test.txt') f = file(fp, 'w') f.write('hello!\n') f.close() runcmd('bzr add --no-recurse foo') runcmd('bzr file-id foo') runcmd('bzr file-id ' + fp, 1) # not versioned yet runcmd('bzr commit -m add-dir-only') runcmd('bzr file-id ' + fp, 1) # still not versioned runcmd('bzr add foo') runcmd('bzr file-id ' + fp) runcmd('bzr commit -m add-sub-file') cd('..') # Run any function in this g = globals() funcs = g.keys() funcs.sort() for k in funcs: if k.startswith('test_') and callable(g[k]): progress(k[5:].replace('_', ' ')) g[k]() progress("all tests passed!") except Exception, e: sys.stderr.write('*' * 50 + '\n' + 'testbzr: tests failed\n' + 'see ' + LOGFILENAME + ' for more information\n' + '*' * 50 + '\n') logfile.write('tests failed!\n') traceback.print_exc(None, logfile) logfile.close() sys.stdout.writelines(file(os.path.join(start_dir, LOGFILENAME), 'rt').readlines()[-50:]) sys.exit(1) commit refs/heads/master mark :733 committer Martin Pool 1119421387 +1000 data 31 - show test names while running from :732 M 644 inline bzrlib/selftest/__init__.py data 6077 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") raise class CommandFailed(Exception): pass class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr, so that we can run it # through a specified Python intepreter OVERRIDE_PYTHON = None # to run with alternative python 'python' BZRPATH = 'bzr' def formcmd(self, cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = self.BZRPATH if self.OVERRIDE_PYTHON: cmd.insert(0, self.OVERRIDE_PYTHON) self.log('$ %r' % cmd) return cmd def runcmd(self, cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = self.formcmd(cmd) self.log('$ ' + ' '.join(cmd)) actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(self, cmd, retcode=0): cmd = self.formcmd(cmd) child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG) outd, errd = child.communicate() self.log(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def log(self, msg): """Log a message to a progress file""" print >>self.TEST_LOG, msg class InTempDir(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.branch_dir = os.path.join(self.TEST_DIR, self.__class__.__name__) os.mkdir(self.branch_dir) os.chdir(self.branch_dir) def tearDown(self): import os os.chdir(self.TEST_DIR) class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ def startTest(self, test): print str(test).ljust(60), TestResult.startTest(self, test) def stopTest(self, test): # print TestResult.stopTest(self, test) def addError(self, test, err): print 'ERROR' TestResult.addError(self, test, err) def addFailure(self, test, err): print 'FAILURE' TestResult.addFailure(self, test, err) def addSuccess(self, test): print 'OK' TestResult.addSuccess(self, test) def selftest(): from unittest import TestLoader, TestSuite import bzrlib import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox from doctest import DocTestSuite import os import shutil import time _setup_test_log() _setup_test_dir() suite = TestSuite() tl = TestLoader() for m in bzrlib.selftest.whitebox, : suite.addTest(tl.loadTestsFromModule(m)) suite.addTest(bzrlib.selftest.blackbox.suite()) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands: suite.addTest(DocTestSuite(m)) result = _MyResult() suite.run(result) _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os log_filename = os.path.abspath('testbzr.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "bzr tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_DIR = os.path.abspath("testbzr.tmp") print '%-30s %s' % ('running tests in', TestBase.TEST_DIR) if os.path.exists(TestBase.TEST_DIR): shutil.rmtree(TestBase.TEST_DIR) os.mkdir(TestBase.TEST_DIR) os.chdir(TestBase.TEST_DIR) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_DIR, '.bzr')) def _show_results(result): for case, tb in result.errors: _show_test_failure('ERROR', case, tb) for case, tb in result.failures: _show_test_failure('FAILURE', case, tb) print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, tb): print (kind + '! ').ljust(60, '-') print case print tb print ''.ljust(60, '-') commit refs/heads/master mark :734 committer Martin Pool 1119421583 +1000 data 35 - remove noise output while testing from :733 M 644 inline bzrlib/selftest/blackbox.py data 2470 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Black-box tests for bzr. These check that it behaves properly when it's invoked through the regular command-line interface. This always reinvokes bzr through a new Python interpreter, which is a bit inefficient but arguably tests in a way more representative of how it's normally invoked. """ # this code was previously in testbzr from unittest import TestCase from bzrlib.selftest import TestBase, InTempDir class TestVersion(TestBase): def runTest(self): # output is intentionally passed through to stdout so that we # can see the version being tested self.runcmd(['bzr', 'version']) class HelpCommands(TestBase): def runTest(self): self.runcmd('bzr --help') self.runcmd('bzr help') self.runcmd('bzr help commands') self.runcmd('bzr help help') self.runcmd('bzr commit -h') class InitBranch(InTempDir): def runTest(self): import os self.runcmd(['bzr', 'init']) class UserIdentity(InTempDir): def runTest(self): # this should always identify something, if only "john@localhost" self.runcmd("bzr whoami") self.runcmd("bzr whoami --email") self.assertEquals(self.backtick("bzr whoami --email").count('@'), 1) # lists all tests from this module in the best order to run them. we # do it this way rather than just discovering them all because it # allows us to test more basic functions first where failures will be # easiest to understand. def suite(): from unittest import TestSuite s = TestSuite() s.addTests([TestVersion(), InitBranch(), HelpCommands(), UserIdentity()]) return s commit refs/heads/master mark :735 committer Martin Pool 1119422240 +1000 data 62 - clean up code for running tests in selfcontained directories from :734 M 644 inline bzrlib/selftest/__init__.py data 6080 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") raise class CommandFailed(Exception): pass class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr, so that we can run it # through a specified Python intepreter OVERRIDE_PYTHON = None # to run with alternative python 'python' BZRPATH = 'bzr' def formcmd(self, cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = self.BZRPATH if self.OVERRIDE_PYTHON: cmd.insert(0, self.OVERRIDE_PYTHON) self.log('$ %r' % cmd) return cmd def runcmd(self, cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = self.formcmd(cmd) self.log('$ ' + ' '.join(cmd)) actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(self, cmd, retcode=0): cmd = self.formcmd(cmd) child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG) outd, errd = child.communicate() self.log(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def log(self, msg): """Log a message to a progress file""" print >>self.TEST_LOG, msg class InTempDir(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.test_dir = os.path.join(self.TEST_ROOT, self.__class__.__name__) os.mkdir(self.test_dir) os.chdir(self.test_dir) def tearDown(self): import os os.chdir(self.TEST_ROOT) class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ def startTest(self, test): print str(test).ljust(60), TestResult.startTest(self, test) def stopTest(self, test): # print TestResult.stopTest(self, test) def addError(self, test, err): print 'ERROR' TestResult.addError(self, test, err) def addFailure(self, test, err): print 'FAILURE' TestResult.addFailure(self, test, err) def addSuccess(self, test): print 'OK' TestResult.addSuccess(self, test) def selftest(): from unittest import TestLoader, TestSuite import bzrlib import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox from doctest import DocTestSuite import os import shutil import time _setup_test_log() _setup_test_dir() suite = TestSuite() tl = TestLoader() for m in bzrlib.selftest.whitebox, : suite.addTest(tl.loadTestsFromModule(m)) suite.addTest(bzrlib.selftest.blackbox.suite()) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands: suite.addTest(DocTestSuite(m)) result = _MyResult() suite.run(result) _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os log_filename = os.path.abspath('testbzr.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "bzr tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_ROOT = os.path.abspath("testbzr.tmp") print '%-30s %s' % ('running tests in', TestBase.TEST_ROOT) if os.path.exists(TestBase.TEST_ROOT): shutil.rmtree(TestBase.TEST_ROOT) os.mkdir(TestBase.TEST_ROOT) os.chdir(TestBase.TEST_ROOT) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_ROOT, '.bzr')) def _show_results(result): for case, tb in result.errors: _show_test_failure('ERROR', case, tb) for case, tb in result.failures: _show_test_failure('FAILURE', case, tb) print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, tb): print (kind + '! ').ljust(60, '-') print case print tb print ''.ljust(60, '-') commit refs/heads/master mark :736 committer Martin Pool 1119422263 +1000 data 67 - move old blackbox code from testbzr into bzrlib.selftest.blackbox from :735 M 644 inline bzrlib/selftest/blackbox.py data 11207 # Copyright (C) 2005 by Canonical Ltd # -*- coding: utf-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Black-box tests for bzr. These check that it behaves properly when it's invoked through the regular command-line interface. This always reinvokes bzr through a new Python interpreter, which is a bit inefficient but arguably tests in a way more representative of how it's normally invoked. """ # this code was previously in testbzr from unittest import TestCase from bzrlib.selftest import TestBase, InTempDir class TestVersion(TestBase): def runTest(self): # output is intentionally passed through to stdout so that we # can see the version being tested self.runcmd(['bzr', 'version']) class HelpCommands(TestBase): def runTest(self): self.runcmd('bzr --help') self.runcmd('bzr help') self.runcmd('bzr help commands') self.runcmd('bzr help help') self.runcmd('bzr commit -h') class InitBranch(InTempDir): def runTest(self): import os self.runcmd(['bzr', 'init']) class UserIdentity(InTempDir): def runTest(self): # this should always identify something, if only "john@localhost" self.runcmd("bzr whoami") self.runcmd("bzr whoami --email") self.assertEquals(self.backtick("bzr whoami --email").count('@'), 1) class InvalidCommands(InTempDir): def runTest(self): self.runcmd("bzr pants", retcode=1) self.runcmd("bzr --pants off", retcode=1) self.runcmd("bzr diff --message foo", retcode=1) class OldTests(InTempDir): # old tests moved from ./testbzr def runTest(self): from os import chdir, mkdir from os.path import exists import os runcmd = self.runcmd backtick = self.backtick progress = self.log progress("basic branch creation") runcmd(['mkdir', 'branch1']) chdir('branch1') runcmd('bzr init') self.assertEquals(backtick('bzr root').rstrip(), os.path.join(self.test_dir, 'branch1')) progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") assert out == 'test.txt\n' out = backtick("bzr status") assert out == 'unknown:\n test.txt\n' out = backtick("bzr status --all") assert out == "unknown:\n test.txt\n" out = backtick("bzr status test.txt --all") assert out == "unknown:\n test.txt\n" f = file('test2.txt', 'wt') f.write('goodbye cruel world...\n') f.close() out = backtick("bzr status test.txt") assert out == "unknown:\n test.txt\n" out = backtick("bzr status") assert out == ("unknown:\n" " test.txt\n" " test2.txt\n") os.unlink('test2.txt') progress("command aliases") out = backtick("bzr st --all") assert out == ("unknown:\n" " test.txt\n") out = backtick("bzr stat") assert out == ("unknown:\n" " test.txt\n") progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == ("added:\n" " test.txt\n") progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == os.path.join("sub2", "hello.txt\n") assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) chdir('sub1/sub2') self.assertEquals(backtick('bzr root')[:-1], os.path.join(self.test_dir, 'branch1')) runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') runcmd(['bzr', 'commit', '-m', 'move to parent directory']) chdir('..') assert backtick('bzr relpath sub2/hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') runcmd('bzr log') runcmd('bzr log -v') progress("file with spaces in name") mkdir('sub directory') file('sub directory/file with spaces ', 'wt').write('see how this works\n') runcmd('bzr add .') runcmd('bzr diff') runcmd('bzr commit -m add-spaces') runcmd('bzr check') runcmd('bzr log') runcmd('bzr log --forward') runcmd('bzr info') chdir('..') chdir('..') progress('branch') # Can't create a branch if it already exists runcmd('bzr branch branch1', retcode=1) # Can't create a branch if its parent doesn't exist runcmd('bzr branch /unlikely/to/exist', retcode=1) runcmd('bzr branch branch1 branch2') progress("pull") chdir('branch1') runcmd('bzr pull', retcode=1) runcmd('bzr pull ../branch2') chdir('.bzr') runcmd('bzr pull') runcmd('bzr commit -m empty') runcmd('bzr pull') chdir('../../branch2') runcmd('bzr pull') runcmd('bzr commit -m empty') chdir('../branch1') runcmd('bzr commit -m empty') runcmd('bzr pull', retcode=1) chdir ('..') progress('status after remove') mkdir('status-after-remove') # see mail from William Dodé, 2005-05-25 # $ bzr init; touch a; bzr add a; bzr commit -m "add a" # * looking for changes... # added a # * commited r1 # $ bzr remove a # $ bzr status # bzr: local variable 'kind' referenced before assignment # at /vrac/python/bazaar-ng/bzrlib/diff.py:286 in compare_trees() # see ~/.bzr.log for debug information chdir('status-after-remove') runcmd('bzr init') file('a', 'w').write('foo') runcmd('bzr add a') runcmd(['bzr', 'commit', '-m', 'add a']) runcmd('bzr remove a') runcmd('bzr status') chdir('..') progress('ignore patterns') mkdir('ignorebranch') chdir('ignorebranch') runcmd('bzr init') assert backtick('bzr unknowns') == '' file('foo.tmp', 'wt').write('tmp files are ignored') assert backtick('bzr unknowns') == '' file('foo.c', 'wt').write('int main() {}') assert backtick('bzr unknowns') == 'foo.c\n' runcmd('bzr add foo.c') assert backtick('bzr unknowns') == '' # 'ignore' works when creating the .bzignore file file('foo.blah', 'wt').write('blah') assert backtick('bzr unknowns') == 'foo.blah\n' runcmd('bzr ignore *.blah') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\n' # 'ignore' works when then .bzrignore file already exists file('garh', 'wt').write('garh') assert backtick('bzr unknowns') == 'garh\n' runcmd('bzr ignore garh') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\ngarh\n' chdir('..') progress("recursive and non-recursive add") mkdir('no-recurse') chdir('no-recurse') runcmd('bzr init') mkdir('foo') fp = os.path.join('foo', 'test.txt') f = file(fp, 'w') f.write('hello!\n') f.close() runcmd('bzr add --no-recurse foo') runcmd('bzr file-id foo') runcmd('bzr file-id ' + fp, 1) # not versioned yet runcmd('bzr commit -m add-dir-only') runcmd('bzr file-id ' + fp, 1) # still not versioned runcmd('bzr add foo') runcmd('bzr file-id ' + fp) runcmd('bzr commit -m add-sub-file') chdir('..') # lists all tests from this module in the best order to run them. we # do it this way rather than just discovering them all because it # allows us to test more basic functions first where failures will be # easiest to understand. def suite(): from unittest import TestSuite s = TestSuite() s.addTests([TestVersion(), InitBranch(), HelpCommands(), UserIdentity(), InvalidCommands(), OldTests()]) return s M 644 inline testbzr data 845 #! /usr/bin/python # -*- coding: utf-8 -*- # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA print 'please use "bzr selftest" instead' import sys sys.exit(1) commit refs/heads/master mark :737 committer Martin Pool 1119422298 +1000 data 42 - add plugin patch, still being integrated from :736 M 644 inline patches/plugins-no-plugins.patch data 19057 *** added file 'bzrlib/plugin.py' --- /dev/null +++ bzrlib/plugin.py @@ -0,0 +1,92 @@ +# Copyright (C) 2004, 2005 by Canonical Ltd + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +# This module implements plug-in support. +# Any python module in $BZR_PLUGIN_PATH will be imported upon initialization +# of bzrlib (and then forgotten about). In the plugin's main body, it should +# update any bzrlib registries it wants to extend; for example, to add new +# commands, import bzrlib.commands and add your new command to the +# plugin_cmds variable. + +import sys, os, imp +try: + set +except NameError: + from sets import Set as set +from bzrlib.trace import log_error + + +def load_plugins(): + """Find all python files which are plugins, and load them + + The environment variable BZR_PLUGIN_PATH is considered a delimited set of + paths to look through. Each entry is searched for *.py files (and whatever + other extensions are used in the platform, such as *.pyd). + """ + bzrpath = os.environ.get('BZR_PLUGIN_PATH', os.path.expanduser('~/.bzr/plugins')) + + # The problem with imp.get_suffixes() is that it doesn't include + # .pyo which is technically valid + # It also means that "testmodule.so" will show up as both test and testmodule + # though it is only valid as 'test' + # but you should be careful, because "testmodule.py" loads as testmodule. + suffixes = imp.get_suffixes() + suffixes.append(('.pyo', 'rb', imp.PY_COMPILED)) + package_entries = ['__init__.py', '__init__.pyc', '__init__.pyo'] + for d in bzrpath.split(os.pathsep): + # going trough them one by one allows different plugins with the same + # filename in different directories in the path + if not d: + continue + plugin_names = set() + if not os.path.isdir(d): + continue + for f in os.listdir(d): + path = os.path.join(d, f) + if os.path.isdir(path): + for entry in package_entries: + # This directory should be a package, and thus added to + # the list + if os.path.isfile(os.path.join(path, entry)): + break + else: # This directory is not a package + continue + else: + for suffix_info in suffixes: + if f.endswith(suffix_info[0]): + f = f[:-len(suffix_info[0])] + if suffix_info[2] == imp.C_EXTENSION and f.endswith('module'): + f = f[:-len('module')] + break + else: + continue + plugin_names.add(f) + + plugin_names = list(plugin_names) + plugin_names.sort() + for name in plugin_names: + try: + plugin_info = imp.find_module(name, [d]) + try: + plugin = imp.load_module('bzrlib.plugin.' + name, + *plugin_info) + finally: + if plugin_info[0] is not None: + plugin_info[0].close() + except Exception, e: + log_error('Unable to load plugin: %r from %r\n%s' % (name, d, e)) + *** modified file 'bzrlib/__init__.py' --- bzrlib/__init__.py +++ bzrlib/__init__.py @@ -23,6 +23,7@@ from diff import compare_trees from trace import mutter, warning, open_tracefile from log import show_log +from plugin import load_plugins import add BZRDIR = ".bzr" @@ -62,4 +63,4 @@ return None except BzrError: return None - + *** modified file 'bzrlib/commands.py' --- bzrlib/commands.py +++ bzrlib/commands.py @@ -24,6 +24,24 @@ from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date + + +plugin_cmds = {} + + +def register_plugin_command(cmd): + "Utility function to help register a command" + global plugin_cmds + k = cmd.__name__ + if k.startswith("cmd_"): + k_unsquished = _unsquish_command_name(k) + else: + k_unsquished = k + if not plugin_cmds.has_key(k_unsquished): + plugin_cmds[k_unsquished] = cmd + else: + log_error('Two plugins defined the same command: %r' % k) + log_error('Not loading the one in %r' % sys.modules[cmd.__module__]) def _squish_command_name(cmd): @@ -68,100 +86,34 @@ revs = int(revstr) return revs -def _find_plugins(): - """Find all python files which are plugins, and load their commands - to add to the list of "all commands" - - The environment variable BZRPATH is considered a delimited set of - paths to look through. Each entry is searched for *.py files. - If a directory is found, it is also searched, but they are - not searched recursively. This allows you to revctl the plugins. - - Inside the plugin should be a series of cmd_* function, which inherit from - the bzrlib.commands.Command class. - """ - bzrpath = os.environ.get('BZRPLUGINPATH', '') - - plugin_cmds = {} - if not bzrpath: - return plugin_cmds - _platform_extensions = { - 'win32':'.pyd', - 'cygwin':'.dll', - 'darwin':'.dylib', - 'linux2':'.so' - } - if _platform_extensions.has_key(sys.platform): - platform_extension = _platform_extensions[sys.platform] - else: - platform_extension = None - for d in bzrpath.split(os.pathsep): - plugin_names = {} # This should really be a set rather than a dict - for f in os.listdir(d): - if f.endswith('.py'): - f = f[:-3] - elif f.endswith('.pyc') or f.endswith('.pyo'): - f = f[:-4] - elif platform_extension and f.endswith(platform_extension): - f = f[:-len(platform_extension)] - if f.endswidth('module'): - f = f[:-len('module')] - else: - continue - if not plugin_names.has_key(f): - plugin_names[f] = True - - plugin_names = plugin_names.keys() - plugin_names.sort() - try: - sys.path.insert(0, d) - for name in plugin_names: - try: - old_module = None - try: - if sys.modules.has_key(name): - old_module = sys.modules[name] - del sys.modules[name] - plugin = __import__(name, locals()) - for k in dir(plugin): - if k.startswith('cmd_'): - k_unsquished = _unsquish_command_name(k) - if not plugin_cmds.has_key(k_unsquished): - plugin_cmds[k_unsquished] = getattr(plugin, k) - else: - log_error('Two plugins defined the same command: %r' % k) - log_error('Not loading the one in %r in dir %r' % (name, d)) - finally: - if old_module: - sys.modules[name] = old_module - except ImportError, e: - log_error('Unable to load plugin: %r from %r\n%s' % (name, d, e)) - finally: - sys.path.pop(0) - return plugin_cmds - -def _get_cmd_dict(include_plugins=True): +def _get_cmd_dict(plugins_override=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v - if include_plugins: - d.update(_find_plugins()) + # If we didn't load plugins, the plugin_cmds dict will be empty + if plugins_override: + d.update(plugin_cmds) + else: + d2 = {} + d2.update(plugin_cmds) + d2.update(d) + d = d2 return d -def get_all_cmds(include_plugins=True): +def get_all_cmds(plugins_override=True): """Return canonical name and class for all registered commands.""" - for k, v in _get_cmd_dict(include_plugins=include_plugins).iteritems(): + for k, v in _get_cmd_dict(plugins_override=plugins_override).iteritems(): yield k,v -def get_cmd_class(cmd,include_plugins=True): +def get_cmd_class(cmd, plugins_override=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name - cmds = _get_cmd_dict(include_plugins=include_plugins) + cmds = _get_cmd_dict(plugins_override=plugins_override) try: return cmd, cmds[cmd] except KeyError: @@ -1461,6 +1413,75 @@ return argdict +def _parse_master_args(argv): + """Parse the arguments that always go with the original command. + These are things like bzr --no-plugins, etc. + + There are now 2 types of option flags. Ones that come *before* the command, + and ones that come *after* the command. + Ones coming *before* the command are applied against all possible commands. + And are generally applied before plugins are loaded. + + The current list are: + --builtin Allow plugins to load, but don't let them override builtin commands, + they will still be allowed if they do not override a builtin. + --no-plugins Don't load any plugins. This lets you get back to official source + behavior. + --profile Enable the hotspot profile before running the command. + For backwards compatibility, this is also a non-master option. + --version Spit out the version of bzr that is running and exit. + This is also a non-master option. + --help Run help and exit, also a non-master option (I think that should stay, though) + + >>> argv, opts = _parse_master_args(['bzr', '--test']) + Traceback (most recent call last): + ... + BzrCommandError: Invalid master option: 'test' + >>> argv, opts = _parse_master_args(['bzr', '--version', 'command']) + >>> print argv + ['command'] + >>> print opts['version'] + True + >>> argv, opts = _parse_master_args(['bzr', '--profile', 'command', '--more-options']) + >>> print argv + ['command', '--more-options'] + >>> print opts['profile'] + True + >>> argv, opts = _parse_master_args(['bzr', '--no-plugins', 'command']) + >>> print argv + ['command'] + >>> print opts['no-plugins'] + True + >>> print opts['profile'] + False + >>> argv, opts = _parse_master_args(['bzr', 'command', '--profile']) + >>> print argv + ['command', '--profile'] + >>> print opts['profile'] + False + """ + master_opts = {'builtin':False, + 'no-plugins':False, + 'version':False, + 'profile':False, + 'help':False + } + + # This is the point where we could hook into argv[0] to determine + # what front-end is supposed to be run + # For now, we are just ignoring it. + cmd_name = argv.pop(0) + for arg in argv[:]: + if arg[:2] != '--': # at the first non-option, we return the rest + break + arg = arg[2:] # Remove '--' + if arg not in master_opts: + # We could say that this is not an error, that we should + # just let it be handled by the main section instead + raise BzrCommandError('Invalid master option: %r' % arg) + argv.pop(0) # We are consuming this entry + master_opts[arg] = True + return argv, master_opts def run_bzr(argv): """Execute a command. @@ -1470,22 +1491,21 @@ """ argv = [a.decode(bzrlib.user_encoding) for a in argv] - include_plugins=True try: - args, opts = parse_args(argv[1:]) - if 'help' in opts: + argv, master_opts = _parse_master_args(argv) + if not master_opts['no-plugins']: + bzrlib.load_plugins() + args, opts = parse_args(argv) + if 'help' in opts or master_opts['help']: import help if args: help.help(args[0]) else: help.help() return 0 - elif 'version' in opts: + elif 'version' in opts or master_opts['version']: show_version() return 0 - elif args and args[0] == 'builtin': - include_plugins=False - args = args[1:] cmd = str(args.pop(0)) except IndexError: import help @@ -1493,14 +1513,15 @@ return 1 - canonical_cmd, cmd_class = get_cmd_class(cmd,include_plugins=include_plugins) - - # global option + plugins_override = not (master_opts['builtin']) + canonical_cmd, cmd_class = get_cmd_class(cmd, plugins_override=plugins_override) + + profile = master_opts['profile'] + # For backwards compatibility, I would rather stick with --profile being a + # master/global option if 'profile' in opts: profile = True del opts['profile'] - else: - profile = False # check options are reasonable allowed = cmd_class.takes_options *** modified file 'testbzr' --- testbzr +++ testbzr @@ -149,6 +149,7 @@ """Run a test involving creating a plugin to load, and making sure it is seen properly. """ + orig_help = backtick('bzr help commands') # No plugins yet mkdir('plugin_test') f = open(os.path.join('plugin_test', 'myplug.py'), 'wb') f.write("""import bzrlib, bzrlib.commands @@ -157,24 +158,36 @@ aliases = ['mplg'] def run(self): print 'Hello from my plugin' +class cmd_myplug_with_opt(bzrlib.commands.Command): + '''A simple plugin that requires a special option''' + takes_options = ['aspecialoptionthatdoesntexist'] + def run(self, aspecialoptionthatdoesntexist=None): + print 'Found: %s' % aspecialoptionthatdoesntexist + +bzrlib.commands.register_plugin_command(cmd_myplug) +bzrlib.commands.register_plugin_command(cmd_myplug_with_opt) +bzrlib.commands.OPTIONS['aspecialoptionthatdoesntexist'] = str """) f.close() - os.environ['BZRPLUGINPATH'] = os.path.abspath('plugin_test') - help = backtick('bzr help commands') + os.environ['BZR_PLUGIN_PATH'] = os.path.abspath('plugin_test') + help = backtick('bzr help commands') #Help with user-visible plugins assert help.find('myplug') != -1 assert help.find('Just a simple test plugin.') != -1 assert backtick('bzr myplug') == 'Hello from my plugin\n' assert backtick('bzr mplg') == 'Hello from my plugin\n' + assert backtick('bzr myplug-with-opt') == 'Found: None\n' + assert backtick('bzr myplug-with-opt --aspecialoptionthatdoesntexist=2') == 'Found: 2\n' f = open(os.path.join('plugin_test', 'override.py'), 'wb') f.write("""import bzrlib, bzrlib.commands -class cmd_commit(bzrlib.commands.cmd_commit): - '''Commit changes into a new revision.''' +class cmd_revno(bzrlib.commands.cmd_revno): + '''Show current revision number.''' def run(self, *args, **kwargs): print "I'm sorry dave, you can't do that" + return 1 class cmd_help(bzrlib.commands.cmd_help): '''Show help on a command or other topic.''' @@ -182,16 +195,67 @@ print "You have been overridden" bzrlib.commands.cmd_help.run(self, *args, **kwargs) +bzrlib.commands.register_plugin_command(cmd_revno) +bzrlib.commands.register_plugin_command(cmd_help) """) f.close() - newhelp = backtick('bzr help commands') + newhelp = backtick('bzr help commands') # Help with no new commands, assert newhelp.startswith('You have been overridden\n') # We added a line, but the rest should work assert newhelp[25:] == help - - assert backtick('bzr commit -m test') == "I'm sorry dave, you can't do that\n" - + # Make sure we can get back to the original command + # Not overridden, and no extra commands present + assert backtick('bzr --builtin help commands') == help + assert backtick('bzr --no-plugins help commands') == orig_help + + assert backtick('bzr revno', retcode=1) == "I'm sorry dave, you can't do that\n" + + print_txt = '** Loading noop plugin' + f = open(os.path.join('plugin_test', 'loading.py'), 'wb') + f.write("""import bzrlib, bzrlib.commands +class cmd_noop(bzrlib.commands.Command): + def run(self, *args, **kwargs): + pass + +print %r +bzrlib.commands.register_plugin_command(cmd_noop) +""" % print_txt) + f.close() + print_txt += '\n' + + # Check that --builtin still loads the plugin, and enables it as + # an extra command, but not as an override + # and that --no-plugins doesn't load the command at all + assert backtick('bzr noop') == print_txt + assert backtick('bzr --builtin help')[:len(print_txt)] == print_txt + assert backtick('bzr --no-plugins help')[:len(print_txt)] != print_txt + runcmd('bzr revno', retcode=1) + runcmd('bzr --builtin revno', retcode=0) + runcmd('bzr --no-plugins revno', retcode=0) + runcmd('bzr --builtin noop', retcode=0) + runcmd('bzr --no-plugins noop', retcode=1) + + # Check that packages can also be loaded + test_str = 'packages work' + os.mkdir(os.path.join('plugin_test', 'testpkg')) + f = open(os.path.join('plugin_test', 'testpkg', '__init__.py'), 'wb') + f.write("""import bzrlib, bzrlib.commands +class testpkgcmd(bzrlib.commands.Command): + def run(self, *args, **kwargs): + print %r + +bzrlib.commands.register_plugin_command(testpkgcmd) +""" % test_str) + f.close() + test_str += '\n' + assert backtick('bzr testpkgcmd') == print_txt + test_str + runcmd('bzr --no-plugins testpkgcmd', retcode=1) + + # Make sure that setting BZR_PLUGIN_PATH to empty is the same as using --no-plugins + os.environ['BZR_PLUGIN_PATH'] = '' + assert backtick('bzr help commands') == orig_help + shutil.rmtree('plugin_test') try: @@ -221,6 +285,9 @@ runcmd(['mkdir', TESTDIR]) cd(TESTDIR) + # This means that any command that is naively run in this directory + # Won't affect the parent directory. + runcmd('bzr init') test_root = os.getcwd() progress("introductory commands") commit refs/heads/master mark :738 committer Martin Pool 1119423546 +1000 data 43 - default plugin dir is ~/.bzr.conf/plugins from :737 M 644 inline TODO data 13603 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * Import ElementTree update patch. * Plugins that provide commands. By just installing a file into some directory (e.g. ``/usr/share/bzr/plugins``) it should be possible to create new top-level commands (``bzr frob``). Extensions can be written in either Python (in which case they use the bzrlib API) or in a separate process (in sh, C, whatever). It should be possible to get help for plugin commands. * Smart rewrap text in help messages to fit in $COLUMNS (or equivalent on Windows) * -r option should take a revision-id as well as a revno. * ``bzr info`` could show space used by working tree, versioned files, unknown and ignored files. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * ``bzr log DIR`` should give changes to any files within DIR. * Check all commands have decent help. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. Perhaps ~/.bzr/http-cache. Baz has a fairly simple cache under ~/.arch-cache, containing revision information encoded almost as a bunch of archives. Perhaps we could simply store full paths. * Maybe also store directories in the statcache so that we can quickly identify that they still exist. * Diff should show timestamps; for files from the working directory we can use the file itself; for files from a revision we should use the commit time of the revision. * Perhaps split command infrastructure from the actual command definitions. * Cleaner support for negative boolean options like --no-recurse. * Statcache should possibly map all file paths to / separators * quotefn doubles all backslashes on Windows; this is probably not the best thing to do. What would be a better way to safely represent filenames? Perhaps we could doublequote things containing spaces, on the principle that filenames containing quotes are unlikely? Nice for humans; less good for machine parsing. * Patches should probably use only forward slashes, even on Windows, otherwise Unix patch can't apply them. (?) * Branch.update_revisions() inefficiently fetches revisions from the remote server twice; once to find out what text and inventory they need and then again to actually get the thing. This is a bit inefficient. One complicating factor here is that we don't really want to have revisions present in the revision-store until all their constituent parts are also stored. The basic problem is that RemoteBranch.get_revision() and similar methods return object, but what we really want is the raw XML, which can be popped into our own store. That needs to be refactored. * ``bzr status FOO`` where foo is ignored should say so. * ``bzr mkdir A...`` should just create and add A. Medium things ------------- * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. We should be able to just get the id for the selected files, look up their location and diff just those files. No need to traverse the entire inventories. * ``bzr status DIR`` or ``bzr diff DIR`` should report on all changes under that directory. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from ElementTree to an object when it is read in, but rather wait until the program actually wants to know about that node. * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. - Selected-file commit - Impossible selected-file commit: adding things in non-versioned directories, crossing renames, etc. * Write a reproducible benchmark, perhaps importing various kernel versions. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Paranoid mode where we never trust SHA-1 matches. * Don't commit if there are no changes unless forced. * --dry-run mode for commit? (Or maybe just run with check-command=false?) * Generally, be a bit more verbose unless --silent is specified. * Function that finds all changes to files under a given directory; perhaps log should use this if a directory is given. * XML attributes might have trouble with filenames containing \n and \r. Do we really want to support this? I think perhaps not. * Remember execute bits, so that exports will work OK. * Unify smart_add and plain Branch.add(); perhaps smart_add should just build a list of files to add and pass that to the regular add function. * Function to list a directory, saying in which revision each file was last modified. Useful for web and gui interfaces, and slow to compute one file at a time. * unittest is standard, but the results are kind of ugly; would be nice to make it cleaner. * Check locking is correct during merge-related operations. * Perhaps attempts to get locks should timeout after some period of time, or at least display a progress message. * Split out upgrade functionality from check command into a separate ``bzr upgrade``. * Don't pass around command classes but rather pass objects. This'd make it cleaner to construct objects wrapping external commands. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. Possibly this should be done by splitting the commit function into several parts (under a single interface). It is already rather large. Decomposition: - find tree modifications and prepare in-memory inventory - export that inventory to a temporary directory - run the test in that temporary directory - if that succeeded, continue to actually finish the commit What should be done with the text of modified files while this is underway? I don't think we want to count on holding them in memory and we can't trust the working files to stay in one place so I suppose we need to move them into the text store, or otherwise into a temporary directory. If the commit does not actually complete, we would rather the content was not left behind in the stores. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` M 644 inline bzrlib/plugin.py data 3863 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # This module implements plug-in support. # Any python module in $BZR_PLUGIN_PATH will be imported upon initialization # of bzrlib (and then forgotten about). In the plugin's main body, it should # update any bzrlib registries it wants to extend; for example, to add new # commands, import bzrlib.commands and add your new command to the # plugin_cmds variable. import sys, os, imp try: set except NameError: from sets import Set as set # python2.3 from bzrlib.trace import log_error DEFAULT_PLUGIN_PATH = '~/.bzr.conf/plugins' def load_plugins(): """Find all python files which are plugins, and load them The environment variable BZR_PLUGIN_PATH is considered a delimited set of paths to look through. Each entry is searched for *.py files (and whatever other extensions are used in the platform, such as *.pyd). """ bzrpath = os.environ.get('BZR_PLUGIN_PATH', os.path.expanduser()) # The problem with imp.get_suffixes() is that it doesn't include # .pyo which is technically valid # It also means that "testmodule.so" will show up as both test and testmodule # though it is only valid as 'test' # but you should be careful, because "testmodule.py" loads as testmodule. suffixes = imp.get_suffixes() suffixes.append(('.pyo', 'rb', imp.PY_COMPILED)) package_entries = ['__init__.py', '__init__.pyc', '__init__.pyo'] for d in bzrpath.split(os.pathsep): # going trough them one by one allows different plugins with the same # filename in different directories in the path if not d: continue plugin_names = set() if not os.path.isdir(d): continue for f in os.listdir(d): path = os.path.join(d, f) if os.path.isdir(path): for entry in package_entries: # This directory should be a package, and thus added to # the list if os.path.isfile(os.path.join(path, entry)): break else: # This directory is not a package continue else: for suffix_info in suffixes: if f.endswith(suffix_info[0]): f = f[:-len(suffix_info[0])] if suffix_info[2] == imp.C_EXTENSION and f.endswith('module'): f = f[:-len('module')] break else: continue plugin_names.add(f) plugin_names = list(plugin_names) plugin_names.sort() for name in plugin_names: try: plugin_info = imp.find_module(name, [d]) try: plugin = imp.load_module('bzrlib.plugin.' + name, *plugin_info) finally: if plugin_info[0] is not None: plugin_info[0].close() except Exception, e: log_error('Unable to load plugin: %r from %r\n%s' % (name, d, e)) commit refs/heads/master mark :739 committer Martin Pool 1119423595 +1000 data 43 - default plugin dir is ~/.bzr.conf/plugins from :738 M 644 inline bzrlib/plugin.py data 3919 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # This module implements plug-in support. # Any python module in $BZR_PLUGIN_PATH will be imported upon initialization # of bzrlib (and then forgotten about). In the plugin's main body, it should # update any bzrlib registries it wants to extend; for example, to add new # commands, import bzrlib.commands and add your new command to the # plugin_cmds variable. import sys, os, imp try: set except NameError: from sets import Set as set # python2.3 from bzrlib.trace import log_error DEFAULT_PLUGIN_PATH = '~/.bzr.conf/plugins' def load_plugins(): """Find all python files which are plugins, and load them The environment variable BZR_PLUGIN_PATH is considered a delimited set of paths to look through. Each entry is searched for *.py files (and whatever other extensions are used in the platform, such as *.pyd). """ bzrpath = os.environ.get('BZR_PLUGIN_PATH') if not bzrpath: bzrpath = os.path.expanduser(DEFAULT_PLUGIN_PATH) # The problem with imp.get_suffixes() is that it doesn't include # .pyo which is technically valid # It also means that "testmodule.so" will show up as both test and testmodule # though it is only valid as 'test' # but you should be careful, because "testmodule.py" loads as testmodule. suffixes = imp.get_suffixes() suffixes.append(('.pyo', 'rb', imp.PY_COMPILED)) package_entries = ['__init__.py', '__init__.pyc', '__init__.pyo'] for d in bzrpath.split(os.pathsep): # going trough them one by one allows different plugins with the same # filename in different directories in the path if not d: continue plugin_names = set() if not os.path.isdir(d): continue for f in os.listdir(d): path = os.path.join(d, f) if os.path.isdir(path): for entry in package_entries: # This directory should be a package, and thus added to # the list if os.path.isfile(os.path.join(path, entry)): break else: # This directory is not a package continue else: for suffix_info in suffixes: if f.endswith(suffix_info[0]): f = f[:-len(suffix_info[0])] if suffix_info[2] == imp.C_EXTENSION and f.endswith('module'): f = f[:-len('module')] break else: continue plugin_names.add(f) plugin_names = list(plugin_names) plugin_names.sort() for name in plugin_names: try: plugin_info = imp.find_module(name, [d]) try: plugin = imp.load_module('bzrlib.plugin.' + name, *plugin_info) finally: if plugin_info[0] is not None: plugin_info[0].close() except Exception, e: log_error('Unable to load plugin: %r from %r\n%s' % (name, d, e)) commit refs/heads/master mark :740 committer Martin Pool 1119423602 +1000 data 43 - default plugin dir is ~/.bzr.conf/plugins from :739 commit refs/heads/master mark :741 committer Martin Pool 1119423609 +1000 data 43 - default plugin dir is ~/.bzr.conf/plugins from :740 M 644 inline NEWS data 8675 DEVELOPMENT HEAD NEW FEATURES: * Python plugins, automatically loaded from the directories on BZR_PLUGIN_PATH or ~/.bzr.conf/plugins by default. CHANGES: * New ``bzr upgrade`` command to upgrade the format of a branch, replacing ``bzr check --update``. * Files within store directories are no longer marked readonly on disk. bzr-0.0.5 2005-06-15 CHANGES: * ``bzr`` with no command now shows help rather than giving an error. Suggested by Michael Ellerman. * ``bzr status`` output format changed, because svn-style output doesn't really match the model of bzr. Now files are grouped by status and can be shown with their IDs. ``bzr status --all`` shows all versioned files and unknown files but not ignored files. * ``bzr log`` runs from most-recent to least-recent, the reverse of the previous order. The previous behaviour can be obtained with the ``--forward`` option. * ``bzr inventory`` by default shows only filenames, and also ids if ``--show-ids`` is given, in which case the id is the second field. ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New ``bzr ignore PATTERN`` command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.sw[nop] .git .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. File may be in a branch other than the working directory. * ``bzr log`` and ``bzr root`` can be given an http URL instead of a filename. * Commands can now be defined by external programs or scripts in a directory on $BZRPATH. * New "stat cache" avoids reading the contents of files if they haven't changed since the previous time. * If the Python interpreter is too old, try to find a better one or give an error. Based on a patch from Fredrik Lundh. * New optional parameter ``bzr info [BRANCH]``. * New form ``bzr commit SELECTED`` to commit only selected files. * New form ``bzr log -r FROM:TO`` shows changes in selected range; contributed by John A Meinel. * New option ``bzr diff --diff-options 'OPTS'`` allows passing options through to an external GNU diff. * New option ``bzr add --no-recurse`` to add a directory but not their contents. * ``bzr --version`` now shows more information if bzr is being run from a branch. BUG FIXES: * Fixed diff format so that added and removed files will be handled properly by patch. Fix from Lalo Martins. * Various fixes for files whose names contain spaces or other metacharacters. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. * testbzr requires python2.4, but can be used to test bzr running under a different version. * Tests added for many other changes in this release. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. * New developer commands ``added``, ``modified``. PORTABILITY: * Cope on Windows on python2.3 by using the weaker random seed. 2.4 is now only recommended. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. commit refs/heads/master mark :742 committer Martin Pool 1119424681 +1000 data 19 - new mkdir command from :741 M 644 inline NEWS data 8707 DEVELOPMENT HEAD NEW FEATURES: * Python plugins, automatically loaded from the directories on BZR_PLUGIN_PATH or ~/.bzr.conf/plugins by default. * New 'bzr mkdir' command. CHANGES: * New ``bzr upgrade`` command to upgrade the format of a branch, replacing ``bzr check --update``. * Files within store directories are no longer marked readonly on disk. bzr-0.0.5 2005-06-15 CHANGES: * ``bzr`` with no command now shows help rather than giving an error. Suggested by Michael Ellerman. * ``bzr status`` output format changed, because svn-style output doesn't really match the model of bzr. Now files are grouped by status and can be shown with their IDs. ``bzr status --all`` shows all versioned files and unknown files but not ignored files. * ``bzr log`` runs from most-recent to least-recent, the reverse of the previous order. The previous behaviour can be obtained with the ``--forward`` option. * ``bzr inventory`` by default shows only filenames, and also ids if ``--show-ids`` is given, in which case the id is the second field. ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New ``bzr ignore PATTERN`` command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.sw[nop] .git .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. File may be in a branch other than the working directory. * ``bzr log`` and ``bzr root`` can be given an http URL instead of a filename. * Commands can now be defined by external programs or scripts in a directory on $BZRPATH. * New "stat cache" avoids reading the contents of files if they haven't changed since the previous time. * If the Python interpreter is too old, try to find a better one or give an error. Based on a patch from Fredrik Lundh. * New optional parameter ``bzr info [BRANCH]``. * New form ``bzr commit SELECTED`` to commit only selected files. * New form ``bzr log -r FROM:TO`` shows changes in selected range; contributed by John A Meinel. * New option ``bzr diff --diff-options 'OPTS'`` allows passing options through to an external GNU diff. * New option ``bzr add --no-recurse`` to add a directory but not their contents. * ``bzr --version`` now shows more information if bzr is being run from a branch. BUG FIXES: * Fixed diff format so that added and removed files will be handled properly by patch. Fix from Lalo Martins. * Various fixes for files whose names contain spaces or other metacharacters. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. * testbzr requires python2.4, but can be used to test bzr running under a different version. * Tests added for many other changes in this release. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. * New developer commands ``added``, ``modified``. PORTABILITY: * Cope on Windows on python2.3 by using the weaker random seed. 2.4 is now only recommended. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 51304 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date plugin_cmds = {} def register_plugin_command(cmd): "Utility function to help register a command" global plugin_cmds k = cmd.__name__ if k.startswith("cmd_"): k_unsquished = _unsquish_command_name(k) else: k_unsquished = k if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = cmd else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r' % sys.modules[cmd.__module__]) def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _get_cmd_dict(plugins_override=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v # If we didn't load plugins, the plugin_cmds dict will be empty if plugins_override: d.update(plugin_cmds) else: d2 = plugin_cmds.copy() d2.update(d) d = d2 return d def get_all_cmds(plugins_override=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(plugins_override=plugins_override).iteritems(): yield k,v def get_cmd_class(cmd, plugins_override=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(plugins_override=plugins_override) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() for opt in self.takes_options: if not opt in OPTIONS: raise BzrError("Unknown option '%s' returned by external command %s" % (opt, path)) # TODO: Is there any way to check takes_args is valid here? self.takes_args = pipe.readline().split() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-usage'" % path) pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-help'" % path) def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: optname = name.replace('_','-') value = kargs[name] if OPTIONS.has_key(optname): # it's an option opts.append('--%s' % optname) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_mkdir(Command): """Create a new versioned directory. This is equivalent to creating the directory and then adding it. """ takes_args = ['dir+'] def run(self, dir_list): import os import bzrlib.branch b = None for d in dir_list: os.mkdir(d) if not b: b = bzrlib.branch.Branch(d) b.add([d], verbose=True) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import errno br_to = Branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if errno == errno.ENOENT: raise if location is None: if stored_loc is None: raise BzrCommandError("No pull location known or specified.") else: print "Using last location: %s" % stored_loc location = stored_loc from branch import find_branch, DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged. Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. To retrieve the branch as of a particular revision, supply the --revision parameter, as in "branch foo/bar -r 5". """ takes_args = ['from_location', 'to_location?'] takes_options = ['revision'] def run(self, from_location, to_location=None, revision=None): import errno from bzrlib.merge import merge from branch import find_branch, DivergedBranches, NoSuchRevision from shutil import rmtree try: br_from = find_branch(from_location) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location.rstrip("/\\")) try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) try: br_to.update_revisions(br_from, stop_revision=revision) except NoSuchRevision: rmtree(to_location) msg = "The branch %s has no revision %d." % (from_location, revision) raise BzrCommandError(msg) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False, ignore_zero=True) from_location = pull_loc(br_from) br_to.controlfile("x-pull", "wb").write(from_location + "\n") def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: raise BzrError("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: raise BzrError("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, exports to a directory (equivalent to --format=dir).""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format'] def run(self, dest, revision=None, format='dir'): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest, format) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_upgrade(Command): """Upgrade branch storage to current format. This should normally be used only after the check command tells you to run it. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.upgrade import upgrade upgrade(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest return int(not selftest()) class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'update': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) >>> parse_args('log -r 500'.split()) (['log'], {'revision': 500}) >>> parse_args('log -r500:600'.split()) (['log'], {'revision': [500, 600]}) >>> parse_args('log -vr500:600'.split()) (['log'], {'verbose': True, 'revision': [500, 600]}) >>> parse_args('log -rv500:600'.split()) #the r takes an argument Traceback (most recent call last): ... ValueError: invalid literal for int(): v500 """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: raise BzrError('unknown long option %r' % a) else: shortopt = a[1:] if shortopt in SHORT_OPTIONS: # Multi-character options must have a space to delimit # their value optname = SHORT_OPTIONS[shortopt] else: # Single character short options, can be chained, # and have their value appended to their name shortopt = a[1:2] if shortopt not in SHORT_OPTIONS: # We didn't find the multi-character name, and we # didn't find the single char name raise BzrError('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if a[2:]: # There are extra things on this option # see if it is the value, or if it is another # short option optargfn = OPTIONS[optname] if optargfn is None: # This option does not take an argument, so the # next entry is another short option, pack it back # into the list argv.insert(0, '-' + a[2:]) else: # This option takes an argument, so pack it # into the array optarg = a[2:] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? raise BzrError('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: raise BzrError('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: raise BzrError('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def _parse_master_args(argv): """Parse the arguments that always go with the original command. These are things like bzr --no-plugins, etc. There are now 2 types of option flags. Ones that come *before* the command, and ones that come *after* the command. Ones coming *before* the command are applied against all possible commands. And are generally applied before plugins are loaded. The current list are: --builtin Allow plugins to load, but don't let them override builtin commands, they will still be allowed if they do not override a builtin. --no-plugins Don't load any plugins. This lets you get back to official source behavior. --profile Enable the hotspot profile before running the command. For backwards compatibility, this is also a non-master option. --version Spit out the version of bzr that is running and exit. This is also a non-master option. --help Run help and exit, also a non-master option (I think that should stay, though) >>> argv, opts = _parse_master_args(['bzr', '--test']) Traceback (most recent call last): ... BzrCommandError: Invalid master option: 'test' >>> argv, opts = _parse_master_args(['bzr', '--version', 'command']) >>> print argv ['command'] >>> print opts['version'] True >>> argv, opts = _parse_master_args(['bzr', '--profile', 'command', '--more-options']) >>> print argv ['command', '--more-options'] >>> print opts['profile'] True >>> argv, opts = _parse_master_args(['bzr', '--no-plugins', 'command']) >>> print argv ['command'] >>> print opts['no-plugins'] True >>> print opts['profile'] False >>> argv, opts = _parse_master_args(['bzr', 'command', '--profile']) >>> print argv ['command', '--profile'] >>> print opts['profile'] False """ master_opts = {'builtin':False, 'no-plugins':False, 'version':False, 'profile':False, 'help':False } # This is the point where we could hook into argv[0] to determine # what front-end is supposed to be run # For now, we are just ignoring it. cmd_name = argv.pop(0) for arg in argv[:]: if arg[:2] != '--': # at the first non-option, we return the rest break arg = arg[2:] # Remove '--' if arg not in master_opts: # We could say that this is not an error, that we should # just let it be handled by the main section instead raise BzrCommandError('Invalid master option: %r' % arg) argv.pop(0) # We are consuming this entry master_opts[arg] = True return argv, master_opts def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: # some options like --builtin and --no-plugins have special effects argv, master_opts = _parse_master_args(argv) if 'no-plugins' not in master_opts: bzrlib.load_plugins() args, opts = parse_args(argv) if master_opts['help']: from bzrlib.help import help if argv: help(argv[0]) else: help() return 0 if 'help' in opts: from bzrlib.help import help if args: help(args[0]) else: help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 plugins_override = not (master_opts['builtin']) canonical_cmd, cmd_class = get_cmd_class(cmd, plugins_override=plugins_override) profile = master_opts['profile'] # For backwards compatibility, I would rather stick with --profile being a # master/global option if 'profile' in opts: profile = True del opts['profile'] # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :743 committer Martin Pool 1119424716 +1000 data 34 - new simple versioning test cases from :742 M 644 inline bzrlib/selftest/versioning.py data 1053 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tests of simple versioning operations""" from bzrlib.selftest import InTempDir class Mkdir(InTempDir): def runTest(self): from bzrlib.commands import run_bzr import os run_bzr(['bzr', 'init']) run_bzr(['bzr', 'mkdir', 'foo']) self.assert_(os.path.isdir('foo')) M 644 inline bzrlib/selftest/__init__.py data 6158 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") raise class CommandFailed(Exception): pass class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr, so that we can run it # through a specified Python intepreter OVERRIDE_PYTHON = None # to run with alternative python 'python' BZRPATH = 'bzr' def formcmd(self, cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = self.BZRPATH if self.OVERRIDE_PYTHON: cmd.insert(0, self.OVERRIDE_PYTHON) self.log('$ %r' % cmd) return cmd def runcmd(self, cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = self.formcmd(cmd) self.log('$ ' + ' '.join(cmd)) actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(self, cmd, retcode=0): cmd = self.formcmd(cmd) child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG) outd, errd = child.communicate() self.log(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def log(self, msg): """Log a message to a progress file""" print >>self.TEST_LOG, msg class InTempDir(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.test_dir = os.path.join(self.TEST_ROOT, self.__class__.__name__) os.mkdir(self.test_dir) os.chdir(self.test_dir) def tearDown(self): import os os.chdir(self.TEST_ROOT) class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ def startTest(self, test): print str(test).ljust(60), TestResult.startTest(self, test) def stopTest(self, test): # print TestResult.stopTest(self, test) def addError(self, test, err): print 'ERROR' TestResult.addError(self, test, err) def addFailure(self, test, err): print 'FAILURE' TestResult.addFailure(self, test, err) def addSuccess(self, test): print 'OK' TestResult.addSuccess(self, test) def selftest(): from unittest import TestLoader, TestSuite import bzrlib import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning from doctest import DocTestSuite import os import shutil import time _setup_test_log() _setup_test_dir() suite = TestSuite() tl = TestLoader() for m in bzrlib.selftest.whitebox, \ bzrlib.selftest.versioning: suite.addTest(tl.loadTestsFromModule(m)) suite.addTest(bzrlib.selftest.blackbox.suite()) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands: suite.addTest(DocTestSuite(m)) result = _MyResult() suite.run(result) _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os log_filename = os.path.abspath('testbzr.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "bzr tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_ROOT = os.path.abspath("testbzr.tmp") print '%-30s %s' % ('running tests in', TestBase.TEST_ROOT) if os.path.exists(TestBase.TEST_ROOT): shutil.rmtree(TestBase.TEST_ROOT) os.mkdir(TestBase.TEST_ROOT) os.chdir(TestBase.TEST_ROOT) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_ROOT, '.bzr')) def _show_results(result): for case, tb in result.errors: _show_test_failure('ERROR', case, tb) for case, tb in result.failures: _show_test_failure('FAILURE', case, tb) print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, tb): print (kind + '! ').ljust(60, '-') print case print tb print ''.ljust(60, '-') commit refs/heads/master mark :744 committer Martin Pool 1119425255 +1000 data 45 - show nicer descriptions while running tests from :743 M 644 inline bzrlib/selftest/__init__.py data 6313 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") raise class CommandFailed(Exception): pass class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr, so that we can run it # through a specified Python intepreter OVERRIDE_PYTHON = None # to run with alternative python 'python' BZRPATH = 'bzr' def formcmd(self, cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = self.BZRPATH if self.OVERRIDE_PYTHON: cmd.insert(0, self.OVERRIDE_PYTHON) self.log('$ %r' % cmd) return cmd def runcmd(self, cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = self.formcmd(cmd) self.log('$ ' + ' '.join(cmd)) actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(self, cmd, retcode=0): cmd = self.formcmd(cmd) child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG) outd, errd = child.communicate() self.log(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def log(self, msg): """Log a message to a progress file""" print >>self.TEST_LOG, msg class InTempDir(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.test_dir = os.path.join(self.TEST_ROOT, self.__class__.__name__) os.mkdir(self.test_dir) os.chdir(self.test_dir) def tearDown(self): import os os.chdir(self.TEST_ROOT) class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ def startTest(self, test): # TODO: Maybe show test.shortDescription somewhere? print '%-60.60s' % test.id(), TestResult.startTest(self, test) def stopTest(self, test): # print TestResult.stopTest(self, test) def addError(self, test, err): print 'ERROR' TestResult.addError(self, test, err) def addFailure(self, test, err): print 'FAILURE' TestResult.addFailure(self, test, err) def addSuccess(self, test): print 'OK' TestResult.addSuccess(self, test) def selftest(): from unittest import TestLoader, TestSuite import bzrlib import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning from doctest import DocTestSuite import os import shutil import time _setup_test_log() _setup_test_dir() print suite = TestSuite() tl = TestLoader() for m in bzrlib.selftest.whitebox, \ bzrlib.selftest.versioning: suite.addTest(tl.loadTestsFromModule(m)) suite.addTest(bzrlib.selftest.blackbox.suite()) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands: suite.addTest(DocTestSuite(m)) result = _MyResult() suite.run(result) _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os log_filename = os.path.abspath('testbzr.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "bzr tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_ROOT = os.path.abspath("testbzr.tmp") print '%-30s %s' % ('running tests in', TestBase.TEST_ROOT) if os.path.exists(TestBase.TEST_ROOT): shutil.rmtree(TestBase.TEST_ROOT) os.mkdir(TestBase.TEST_ROOT) os.chdir(TestBase.TEST_ROOT) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_ROOT, '.bzr')) def _show_results(result): for case, tb in result.errors: _show_test_failure('ERROR', case, tb) for case, tb in result.failures: _show_test_failure('FAILURE', case, tb) print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, tb): print (kind + '! ').ljust(60, '-') print case desc = test.shortDescription() if desc: print ' (%s)' % desc print tb print ''.ljust(60, '-') M 644 inline bzrlib/selftest/versioning.py data 1095 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tests of simple versioning operations""" from bzrlib.selftest import InTempDir class Mkdir(InTempDir): def runTest(self): """Basic 'bzr mkdir' operation""" from bzrlib.commands import run_bzr import os run_bzr(['bzr', 'init']) run_bzr(['bzr', 'mkdir', 'foo']) self.assert_(os.path.isdir('foo')) commit refs/heads/master mark :745 committer Martin Pool 1119425445 +1000 data 44 - redirect stdout/stderr while running tests from :744 M 644 inline bzrlib/selftest/__init__.py data 6748 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") raise class CommandFailed(Exception): pass class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr, so that we can run it # through a specified Python intepreter OVERRIDE_PYTHON = None # to run with alternative python 'python' BZRPATH = 'bzr' def formcmd(self, cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = self.BZRPATH if self.OVERRIDE_PYTHON: cmd.insert(0, self.OVERRIDE_PYTHON) self.log('$ %r' % cmd) return cmd def runcmd(self, cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = self.formcmd(cmd) self.log('$ ' + ' '.join(cmd)) actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(self, cmd, retcode=0): cmd = self.formcmd(cmd) child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG) outd, errd = child.communicate() self.log(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def log(self, msg): """Log a message to a progress file""" print >>self.TEST_LOG, msg class InTempDir(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.test_dir = os.path.join(self.TEST_ROOT, self.__class__.__name__) os.mkdir(self.test_dir) os.chdir(self.test_dir) def tearDown(self): import os os.chdir(self.TEST_ROOT) class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ def __init__(self, out): self.out = out TestResult.__init__(self) def startTest(self, test): # TODO: Maybe show test.shortDescription somewhere? print >>self.out, '%-60.60s' % test.id(), TestResult.startTest(self, test) def stopTest(self, test): # print TestResult.stopTest(self, test) def addError(self, test, err): print >>self.out, 'ERROR' TestResult.addError(self, test, err) def addFailure(self, test, err): print >>self.out, 'FAILURE' TestResult.addFailure(self, test, err) def addSuccess(self, test): print >>self.out, 'OK' TestResult.addSuccess(self, test) def selftest(): from unittest import TestLoader, TestSuite import bzrlib import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning from doctest import DocTestSuite import os import shutil import time import sys _setup_test_log() _setup_test_dir() print suite = TestSuite() tl = TestLoader() for m in bzrlib.selftest.whitebox, \ bzrlib.selftest.versioning: suite.addTest(tl.loadTestsFromModule(m)) suite.addTest(bzrlib.selftest.blackbox.suite()) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands: suite.addTest(DocTestSuite(m)) # save stdout & stderr so there's no leakage from code-under-test real_stdout = sys.stdout real_stderr = sys.stderr sys.stdout = sys.stderr = TestBase.TEST_LOG try: result = _MyResult(real_stdout) suite.run(result) finally: sys.stdout = real_stdout sys.stderr = real_stderr _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os log_filename = os.path.abspath('testbzr.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "bzr tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_ROOT = os.path.abspath("testbzr.tmp") print '%-30s %s' % ('running tests in', TestBase.TEST_ROOT) if os.path.exists(TestBase.TEST_ROOT): shutil.rmtree(TestBase.TEST_ROOT) os.mkdir(TestBase.TEST_ROOT) os.chdir(TestBase.TEST_ROOT) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_ROOT, '.bzr')) def _show_results(result): for case, tb in result.errors: _show_test_failure('ERROR', case, tb) for case, tb in result.failures: _show_test_failure('FAILURE', case, tb) print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, tb): print (kind + '! ').ljust(60, '-') print case desc = test.shortDescription() if desc: print ' (%s)' % desc print tb print ''.ljust(60, '-') commit refs/heads/master mark :746 committer Martin Pool 1119425798 +1000 data 57 - compare_trees doesn't return unchanged files by default from :745 M 644 inline bzrlib/diff.py data 14170 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from trace import mutter from errors import BzrError def internal_diff(old_label, oldlines, new_label, newlines, to_file): import difflib # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, fromfile=old_label, tofile=new_label) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') to_file.writelines(ud) if nonl: print >>to_file, "\\ No newline at end of file" print >>to_file def external_diff(old_label, oldlines, new_label, newlines, to_file, diff_opts): """Display a diff by calling out to the external diff program.""" import sys if to_file != sys.stdout: raise NotImplementedError("sorry, can't send external diff other than to stdout yet", to_file) # make sure our own output is properly ordered before the diff to_file.flush() from tempfile import NamedTemporaryFile import os oldtmpf = NamedTemporaryFile() newtmpf = NamedTemporaryFile() try: # TODO: perhaps a special case for comparing to or from the empty # sequence; can just use /dev/null on Unix # TODO: if either of the files being compared already exists as a # regular named file (e.g. in the working directory) then we can # compare directly to that, rather than copying it. oldtmpf.writelines(oldlines) newtmpf.writelines(newlines) oldtmpf.flush() newtmpf.flush() if not diff_opts: diff_opts = [] diffcmd = ['diff', '--label', old_label, oldtmpf.name, '--label', new_label, newtmpf.name] # diff only allows one style to be specified; they don't override. # note that some of these take optargs, and the optargs can be # directly appended to the options. # this is only an approximate parser; it doesn't properly understand # the grammar. for s in ['-c', '-u', '-C', '-U', '-e', '--ed', '-q', '--brief', '--normal', '-n', '--rcs', '-y', '--side-by-side', '-D', '--ifdef']: for j in diff_opts: if j.startswith(s): break else: continue break else: diffcmd.append('-u') if diff_opts: diffcmd.extend(diff_opts) rc = os.spawnvp(os.P_WAIT, 'diff', diffcmd) if rc != 0 and rc != 1: # returns 1 if files differ; that's OK if rc < 0: msg = 'signal %d' % (-rc) else: msg = 'exit code %d' % rc raise BzrError('external diff failed with %s; command: %r' % (rc, diffcmd)) finally: oldtmpf.close() # and delete newtmpf.close() def show_diff(b, revision, specific_files, external_diff_options=None): """Shortcut for showing the diff to the working tree. b Branch. revision None for each, or otherwise the old revision to compare against. The more general form is show_diff_trees(), where the caller supplies any two trees. """ import sys if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() show_diff_trees(old_tree, new_tree, sys.stdout, specific_files, external_diff_options) def show_diff_trees(old_tree, new_tree, to_file, specific_files=None, external_diff_options=None): """Show in text form the changes from one tree to another. to_files If set, include only changes to these files. external_diff_options If set, use an external GNU diff and pass these options. """ # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. if external_diff_options: assert isinstance(external_diff_options, basestring) opts = external_diff_options.split() def diff_file(olab, olines, nlab, nlines, to_file): external_diff(olab, olines, nlab, nlines, to_file, opts) else: diff_file = internal_diff delta = compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=specific_files) for path, file_id, kind in delta.removed: print >>to_file, '*** removed %s %r' % (kind, path) if kind == 'file': diff_file(old_label + path, old_tree.get_file(file_id).readlines(), DEVNULL, [], to_file) for path, file_id, kind in delta.added: print >>to_file, '*** added %s %r' % (kind, path) if kind == 'file': diff_file(DEVNULL, [], new_label + path, new_tree.get_file(file_id).readlines(), to_file) for old_path, new_path, file_id, kind, text_modified in delta.renamed: print >>to_file, '*** renamed %s %r => %r' % (kind, old_path, new_path) if text_modified: diff_file(old_label + old_path, old_tree.get_file(file_id).readlines(), new_label + new_path, new_tree.get_file(file_id).readlines(), to_file) for path, file_id, kind in delta.modified: print >>to_file, '*** modified %s %r' % (kind, path) if kind == 'file': diff_file(old_label + path, old_tree.get_file(file_id).readlines(), new_label + path, new_tree.get_file(file_id).readlines(), to_file) class TreeDelta(object): """Describes changes from one tree to another. Contains four lists: added (path, id, kind) removed (path, id, kind) renamed (oldpath, newpath, id, kind, text_modified) modified (path, id, kind) unchanged (path, id, kind) Each id is listed only once. Files that are both modified and renamed are listed only in renamed, with the text_modified flag true. The lists are normally sorted when the delta is created. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] self.unchanged = [] def __repr__(self): return "TreeDelta(added=%r, removed=%r, renamed=%r, modified=%r," \ " unchanged=%r)" % (self.added, self.removed, self.renamed, self.modified, self.unchanged) def has_changed(self): changes = len(self.added) + len(self.removed) + len(self.renamed) changes += len(self.modified) return (changes != 0) def touches_file_id(self, file_id): """Return True if file_id is modified by this delta.""" for l in self.added, self.removed, self.modified: for v in l: if v[1] == file_id: return True for v in self.renamed: if v[2] == file_id: return True return False def show(self, to_file, show_ids=False, show_unchanged=False): def show_list(files): for path, fid, kind in files: if kind == 'directory': path += '/' elif kind == 'symlink': path += '@' if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if self.removed: print >>to_file, 'removed:' show_list(self.removed) if self.added: print >>to_file, 'added:' show_list(self.added) if self.renamed: print >>to_file, 'renamed:' for oldpath, newpath, fid, kind, text_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if self.modified: print >>to_file, 'modified:' show_list(self.modified) if show_unchanged and self.unchanged: print >>to_file, 'unchanged:' show_list(self.unchanged) def compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=None): """Describe changes from one tree to another. Returns a TreeDelta with details of added, modified, renamed, and deleted entries. The root entry is specifically exempt. This only considers versioned files. want_unchanged If true, also list files unchanged from one version to the next. specific_files If true, only check for changes to specified names or files within them. """ from osutils import is_inside_any old_inv = old_tree.inventory new_inv = new_tree.inventory delta = TreeDelta() mutter('start compare_trees') # TODO: match for specific files can be rather smarter by finding # the IDs of those files up front and then considering only that. for file_id in old_tree: if file_id in new_tree: kind = old_inv.get_file_kind(file_id) assert kind == new_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ 'invalid file kind %r' % kind if kind == 'root_directory': continue old_path = old_inv.id2path(file_id) new_path = new_inv.id2path(file_id) if specific_files: if (not is_inside_any(specific_files, old_path) and not is_inside_any(specific_files, new_path)): continue if kind == 'file': old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False # TODO: Can possibly avoid calculating path strings if the # two files are unchanged and their names and parents are # the same and the parents are unchanged all the way up. # May not be worthwhile. if old_path != new_path: delta.renamed.append((old_path, new_path, file_id, kind, text_modified)) elif text_modified: delta.modified.append((new_path, file_id, kind)) elif want_unchanged: delta.unchanged.append((new_path, file_id, kind)) else: kind = old_inv.get_file_kind(file_id) old_path = old_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, old_path): continue delta.removed.append((old_path, file_id, kind)) mutter('start looking for new files') for file_id in new_inv: if file_id in old_inv: continue new_path = new_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, new_path): continue kind = new_inv.get_file_kind(file_id) delta.added.append((new_path, file_id, kind)) delta.removed.sort() delta.added.sort() delta.renamed.sort() delta.modified.sort() delta.unchanged.sort() return delta commit refs/heads/master mark :747 committer Martin Pool 1119426990 +1000 data 37 - TreeDelta __eq__ and __ne__ methods from :746 M 644 inline bzrlib/diff.py data 14586 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from trace import mutter from errors import BzrError def internal_diff(old_label, oldlines, new_label, newlines, to_file): import difflib # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, fromfile=old_label, tofile=new_label) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') to_file.writelines(ud) if nonl: print >>to_file, "\\ No newline at end of file" print >>to_file def external_diff(old_label, oldlines, new_label, newlines, to_file, diff_opts): """Display a diff by calling out to the external diff program.""" import sys if to_file != sys.stdout: raise NotImplementedError("sorry, can't send external diff other than to stdout yet", to_file) # make sure our own output is properly ordered before the diff to_file.flush() from tempfile import NamedTemporaryFile import os oldtmpf = NamedTemporaryFile() newtmpf = NamedTemporaryFile() try: # TODO: perhaps a special case for comparing to or from the empty # sequence; can just use /dev/null on Unix # TODO: if either of the files being compared already exists as a # regular named file (e.g. in the working directory) then we can # compare directly to that, rather than copying it. oldtmpf.writelines(oldlines) newtmpf.writelines(newlines) oldtmpf.flush() newtmpf.flush() if not diff_opts: diff_opts = [] diffcmd = ['diff', '--label', old_label, oldtmpf.name, '--label', new_label, newtmpf.name] # diff only allows one style to be specified; they don't override. # note that some of these take optargs, and the optargs can be # directly appended to the options. # this is only an approximate parser; it doesn't properly understand # the grammar. for s in ['-c', '-u', '-C', '-U', '-e', '--ed', '-q', '--brief', '--normal', '-n', '--rcs', '-y', '--side-by-side', '-D', '--ifdef']: for j in diff_opts: if j.startswith(s): break else: continue break else: diffcmd.append('-u') if diff_opts: diffcmd.extend(diff_opts) rc = os.spawnvp(os.P_WAIT, 'diff', diffcmd) if rc != 0 and rc != 1: # returns 1 if files differ; that's OK if rc < 0: msg = 'signal %d' % (-rc) else: msg = 'exit code %d' % rc raise BzrError('external diff failed with %s; command: %r' % (rc, diffcmd)) finally: oldtmpf.close() # and delete newtmpf.close() def show_diff(b, revision, specific_files, external_diff_options=None): """Shortcut for showing the diff to the working tree. b Branch. revision None for each, or otherwise the old revision to compare against. The more general form is show_diff_trees(), where the caller supplies any two trees. """ import sys if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() show_diff_trees(old_tree, new_tree, sys.stdout, specific_files, external_diff_options) def show_diff_trees(old_tree, new_tree, to_file, specific_files=None, external_diff_options=None): """Show in text form the changes from one tree to another. to_files If set, include only changes to these files. external_diff_options If set, use an external GNU diff and pass these options. """ # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. if external_diff_options: assert isinstance(external_diff_options, basestring) opts = external_diff_options.split() def diff_file(olab, olines, nlab, nlines, to_file): external_diff(olab, olines, nlab, nlines, to_file, opts) else: diff_file = internal_diff delta = compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=specific_files) for path, file_id, kind in delta.removed: print >>to_file, '*** removed %s %r' % (kind, path) if kind == 'file': diff_file(old_label + path, old_tree.get_file(file_id).readlines(), DEVNULL, [], to_file) for path, file_id, kind in delta.added: print >>to_file, '*** added %s %r' % (kind, path) if kind == 'file': diff_file(DEVNULL, [], new_label + path, new_tree.get_file(file_id).readlines(), to_file) for old_path, new_path, file_id, kind, text_modified in delta.renamed: print >>to_file, '*** renamed %s %r => %r' % (kind, old_path, new_path) if text_modified: diff_file(old_label + old_path, old_tree.get_file(file_id).readlines(), new_label + new_path, new_tree.get_file(file_id).readlines(), to_file) for path, file_id, kind in delta.modified: print >>to_file, '*** modified %s %r' % (kind, path) if kind == 'file': diff_file(old_label + path, old_tree.get_file(file_id).readlines(), new_label + path, new_tree.get_file(file_id).readlines(), to_file) class TreeDelta(object): """Describes changes from one tree to another. Contains four lists: added (path, id, kind) removed (path, id, kind) renamed (oldpath, newpath, id, kind, text_modified) modified (path, id, kind) unchanged (path, id, kind) Each id is listed only once. Files that are both modified and renamed are listed only in renamed, with the text_modified flag true. The lists are normally sorted when the delta is created. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] self.unchanged = [] def __eq__(self, other): if not isinstance(other, TreeDelta): return False return self.added == other.added \ and self.removed == other.removed \ and self.renamed == other.renamed \ and self.modified == other.modified \ and self.unchanged == other.unchanged def __ne__(self, other): return not (self == other) def __repr__(self): return "TreeDelta(added=%r, removed=%r, renamed=%r, modified=%r," \ " unchanged=%r)" % (self.added, self.removed, self.renamed, self.modified, self.unchanged) def has_changed(self): changes = len(self.added) + len(self.removed) + len(self.renamed) changes += len(self.modified) return (changes != 0) def touches_file_id(self, file_id): """Return True if file_id is modified by this delta.""" for l in self.added, self.removed, self.modified: for v in l: if v[1] == file_id: return True for v in self.renamed: if v[2] == file_id: return True return False def show(self, to_file, show_ids=False, show_unchanged=False): def show_list(files): for path, fid, kind in files: if kind == 'directory': path += '/' elif kind == 'symlink': path += '@' if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if self.removed: print >>to_file, 'removed:' show_list(self.removed) if self.added: print >>to_file, 'added:' show_list(self.added) if self.renamed: print >>to_file, 'renamed:' for oldpath, newpath, fid, kind, text_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if self.modified: print >>to_file, 'modified:' show_list(self.modified) if show_unchanged and self.unchanged: print >>to_file, 'unchanged:' show_list(self.unchanged) def compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=None): """Describe changes from one tree to another. Returns a TreeDelta with details of added, modified, renamed, and deleted entries. The root entry is specifically exempt. This only considers versioned files. want_unchanged If true, also list files unchanged from one version to the next. specific_files If true, only check for changes to specified names or files within them. """ from osutils import is_inside_any old_inv = old_tree.inventory new_inv = new_tree.inventory delta = TreeDelta() mutter('start compare_trees') # TODO: match for specific files can be rather smarter by finding # the IDs of those files up front and then considering only that. for file_id in old_tree: if file_id in new_tree: kind = old_inv.get_file_kind(file_id) assert kind == new_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ 'invalid file kind %r' % kind if kind == 'root_directory': continue old_path = old_inv.id2path(file_id) new_path = new_inv.id2path(file_id) if specific_files: if (not is_inside_any(specific_files, old_path) and not is_inside_any(specific_files, new_path)): continue if kind == 'file': old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False # TODO: Can possibly avoid calculating path strings if the # two files are unchanged and their names and parents are # the same and the parents are unchanged all the way up. # May not be worthwhile. if old_path != new_path: delta.renamed.append((old_path, new_path, file_id, kind, text_modified)) elif text_modified: delta.modified.append((new_path, file_id, kind)) elif want_unchanged: delta.unchanged.append((new_path, file_id, kind)) else: kind = old_inv.get_file_kind(file_id) old_path = old_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, old_path): continue delta.removed.append((old_path, file_id, kind)) mutter('start looking for new files') for file_id in new_inv: if file_id in old_inv: continue new_path = new_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, new_path): continue kind = new_inv.get_file_kind(file_id) delta.added.append((new_path, file_id, kind)) delta.removed.sort() delta.added.sort() delta.renamed.sort() delta.modified.sort() delta.unchanged.sort() return delta commit refs/heads/master mark :748 committer Martin Pool 1119427001 +1000 data 10 - Fix typo from :747 M 644 inline bzrlib/selftest/__init__.py data 6748 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") raise class CommandFailed(Exception): pass class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr, so that we can run it # through a specified Python intepreter OVERRIDE_PYTHON = None # to run with alternative python 'python' BZRPATH = 'bzr' def formcmd(self, cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = self.BZRPATH if self.OVERRIDE_PYTHON: cmd.insert(0, self.OVERRIDE_PYTHON) self.log('$ %r' % cmd) return cmd def runcmd(self, cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = self.formcmd(cmd) self.log('$ ' + ' '.join(cmd)) actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(self, cmd, retcode=0): cmd = self.formcmd(cmd) child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG) outd, errd = child.communicate() self.log(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def log(self, msg): """Log a message to a progress file""" print >>self.TEST_LOG, msg class InTempDir(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.test_dir = os.path.join(self.TEST_ROOT, self.__class__.__name__) os.mkdir(self.test_dir) os.chdir(self.test_dir) def tearDown(self): import os os.chdir(self.TEST_ROOT) class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ def __init__(self, out): self.out = out TestResult.__init__(self) def startTest(self, test): # TODO: Maybe show test.shortDescription somewhere? print >>self.out, '%-60.60s' % test.id(), TestResult.startTest(self, test) def stopTest(self, test): # print TestResult.stopTest(self, test) def addError(self, test, err): print >>self.out, 'ERROR' TestResult.addError(self, test, err) def addFailure(self, test, err): print >>self.out, 'FAILURE' TestResult.addFailure(self, test, err) def addSuccess(self, test): print >>self.out, 'OK' TestResult.addSuccess(self, test) def selftest(): from unittest import TestLoader, TestSuite import bzrlib import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning from doctest import DocTestSuite import os import shutil import time import sys _setup_test_log() _setup_test_dir() print suite = TestSuite() tl = TestLoader() for m in bzrlib.selftest.whitebox, \ bzrlib.selftest.versioning: suite.addTest(tl.loadTestsFromModule(m)) suite.addTest(bzrlib.selftest.blackbox.suite()) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands: suite.addTest(DocTestSuite(m)) # save stdout & stderr so there's no leakage from code-under-test real_stdout = sys.stdout real_stderr = sys.stderr sys.stdout = sys.stderr = TestBase.TEST_LOG try: result = _MyResult(real_stdout) suite.run(result) finally: sys.stdout = real_stdout sys.stderr = real_stderr _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os log_filename = os.path.abspath('testbzr.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "bzr tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_ROOT = os.path.abspath("testbzr.tmp") print '%-30s %s' % ('running tests in', TestBase.TEST_ROOT) if os.path.exists(TestBase.TEST_ROOT): shutil.rmtree(TestBase.TEST_ROOT) os.mkdir(TestBase.TEST_ROOT) os.chdir(TestBase.TEST_ROOT) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_ROOT, '.bzr')) def _show_results(result): for case, tb in result.errors: _show_test_failure('ERROR', case, tb) for case, tb in result.failures: _show_test_failure('FAILURE', case, tb) print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, tb): print (kind + '! ').ljust(60, '-') print case desc = case.shortDescription() if desc: print ' (%s)' % desc print tb print ''.ljust(60, '-') commit refs/heads/master mark :749 committer Martin Pool 1119427011 +1000 data 26 - More tests for bzr mkdir from :748 M 644 inline bzrlib/selftest/versioning.py data 1497 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tests of simple versioning operations""" from bzrlib.selftest import InTempDir class Mkdir(InTempDir): def runTest(self): """Basic 'bzr mkdir' operation""" from bzrlib.commands import run_bzr import os run_bzr(['bzr', 'init']) run_bzr(['bzr', 'mkdir', 'foo']) self.assert_(os.path.isdir('foo')) self.assertRaises(OSError, run_bzr, ['bzr', 'mkdir', 'foo']) from bzrlib.diff import compare_trees, TreeDelta from bzrlib.branch import Branch b = Branch('.') delta = compare_trees(b.basis_tree(), b.working_tree()) self.assertEquals(len(delta.added), 1) self.assertEquals(delta.added[0][0], 'foo') self.failIf(delta.modified) commit refs/heads/master mark :750 committer Martin Pool 1119427076 +1000 data 38 - stubbed-out tests for python plugins from :749 M 644 inline bzrlib/selftest/plugins.py data 2582 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tests for plugins""" # NOT RUN YET from bzrlib.selftest import InTempDir def PluginTest(InTempDir): """Create an external plugin and test loading.""" def runTest(self): import os orig_help = self.backtick('bzr help commands') # No plugins yet os.mkdir('plugin_test') f = open(os.path.join('plugin_test', 'myplug.py'), 'wt') f.write(PLUGIN_TEXT) f.close() newhelp = backtick('bzr help commands') assert newhelp.startswith('You have been overridden\n') # We added a line, but the rest should work assert newhelp[25:] == help assert backtick('bzr commit -m test') == "I'm sorry dave, you can't do that\n" shutil.rmtree('plugin_test') PLUGIN_TEXT = \ """import bzrlib, bzrlib.commands class cmd_myplug(bzrlib.commands.Command): '''Just a simple test plugin.''' aliases = ['mplg'] def run(self): print 'Hello from my plugin' """) f.close() os.environ['BZRPLUGINPATH'] = os.path.abspath('plugin_test') help = backtick('bzr help commands') assert help.find('myplug') != -1 assert help.find('Just a simple test plugin.') != -1 assert backtick('bzr myplug') == 'Hello from my plugin\n' assert backtick('bzr mplg') == 'Hello from my plugin\n' f = open(os.path.join('plugin_test', 'override.py'), 'wb') f.write("""import bzrlib, bzrlib.commands class cmd_commit(bzrlib.commands.cmd_commit): '''Commit changes into a new revision.''' def run(self, *args, **kwargs): print "I'm sorry dave, you can't do that" class cmd_help(bzrlib.commands.cmd_help): '''Show help on a command or other topic.''' def run(self, *args, **kwargs): print "You have been overridden" bzrlib.commands.cmd_help.run(self, *args, **kwargs) """ commit refs/heads/master mark :751 committer Martin Pool 1119427650 +1000 data 39 - new TestBase.build_tree helper method from :750 M 644 inline bzrlib/selftest/__init__.py data 7410 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") raise class CommandFailed(Exception): pass class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr, so that we can run it # through a specified Python intepreter OVERRIDE_PYTHON = None # to run with alternative python 'python' BZRPATH = 'bzr' def formcmd(self, cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = self.BZRPATH if self.OVERRIDE_PYTHON: cmd.insert(0, self.OVERRIDE_PYTHON) self.log('$ %r' % cmd) return cmd def runcmd(self, cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = self.formcmd(cmd) self.log('$ ' + ' '.join(cmd)) actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(self, cmd, retcode=0): """Run a command and return its output""" cmd = self.formcmd(cmd) child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG) outd, errd = child.communicate() self.log(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def build_tree(self, shape): """Build a test tree according to a pattern. shape is a sequence of file specifications. If the final character is '/', a directory is created. This doesn't add anything to a branch. """ # XXX: It's OK to just create them using forward slashes on windows? for name in shape: assert isinstance(name, basestring) if name[-1] == '/': os.mkdir(name[:-1]) else: f = file(name, 'wt') print >>f, "contents of", name f.close() def log(self, msg): """Log a message to a progress file""" print >>self.TEST_LOG, msg class InTempDir(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.test_dir = os.path.join(self.TEST_ROOT, self.__class__.__name__) os.mkdir(self.test_dir) os.chdir(self.test_dir) def tearDown(self): import os os.chdir(self.TEST_ROOT) class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ def __init__(self, out): self.out = out TestResult.__init__(self) def startTest(self, test): # TODO: Maybe show test.shortDescription somewhere? print >>self.out, '%-60.60s' % test.id(), TestResult.startTest(self, test) def stopTest(self, test): # print TestResult.stopTest(self, test) def addError(self, test, err): print >>self.out, 'ERROR' TestResult.addError(self, test, err) def addFailure(self, test, err): print >>self.out, 'FAILURE' TestResult.addFailure(self, test, err) def addSuccess(self, test): print >>self.out, 'OK' TestResult.addSuccess(self, test) def selftest(): from unittest import TestLoader, TestSuite import bzrlib import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning from doctest import DocTestSuite import os import shutil import time import sys _setup_test_log() _setup_test_dir() print suite = TestSuite() tl = TestLoader() for m in bzrlib.selftest.whitebox, \ bzrlib.selftest.versioning: suite.addTest(tl.loadTestsFromModule(m)) suite.addTest(bzrlib.selftest.blackbox.suite()) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands: suite.addTest(DocTestSuite(m)) # save stdout & stderr so there's no leakage from code-under-test real_stdout = sys.stdout real_stderr = sys.stderr sys.stdout = sys.stderr = TestBase.TEST_LOG try: result = _MyResult(real_stdout) suite.run(result) finally: sys.stdout = real_stdout sys.stderr = real_stderr _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os log_filename = os.path.abspath('testbzr.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "bzr tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_ROOT = os.path.abspath("testbzr.tmp") print '%-30s %s' % ('running tests in', TestBase.TEST_ROOT) if os.path.exists(TestBase.TEST_ROOT): shutil.rmtree(TestBase.TEST_ROOT) os.mkdir(TestBase.TEST_ROOT) os.chdir(TestBase.TEST_ROOT) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_ROOT, '.bzr')) def _show_results(result): for case, tb in result.errors: _show_test_failure('ERROR', case, tb) for case, tb in result.failures: _show_test_failure('FAILURE', case, tb) print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, tb): print (kind + '! ').ljust(60, '-') print case desc = case.shortDescription() if desc: print ' (%s)' % desc print tb print ''.ljust(60, '-') commit refs/heads/master mark :752 committer Martin Pool 1119427690 +1000 data 20 - fix missing import from :751 M 644 inline bzrlib/selftest/__init__.py data 7428 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") raise class CommandFailed(Exception): pass class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr, so that we can run it # through a specified Python intepreter OVERRIDE_PYTHON = None # to run with alternative python 'python' BZRPATH = 'bzr' def formcmd(self, cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = self.BZRPATH if self.OVERRIDE_PYTHON: cmd.insert(0, self.OVERRIDE_PYTHON) self.log('$ %r' % cmd) return cmd def runcmd(self, cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = self.formcmd(cmd) self.log('$ ' + ' '.join(cmd)) actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(self, cmd, retcode=0): """Run a command and return its output""" cmd = self.formcmd(cmd) child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG) outd, errd = child.communicate() self.log(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def build_tree(self, shape): """Build a test tree according to a pattern. shape is a sequence of file specifications. If the final character is '/', a directory is created. This doesn't add anything to a branch. """ # XXX: It's OK to just create them using forward slashes on windows? import os for name in shape: assert isinstance(name, basestring) if name[-1] == '/': os.mkdir(name[:-1]) else: f = file(name, 'wt') print >>f, "contents of", name f.close() def log(self, msg): """Log a message to a progress file""" print >>self.TEST_LOG, msg class InTempDir(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.test_dir = os.path.join(self.TEST_ROOT, self.__class__.__name__) os.mkdir(self.test_dir) os.chdir(self.test_dir) def tearDown(self): import os os.chdir(self.TEST_ROOT) class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ def __init__(self, out): self.out = out TestResult.__init__(self) def startTest(self, test): # TODO: Maybe show test.shortDescription somewhere? print >>self.out, '%-60.60s' % test.id(), TestResult.startTest(self, test) def stopTest(self, test): # print TestResult.stopTest(self, test) def addError(self, test, err): print >>self.out, 'ERROR' TestResult.addError(self, test, err) def addFailure(self, test, err): print >>self.out, 'FAILURE' TestResult.addFailure(self, test, err) def addSuccess(self, test): print >>self.out, 'OK' TestResult.addSuccess(self, test) def selftest(): from unittest import TestLoader, TestSuite import bzrlib import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning from doctest import DocTestSuite import os import shutil import time import sys _setup_test_log() _setup_test_dir() print suite = TestSuite() tl = TestLoader() for m in bzrlib.selftest.whitebox, \ bzrlib.selftest.versioning: suite.addTest(tl.loadTestsFromModule(m)) suite.addTest(bzrlib.selftest.blackbox.suite()) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands: suite.addTest(DocTestSuite(m)) # save stdout & stderr so there's no leakage from code-under-test real_stdout = sys.stdout real_stderr = sys.stderr sys.stdout = sys.stderr = TestBase.TEST_LOG try: result = _MyResult(real_stdout) suite.run(result) finally: sys.stdout = real_stdout sys.stderr = real_stderr _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os log_filename = os.path.abspath('testbzr.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "bzr tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_ROOT = os.path.abspath("testbzr.tmp") print '%-30s %s' % ('running tests in', TestBase.TEST_ROOT) if os.path.exists(TestBase.TEST_ROOT): shutil.rmtree(TestBase.TEST_ROOT) os.mkdir(TestBase.TEST_ROOT) os.chdir(TestBase.TEST_ROOT) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_ROOT, '.bzr')) def _show_results(result): for case, tb in result.errors: _show_test_failure('ERROR', case, tb) for case, tb in result.failures: _show_test_failure('FAILURE', case, tb) print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, tb): print (kind + '! ').ljust(60, '-') print case desc = case.shortDescription() if desc: print ' (%s)' % desc print tb print ''.ljust(60, '-') commit refs/heads/master mark :753 committer Martin Pool 1119427951 +1000 data 95 - new exception NotVersionedError - raise this from Inventory.add_path if parent isnt versioned from :752 M 644 inline bzrlib/errors.py data 1930 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " ###################################################################### # exceptions class BzrError(StandardError): pass class BzrCheckError(BzrError): pass class BzrCommandError(BzrError): # Error from malformed user command pass class NotBranchError(BzrError): """Specified path is not in a branch""" pass class NotVersionedError(BzrError): """Specified object is not versioned.""" class BadFileKindError(BzrError): """Specified file is of a kind that cannot be added. (For example a symlink or device file.)""" pass class ForbiddenFileError(BzrError): """Cannot operate on a file because it is a control file.""" pass class LockError(Exception): """All exceptions from the lock/unlock functions should be from this exception class. They will be translated as necessary. The original exception is available as e.original_error """ def __init__(self, e=None): self.original_error = e if e: Exception.__init__(self, e) else: Exception.__init__(self) M 644 inline bzrlib/inventory.py data 19411 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # This should really be an id randomly assigned when the tree is # created, but it's not for now. ROOT_ID = "TREE_ROOT" import sys, os.path, types, re try: from cElementTree import Element, ElementTree, SubElement except ImportError: from elementtree.ElementTree import Element, ElementTree, SubElement from bzrlib.xml import XMLMixin from bzrlib.errors import BzrError, BzrCheckError import bzrlib from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') 'TREE_ROOT' >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID)) >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123')) Traceback (most recent call last): ... BzrError: inventory already contains entry with id {2323} >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123')) >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' TODO: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ # TODO: split InventoryEntry into subclasses for files, # directories, etc etc. text_sha1 = None text_size = None def __init__(self, file_id, name, kind, parent_id, text_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID) >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID) Traceback (most recent call last): BzrCheckError: InventoryEntry name 'src/hello.c' is invalid """ if '/' in name or '\\' in name: raise BzrCheckError('InventoryEntry name %r is invalid' % name) self.file_id = file_id self.name = name self.kind = kind self.text_id = text_id self.parent_id = parent_id if kind == 'directory': self.children = {} elif kind == 'file': pass else: raise BzrError("unhandled entry kind %r" % kind) def sorted_children(self): l = self.children.items() l.sort() return l def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.parent_id, text_id=self.text_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size # note that children are *not* copied; they're pulled across when # others are added return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size != None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1']: v = getattr(self, f) if v != None: e.set(f, v) # to be conservative, we don't externalize the root pointers # for now, leaving them as null in the xml form. in a future # version it will be implied by nested elements. if self.parent_id != ROOT_ID: assert isinstance(self.parent_id, basestring) e.set('parent_id', self.parent_id) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' ## original format inventories don't have a parent_id for ## nodes in the root directory, but it's cleaner to use one ## internally. parent_id = elt.get('parent_id') if parent_id == None: parent_id = ROOT_ID self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __eq__(self, other): if not isinstance(other, InventoryEntry): return NotImplemented return (self.file_id == other.file_id) \ and (self.name == other.name) \ and (self.text_sha1 == other.text_sha1) \ and (self.text_size == other.text_size) \ and (self.text_id == other.text_id) \ and (self.parent_id == other.parent_id) \ and (self.kind == other.kind) def __ne__(self, other): return not (self == other) def __hash__(self): raise ValueError('not hashable') class RootEntry(InventoryEntry): def __init__(self, file_id): self.file_id = file_id self.children = {} self.kind = 'root_directory' self.parent_id = None self.name = '' def __eq__(self, other): if not isinstance(other, RootEntry): return NotImplemented return (self.file_id == other.file_id) \ and (self.children == other.children) class Inventory(XMLMixin): """Inventory of versioned files in a tree. This describes which file_id is present at each point in the tree, and possibly the SHA-1 or other information about the file. Entries can be looked up either by path or by file_id. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted, other than through the Inventory API. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID)) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. The inventory is created with a default root directory, with an id of None. """ self.root = RootEntry(ROOT_ID) self._byid = {self.root.file_id: self.root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, from_dir=None): """Return (path, entry) pairs, in order by name.""" if from_dir == None: assert self.root from_dir = self.root elif isinstance(from_dir, basestring): from_dir = self._byid[from_dir] kids = from_dir.children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(from_dir=ie.file_id): yield os.path.join(name, cn), cie def entries(self): """Return list of (path, ie) for all entries except the root. This may be faster than iter_entries. """ accum = [] def descend(dir_ie, dir_path): kids = dir_ie.children.items() kids.sort() for name, ie in kids: child_path = os.path.join(dir_path, name) accum.append((child_path, ie)) if ie.kind == 'directory': descend(ie, child_path) descend(self.root, '') return accum def directories(self): """Return (path, entry) pairs for all directories, including the root. """ accum = [] def descend(parent_ie, parent_path): accum.append((parent_path, parent_ie)) kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory'] kids.sort() for name, child_ie in kids: child_path = os.path.join(parent_path, name) descend(child_ie, child_path) descend(self.root, '') return accum def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID)) >>> inv['123123'].name 'hello.c' """ try: return self._byid[file_id] except KeyError: if file_id == None: raise BzrError("can't look up file_id None") else: raise BzrError("file_id {%s} not in inventory" % file_id) def get_file_kind(self, file_id): return self._byid[file_id].kind def get_child(self, parent_id, filename): return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self._byid: raise BzrError("inventory already contains entry with id {%s}" % entry.file_id) try: parent = self._byid[entry.parent_id] except KeyError: raise BzrError("parent_id {%s} not in inventory" % entry.parent_id) if parent.children.has_key(entry.name): raise BzrError("%s is already versioned" % appendpath(self.id2path(parent.file_id), entry.name)) self._byid[entry.file_id] = entry parent.children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" from bzrlib.errors import NotVersionedError parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: raise BzrError("cannot re-add root of inventory") if file_id == None: file_id = bzrlib.branch.gen_file_id(relpath) parent_path = parts[:-1] parent_id = self.path2id(parent_path) if parent_id == None: raise NotVersionedError(parent_path) ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID)) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __eq__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 True """ if not isinstance(other, Inventory): return NotImplemented if len(self._byid) != len(other._byid): # shortcut: obviously not the same return False return self._byid == other._byid def __ne__(self, other): return not (self == other) def __hash__(self): raise ValueError('not hashable') def get_idpath(self, file_id): """Return a list of file_ids for the path to an entry. The list contains one element for each directory followed by the id of the file itself. So the length of the returned list is equal to the depth of the file in the tree, counting the root directory as depth 1. """ p = [] while file_id != None: try: ie = self._byid[file_id] except KeyError: raise BzrError("file_id {%s} not found in inventory" % file_id) p.insert(0, ie.file_id) file_id = ie.parent_id return p def id2path(self, file_id): """Return as a list the path to file_id.""" # get all names, skipping root p = [self[fid].name for fid in self.get_idpath(file_id)[1:]] return os.sep.join(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. Returns None iff the path is not found. """ if isinstance(name, types.StringTypes): name = splitpath(name) mutter("lookup path %r" % name) parent = self.root for f in name: try: cie = parent.children[f] assert cie.name == f assert cie.parent_id == parent.file_id parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): return self._byid.has_key(file_id) def rename(self, file_id, new_parent_id, new_name): """Move a file within the inventory. This can change either the name, or the parent, or both. This does not move the working file.""" if not is_valid_name(new_name): raise BzrError("not an acceptable filename: %r" % new_name) new_parent = self._byid[new_parent_id] if new_name in new_parent.children: raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id))) new_parent_idpath = self.get_idpath(new_parent_id) if file_id in new_parent_idpath: raise BzrError("cannot move directory %r into a subdirectory of itself, %r" % (self.id2path(file_id), self.id2path(new_parent_id))) file_ie = self._byid[file_id] old_parent = self._byid[file_ie.parent_id] # TODO: Don't leave things messed up if this fails del old_parent.children[file_ie.name] new_parent.children[new_name] = file_ie file_ie.name = new_name file_ie.parent_id = new_parent_id _NAME_RE = re.compile(r'^[^/\\]+$') def is_valid_name(name): return bool(_NAME_RE.match(name)) commit refs/heads/master mark :754 committer Martin Pool 1119427969 +1000 data 74 - new check for attempting to add a file in an unversioned subdirectory from :753 M 644 inline bzrlib/selftest/versioning.py data 2084 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tests of simple versioning operations""" from bzrlib.selftest import InTempDir class Mkdir(InTempDir): def runTest(self): """Basic 'bzr mkdir' operation""" from bzrlib.commands import run_bzr import os run_bzr(['bzr', 'init']) run_bzr(['bzr', 'mkdir', 'foo']) self.assert_(os.path.isdir('foo')) self.assertRaises(OSError, run_bzr, ['bzr', 'mkdir', 'foo']) from bzrlib.diff import compare_trees, TreeDelta from bzrlib.branch import Branch b = Branch('.') delta = compare_trees(b.basis_tree(), b.working_tree()) self.assertEquals(len(delta.added), 1) self.assertEquals(delta.added[0][0], 'foo') self.failIf(delta.modified) class AddInUnversioned(InTempDir): def runTest(self): """Try to add a file in an unversioned directory. smart_add may eventually add the parent as necessary, but simple branch add doesn't do that. """ from bzrlib.branch import Branch import os from bzrlib.errors import NotVersionedError b = Branch('.', init=True) self.build_tree(['foo/', 'foo/hello']) self.assertRaises(NotVersionedError, b.add, 'foo/hello') commit refs/heads/master mark :755 committer Martin Pool 1119430725 +1000 data 67 - new 'plugins' command - fix inverted sense of --no-plugins option from :754 M 644 inline bzrlib/commands.py data 51455 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date plugin_cmds = {} def register_plugin_command(cmd): "Utility function to help register a command" global plugin_cmds k = cmd.__name__ if k.startswith("cmd_"): k_unsquished = _unsquish_command_name(k) else: k_unsquished = k if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = cmd else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r' % sys.modules[cmd.__module__]) def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _get_cmd_dict(plugins_override=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v # If we didn't load plugins, the plugin_cmds dict will be empty if plugins_override: d.update(plugin_cmds) else: d2 = plugin_cmds.copy() d2.update(d) d = d2 return d def get_all_cmds(plugins_override=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(plugins_override=plugins_override).iteritems(): yield k,v def get_cmd_class(cmd, plugins_override=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(plugins_override=plugins_override) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() for opt in self.takes_options: if not opt in OPTIONS: raise BzrError("Unknown option '%s' returned by external command %s" % (opt, path)) # TODO: Is there any way to check takes_args is valid here? self.takes_args = pipe.readline().split() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-usage'" % path) pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-help'" % path) def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: optname = name.replace('_','-') value = kargs[name] if OPTIONS.has_key(optname): # it's an option opts.append('--%s' % optname) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_mkdir(Command): """Create a new versioned directory. This is equivalent to creating the directory and then adding it. """ takes_args = ['dir+'] def run(self, dir_list): import os import bzrlib.branch b = None for d in dir_list: os.mkdir(d) if not b: b = bzrlib.branch.Branch(d) b.add([d], verbose=True) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import errno br_to = Branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if errno == errno.ENOENT: raise if location is None: if stored_loc is None: raise BzrCommandError("No pull location known or specified.") else: print "Using last location: %s" % stored_loc location = stored_loc from branch import find_branch, DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged. Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. To retrieve the branch as of a particular revision, supply the --revision parameter, as in "branch foo/bar -r 5". """ takes_args = ['from_location', 'to_location?'] takes_options = ['revision'] def run(self, from_location, to_location=None, revision=None): import errno from bzrlib.merge import merge from branch import find_branch, DivergedBranches, NoSuchRevision from shutil import rmtree try: br_from = find_branch(from_location) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location.rstrip("/\\")) try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) try: br_to.update_revisions(br_from, stop_revision=revision) except NoSuchRevision: rmtree(to_location) msg = "The branch %s has no revision %d." % (from_location, revision) raise BzrCommandError(msg) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False, ignore_zero=True) from_location = pull_loc(br_from) br_to.controlfile("x-pull", "wb").write(from_location + "\n") def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: raise BzrError("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: raise BzrError("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, exports to a directory (equivalent to --format=dir).""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format'] def run(self, dest, revision=None, format='dir'): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest, format) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_upgrade(Command): """Upgrade branch storage to current format. This should normally be used only after the check command tells you to run it. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.upgrade import upgrade upgrade(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest return int(not selftest()) class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) class cmd_plugins(Command): """List plugins""" hidden = True def run(self): import bzrlib.plugin print dir(bzrlib.plugin) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'update': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) >>> parse_args('log -r 500'.split()) (['log'], {'revision': 500}) >>> parse_args('log -r500:600'.split()) (['log'], {'revision': [500, 600]}) >>> parse_args('log -vr500:600'.split()) (['log'], {'verbose': True, 'revision': [500, 600]}) >>> parse_args('log -rv500:600'.split()) #the r takes an argument Traceback (most recent call last): ... ValueError: invalid literal for int(): v500 """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: raise BzrError('unknown long option %r' % a) else: shortopt = a[1:] if shortopt in SHORT_OPTIONS: # Multi-character options must have a space to delimit # their value optname = SHORT_OPTIONS[shortopt] else: # Single character short options, can be chained, # and have their value appended to their name shortopt = a[1:2] if shortopt not in SHORT_OPTIONS: # We didn't find the multi-character name, and we # didn't find the single char name raise BzrError('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if a[2:]: # There are extra things on this option # see if it is the value, or if it is another # short option optargfn = OPTIONS[optname] if optargfn is None: # This option does not take an argument, so the # next entry is another short option, pack it back # into the list argv.insert(0, '-' + a[2:]) else: # This option takes an argument, so pack it # into the array optarg = a[2:] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? raise BzrError('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: raise BzrError('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: raise BzrError('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def _parse_master_args(argv): """Parse the arguments that always go with the original command. These are things like bzr --no-plugins, etc. There are now 2 types of option flags. Ones that come *before* the command, and ones that come *after* the command. Ones coming *before* the command are applied against all possible commands. And are generally applied before plugins are loaded. The current list are: --builtin Allow plugins to load, but don't let them override builtin commands, they will still be allowed if they do not override a builtin. --no-plugins Don't load any plugins. This lets you get back to official source behavior. --profile Enable the hotspot profile before running the command. For backwards compatibility, this is also a non-master option. --version Spit out the version of bzr that is running and exit. This is also a non-master option. --help Run help and exit, also a non-master option (I think that should stay, though) >>> argv, opts = _parse_master_args(['bzr', '--test']) Traceback (most recent call last): ... BzrCommandError: Invalid master option: 'test' >>> argv, opts = _parse_master_args(['bzr', '--version', 'command']) >>> print argv ['command'] >>> print opts['version'] True >>> argv, opts = _parse_master_args(['bzr', '--profile', 'command', '--more-options']) >>> print argv ['command', '--more-options'] >>> print opts['profile'] True >>> argv, opts = _parse_master_args(['bzr', '--no-plugins', 'command']) >>> print argv ['command'] >>> print opts['no-plugins'] True >>> print opts['profile'] False >>> argv, opts = _parse_master_args(['bzr', 'command', '--profile']) >>> print argv ['command', '--profile'] >>> print opts['profile'] False """ master_opts = {'builtin':False, 'no-plugins':False, 'version':False, 'profile':False, 'help':False } # This is the point where we could hook into argv[0] to determine # what front-end is supposed to be run # For now, we are just ignoring it. cmd_name = argv.pop(0) for arg in argv[:]: if arg[:2] != '--': # at the first non-option, we return the rest break arg = arg[2:] # Remove '--' if arg not in master_opts: # We could say that this is not an error, that we should # just let it be handled by the main section instead raise BzrCommandError('Invalid master option: %r' % arg) argv.pop(0) # We are consuming this entry master_opts[arg] = True return argv, master_opts def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: # some options like --builtin and --no-plugins have special effects argv, master_opts = _parse_master_args(argv) if not master_opts['no-plugins']: bzrlib.load_plugins() args, opts = parse_args(argv) if master_opts['help']: from bzrlib.help import help if argv: help(argv[0]) else: help() return 0 if 'help' in opts: from bzrlib.help import help if args: help(args[0]) else: help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 plugins_override = not (master_opts['builtin']) canonical_cmd, cmd_class = get_cmd_class(cmd, plugins_override=plugins_override) profile = master_opts['profile'] # For backwards compatibility, I would rather stick with --profile being a # master/global option if 'profile' in opts: profile = True del opts['profile'] # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :756 committer Martin Pool 1119431296 +1000 data 108 - plugins documentation; better error reporting when failing to load; keep a list of what has been loaded from :755 M 644 inline bzrlib/commands.py data 51498 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date plugin_cmds = {} def register_plugin_command(cmd): "Utility function to help register a command" global plugin_cmds k = cmd.__name__ if k.startswith("cmd_"): k_unsquished = _unsquish_command_name(k) else: k_unsquished = k if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = cmd else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r' % sys.modules[cmd.__module__]) def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _get_cmd_dict(plugins_override=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v # If we didn't load plugins, the plugin_cmds dict will be empty if plugins_override: d.update(plugin_cmds) else: d2 = plugin_cmds.copy() d2.update(d) d = d2 return d def get_all_cmds(plugins_override=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(plugins_override=plugins_override).iteritems(): yield k,v def get_cmd_class(cmd, plugins_override=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(plugins_override=plugins_override) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() for opt in self.takes_options: if not opt in OPTIONS: raise BzrError("Unknown option '%s' returned by external command %s" % (opt, path)) # TODO: Is there any way to check takes_args is valid here? self.takes_args = pipe.readline().split() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-usage'" % path) pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-help'" % path) def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: optname = name.replace('_','-') value = kargs[name] if OPTIONS.has_key(optname): # it's an option opts.append('--%s' % optname) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_mkdir(Command): """Create a new versioned directory. This is equivalent to creating the directory and then adding it. """ takes_args = ['dir+'] def run(self, dir_list): import os import bzrlib.branch b = None for d in dir_list: os.mkdir(d) if not b: b = bzrlib.branch.Branch(d) b.add([d], verbose=True) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import errno br_to = Branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if errno == errno.ENOENT: raise if location is None: if stored_loc is None: raise BzrCommandError("No pull location known or specified.") else: print "Using last location: %s" % stored_loc location = stored_loc from branch import find_branch, DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged. Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. To retrieve the branch as of a particular revision, supply the --revision parameter, as in "branch foo/bar -r 5". """ takes_args = ['from_location', 'to_location?'] takes_options = ['revision'] def run(self, from_location, to_location=None, revision=None): import errno from bzrlib.merge import merge from branch import find_branch, DivergedBranches, NoSuchRevision from shutil import rmtree try: br_from = find_branch(from_location) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location.rstrip("/\\")) try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) try: br_to.update_revisions(br_from, stop_revision=revision) except NoSuchRevision: rmtree(to_location) msg = "The branch %s has no revision %d." % (from_location, revision) raise BzrCommandError(msg) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False, ignore_zero=True) from_location = pull_loc(br_from) br_to.controlfile("x-pull", "wb").write(from_location + "\n") def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: raise BzrError("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: raise BzrError("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, exports to a directory (equivalent to --format=dir).""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format'] def run(self, dest, revision=None, format='dir'): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest, format) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_upgrade(Command): """Upgrade branch storage to current format. This should normally be used only after the check command tells you to run it. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.upgrade import upgrade upgrade(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest return int(not selftest()) class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) class cmd_plugins(Command): """List plugins""" hidden = True def run(self): import bzrlib.plugin from pprint import pprint pprint(bzrlib.plugin.all_plugins) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'update': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) >>> parse_args('log -r 500'.split()) (['log'], {'revision': 500}) >>> parse_args('log -r500:600'.split()) (['log'], {'revision': [500, 600]}) >>> parse_args('log -vr500:600'.split()) (['log'], {'verbose': True, 'revision': [500, 600]}) >>> parse_args('log -rv500:600'.split()) #the r takes an argument Traceback (most recent call last): ... ValueError: invalid literal for int(): v500 """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: raise BzrError('unknown long option %r' % a) else: shortopt = a[1:] if shortopt in SHORT_OPTIONS: # Multi-character options must have a space to delimit # their value optname = SHORT_OPTIONS[shortopt] else: # Single character short options, can be chained, # and have their value appended to their name shortopt = a[1:2] if shortopt not in SHORT_OPTIONS: # We didn't find the multi-character name, and we # didn't find the single char name raise BzrError('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if a[2:]: # There are extra things on this option # see if it is the value, or if it is another # short option optargfn = OPTIONS[optname] if optargfn is None: # This option does not take an argument, so the # next entry is another short option, pack it back # into the list argv.insert(0, '-' + a[2:]) else: # This option takes an argument, so pack it # into the array optarg = a[2:] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? raise BzrError('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: raise BzrError('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: raise BzrError('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def _parse_master_args(argv): """Parse the arguments that always go with the original command. These are things like bzr --no-plugins, etc. There are now 2 types of option flags. Ones that come *before* the command, and ones that come *after* the command. Ones coming *before* the command are applied against all possible commands. And are generally applied before plugins are loaded. The current list are: --builtin Allow plugins to load, but don't let them override builtin commands, they will still be allowed if they do not override a builtin. --no-plugins Don't load any plugins. This lets you get back to official source behavior. --profile Enable the hotspot profile before running the command. For backwards compatibility, this is also a non-master option. --version Spit out the version of bzr that is running and exit. This is also a non-master option. --help Run help and exit, also a non-master option (I think that should stay, though) >>> argv, opts = _parse_master_args(['bzr', '--test']) Traceback (most recent call last): ... BzrCommandError: Invalid master option: 'test' >>> argv, opts = _parse_master_args(['bzr', '--version', 'command']) >>> print argv ['command'] >>> print opts['version'] True >>> argv, opts = _parse_master_args(['bzr', '--profile', 'command', '--more-options']) >>> print argv ['command', '--more-options'] >>> print opts['profile'] True >>> argv, opts = _parse_master_args(['bzr', '--no-plugins', 'command']) >>> print argv ['command'] >>> print opts['no-plugins'] True >>> print opts['profile'] False >>> argv, opts = _parse_master_args(['bzr', 'command', '--profile']) >>> print argv ['command', '--profile'] >>> print opts['profile'] False """ master_opts = {'builtin':False, 'no-plugins':False, 'version':False, 'profile':False, 'help':False } # This is the point where we could hook into argv[0] to determine # what front-end is supposed to be run # For now, we are just ignoring it. cmd_name = argv.pop(0) for arg in argv[:]: if arg[:2] != '--': # at the first non-option, we return the rest break arg = arg[2:] # Remove '--' if arg not in master_opts: # We could say that this is not an error, that we should # just let it be handled by the main section instead raise BzrCommandError('Invalid master option: %r' % arg) argv.pop(0) # We are consuming this entry master_opts[arg] = True return argv, master_opts def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: # some options like --builtin and --no-plugins have special effects argv, master_opts = _parse_master_args(argv) if not master_opts['no-plugins']: bzrlib.load_plugins() args, opts = parse_args(argv) if master_opts['help']: from bzrlib.help import help if argv: help(argv[0]) else: help() return 0 if 'help' in opts: from bzrlib.help import help if args: help(args[0]) else: help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 plugins_override = not (master_opts['builtin']) canonical_cmd, cmd_class = get_cmd_class(cmd, plugins_override=plugins_override) profile = master_opts['profile'] # For backwards compatibility, I would rather stick with --profile being a # master/global option if 'profile' in opts: profile = True del opts['profile'] # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/plugin.py data 4657 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # This module implements plug-in support. # Any python module in $BZR_PLUGIN_PATH will be imported upon initialization # of bzrlib (and then forgotten about). In the plugin's main body, it should # update any bzrlib registries it wants to extend; for example, to add new # commands, import bzrlib.commands and add your new command to the # plugin_cmds variable. DEFAULT_PLUGIN_PATH = '~/.bzr.conf/plugins' all_plugins = [] def load_plugins(): """ Find all python plugins and load them. Loading a plugin means importing it into the python interpreter. The plugin is expected to make calls to register commands when it's loaded (or perhaps access other hooks in future.) A list of plugs is stored in bzrlib.plugin.all_plugins for future reference. The environment variable BZR_PLUGIN_PATH is considered a delimited set of paths to look through. Each entry is searched for *.py files (and whatever other extensions are used in the platform, such as *.pyd). """ import sys, os, imp try: set except NameError: from sets import Set as set # python2.3 from bzrlib.trace import log_error, mutter, log_exception from bzrlib.errors import BzrError bzrpath = os.environ.get('BZR_PLUGIN_PATH') if not bzrpath: bzrpath = os.path.expanduser(DEFAULT_PLUGIN_PATH) global all_plugins if all_plugins: raise BzrError("plugins already initialized") # The problem with imp.get_suffixes() is that it doesn't include # .pyo which is technically valid # It also means that "testmodule.so" will show up as both test and testmodule # though it is only valid as 'test' # but you should be careful, because "testmodule.py" loads as testmodule. suffixes = imp.get_suffixes() suffixes.append(('.pyo', 'rb', imp.PY_COMPILED)) package_entries = ['__init__.py', '__init__.pyc', '__init__.pyo'] for d in bzrpath.split(os.pathsep): # going through them one by one allows different plugins with the same # filename in different directories in the path mutter('looking for plugins in %s' % d) if not d: continue plugin_names = set() if not os.path.isdir(d): continue for f in os.listdir(d): path = os.path.join(d, f) if os.path.isdir(path): for entry in package_entries: # This directory should be a package, and thus added to # the list if os.path.isfile(os.path.join(path, entry)): break else: # This directory is not a package continue else: for suffix_info in suffixes: if f.endswith(suffix_info[0]): f = f[:-len(suffix_info[0])] if suffix_info[2] == imp.C_EXTENSION and f.endswith('module'): f = f[:-len('module')] break else: continue mutter('add plugin name' + f) plugin_names.add(f) plugin_names = list(plugin_names) plugin_names.sort() for name in plugin_names: try: plugin_info = imp.find_module(name, [d]) mutter('load plugin %r' % (plugin_info,)) try: plugin = imp.load_module('bzrlib.plugin.' + name, *plugin_info) all_plugins.append(plugin_info) finally: if plugin_info[0] is not None: plugin_info[0].close() except Exception, e: log_error('Unable to load plugin %r from %r' % (name, d)) log_error(str(e)) log_exception() commit refs/heads/master mark :757 committer Martin Pool 1119431323 +1000 data 29 - add john's changeset plugin from :756 M 644 inline contrib/plugins/changeset/__init__.py data 3198 #!/usr/bin/env python """\ This is an attempt to take the internal delta object, and represent it as a single-file text-only changeset. This should have commands for both generating a changeset, and for applying a changeset. """ import bzrlib, bzrlib.commands class cmd_changeset(bzrlib.commands.Command): """Generate a bundled up changeset. This changeset contains all of the meta-information of a diff, rather than just containing the patch information. Right now, rollup changesets, or working tree changesets are not supported. This will only generate a changeset that has been committed. You can use "--revision" to specify a certain change to display. """ takes_options = ['revision', 'diff-options'] takes_args = ['file*'] aliases = ['cset'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib import find_branch import gen_changeset import sys if isinstance(revision, (list, tuple)): if len(revision) > 1: raise BzrCommandError('We do not support rollup-changesets yet.') revision = revision[0] if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = find_branch('.') gen_changeset.show_changeset(b, revision, specific_files=file_list, external_diff_options=diff_options, to_file=sys.stdout) class cmd_verify_changeset(bzrlib.commands.Command): """Read a written changeset, and make sure it is valid. """ takes_args = ['filename?'] def run(self, filename=None): import sys, read_changeset if filename is None or filename == '-': f = sys.stdin else: f = open(filename, 'rb') cset_info = read_changeset.read_changeset(f) print cset_info cset = cset_info.get_changeset() print cset.entries class cmd_apply_changeset(bzrlib.commands.Command): """Read in the given changeset, and apply it to the current tree. """ takes_args = ['filename?'] takes_options = [] def run(self, filename=None, reverse=False, auto_commit=False): from bzrlib import find_branch import sys import apply_changeset b = find_branch('.') # Make sure we are in a branch if filename is None or filename == '-': f = sys.stdin else: f = open(filename, 'rb') apply_changeset.apply_changeset(b, f, reverse=reverse, auto_commit=auto_commit) if hasattr(bzrlib.commands, 'register_plugin_cmd'): bzrlib.commands.register_plugin_cmd(cmd_changeset) bzrlib.commands.register_plugin_cmd(cmd_verify_changeset) bzrlib.commands.register_plugin_cmd(cmd_apply_changeset) bzrlib.commands.OPTIONS['reverse'] = None bzrlib.commands.OPTIONS['auto-commit'] = None cmd_apply_changeset.takes_options.append('reverse') cmd_apply_changeset.takes_options.append('auto-commit') M 644 inline contrib/plugins/changeset/apply_changeset.py data 1298 #!/usr/bin/env python """\ This contains the apply changset function for bzr """ import bzrlib def apply_changeset(branch, from_file, reverse=False, auto_commit=False): from bzrlib.changeset import apply_changeset as _apply_changeset from bzrlib.merge import regen_inventory import sys, read_changeset cset_info = read_changeset.read_changeset(from_file) cset = cset_info.get_changeset() inv = {} for file_id in branch.inventory: inv[file_id] = branch.inventory.id2path(file_id) changes = _apply_changeset(cset, inv, branch.base, reverse=reverse) adjust_ids = [] for id, path in changes.iteritems(): if path is not None: if path == '.': path = '' adjust_ids.append((path, id)) branch.set_inventory(regen_inventory(branch, branch.base, adjust_ids)) if auto_commit: from bzrlib.commit import commit if branch.last_patch() == cset_info.precursor: # This patch can be applied directly commit(branch, message = cset_info.message, timestamp=float(cset_info.timestamp), timezone=float(cset_info.timezone), committer=cset_info.committer, rev_id=cset_info.revision) M 644 inline contrib/plugins/changeset/common.py data 346 #!/usr/bin/env python """\ Common entries, like strings, etc, for the changeset reading + writing code. """ header_str = 'Bazaar-NG (bzr) changeset v' version = (0, 0, 5) def get_header(): return [ header_str + '.'.join([str(v) for v in version]), 'This changeset can be applied with bzr apply-changeset', '' ] M 644 inline contrib/plugins/changeset/gen_changeset.py data 12235 #!/usr/bin/env python """\ Just some work for generating a changeset. """ import bzrlib, bzrlib.errors import common from bzrlib.inventory import ROOT_ID try: set except NameError: from sets import Set as set def _canonicalize_revision(branch, revno): """Turn some sort of revision information into a single set of from-to revision ids. A revision id can be None if there is no associated revison. :return: (old, new) """ # This is a little clumsy because revision parsing may return # a single entry, or a list if revno is None: new = branch.last_patch() else: new = branch.lookup_revision(revno) if new is None: raise BzrCommandError('Cannot generate a changset with no commits in tree.') old = branch.get_revision(new).precursor return old, new def _get_trees(branch, revisions): """Get the old and new trees based on revision. """ from bzrlib.tree import EmptyTree if revisions[0] is None: if hasattr(branch, 'get_root_id'): # Watch out for trees with labeled ROOT ids old_tree = EmptyTree(branch.get_root_id) else: old_tree = EmptyTree() else: old_tree = branch.revision_tree(revisions[0]) if revisions[1] is None: # This is for the future, once we support rollup revisions # Or working tree revisions new_tree = branch.working_tree() else: new_tree = branch.revision_tree(revisions[1]) return old_tree, new_tree def _fake_working_revision(branch): """Fake a Revision object for the working tree. This is for the future, to support changesets against the working tree. """ from bzrlib.revision import Revision import time from bzrlib.osutils import local_time_offset, \ username precursor = branch.last_patch() precursor_sha1 = branch.get_revision_sha1(precursor) return Revision(timestamp=time.time(), timezone=local_time_offset(), committer=username(), precursor=precursor, precursor_sha1=precursor_sha1) class MetaInfoHeader(object): """Maintain all of the header information about this changeset. """ def __init__(self, branch, revisions, delta, full_remove=True, full_rename=False, external_diff_options = None, new_tree=None, old_tree=None, old_label = '', new_label = ''): """ :param full_remove: Include the full-text for a delete :param full_rename: Include an add+delete patch for a rename """ self.branch = branch self.delta = delta self.full_remove=full_remove self.full_rename=full_rename self.external_diff_options = external_diff_options self.old_label = old_label self.new_label = new_label self.old_tree = old_tree self.new_tree = new_tree self.to_file = None self.revno = None self.precursor_revno = None self._get_revision_list(revisions) def _get_revision_list(self, revisions): """This generates the list of all revisions from->to. This is for the future, when we support having a rollup changeset. For now, the list should only be one long. """ old_revno = None new_revno = None rh = self.branch.revision_history() for revno, rev in enumerate(rh): if rev == revisions[0]: old_revno = revno if rev == revisions[1]: new_revno = revno self.revision_list = [] if old_revno is None: self.base_revision = None # Effectively the EmptyTree() old_revno = -1 else: self.base_revision = self.branch.get_revision(rh[old_revno]) if new_revno is None: # For the future, when we support working tree changesets. for rev_id in rh[old_revno+1:]: self.revision_list.append(self.branch.get_revision(rev_id)) self.revision_list.append(_fake_working_revision(self.branch)) else: for rev_id in rh[old_revno+1:new_revno+1]: self.revision_list.append(self.branch.get_revision(rev_id)) self.precursor_revno = old_revno self.revno = new_revno def _write(self, txt, key=None): if key: self.to_file.write('# %s: %s\n' % (key, txt)) else: self.to_file.write('# %s\n' % (txt,)) def write_meta_info(self, to_file): """Write out the meta-info portion to the supplied file. :param to_file: Write out the meta information to the supplied file """ self.to_file = to_file self._write_header() self._write_diffs() self._write_footer() def _write_header(self): """Write the stuff that comes before the patches.""" from bzrlib.osutils import username, format_date write = self._write for line in common.get_header(): write(line) # This grabs the current username, what we really want is the # username from the actual patches. #write(username(), key='committer') assert len(self.revision_list) == 1 rev = self.revision_list[0] write(rev.committer, key='committer') write(format_date(rev.timestamp, offset=rev.timezone), key='date') write(str(self.revno), key='revno') if rev.message: self.to_file.write('# message:\n') for line in rev.message.split('\n'): self.to_file.write('# %s\n' % line) write(rev.revision_id, key='revision') if self.base_revision: write(self.base_revision.revision_id, key='precursor') write(str(self.precursor_revno), key='precursor revno') write('') self.to_file.write('\n') def _write_footer(self): """Write the stuff that comes after the patches. This is meant to be more meta-information, which people probably don't want to read, but which is required for proper bzr operation. """ write = self._write write('BEGIN BZR FOOTER') assert len(self.revision_list) == 1 # We only handle single revision entries rev = self.revision_list[0] write(self.branch.get_revision_sha1(rev.revision_id), key='revision sha1') if self.base_revision: rev_id = self.base_revision.revision_id write(self.branch.get_revision_sha1(rev_id), key='precursor sha1') write('%.9f' % rev.timestamp, key='timestamp') write(str(rev.timezone), key='timezone') self._write_ids() write('END BZR FOOTER') def _write_revisions(self): """Not used. Used for writing multiple revisions.""" first = True for rev in self.revision_list: if rev.revision_id is not None: if first: self._write('revisions:') first = False self._write(' '*4 + rev.revision_id + '\t' + self.branch.get_revision_sha1(rev.revision_id)) def _write_ids(self): if hasattr(self.branch, 'get_root_id'): root_id = self.branch.get_root_id() else: root_id = ROOT_ID old_ids = set() new_ids = set() for path, file_id, kind in self.delta.removed: old_ids.add(file_id) for path, file_id, kind in self.delta.added: new_ids.add(file_id) for old_path, new_path, file_id, kind, text_modified in self.delta.renamed: old_ids.add(file_id) new_ids.add(file_id) for path, file_id, kind in self.delta.modified: new_ids.add(file_id) self._write(root_id, key='tree root id') def write_ids(tree, id_set, name): if len(id_set) > 0: self.to_file.write('# %s ids:\n' % name) seen_ids = set([root_id]) while len(id_set) > 0: file_id = id_set.pop() if file_id in seen_ids: continue seen_ids.add(file_id) ie = tree.inventory[file_id] if ie.parent_id not in seen_ids: id_set.add(ie.parent_id) path = tree.inventory.id2path(file_id) self.to_file.write('# %s\t%s\t%s\n' % (path.encode('utf8'), file_id.encode('utf8'), ie.parent_id.encode('utf8'))) write_ids(self.new_tree, new_ids, 'file') write_ids(self.old_tree, old_ids, 'old file') def _write_diffs(self): """Write out the specific diffs""" from bzrlib.diff import internal_diff, external_diff DEVNULL = '/dev/null' if self.external_diff_options: assert isinstance(self.external_diff_options, basestring) opts = self.external_diff_options.split() def diff_file(olab, olines, nlab, nlines, to_file): external_diff(olab, olines, nlab, nlines, to_file, opts) else: diff_file = internal_diff for path, file_id, kind in self.delta.removed: print >>self.to_file, '*** removed %s %r' % (kind, path) if kind == 'file' and self.full_remove: diff_file(self.old_label + path, self.old_tree.get_file(file_id).readlines(), DEVNULL, [], self.to_file) for path, file_id, kind in self.delta.added: print >>self.to_file, '*** added %s %r' % (kind, path) if kind == 'file': diff_file(DEVNULL, [], self.new_label + path, self.new_tree.get_file(file_id).readlines(), self.to_file) for old_path, new_path, file_id, kind, text_modified in self.delta.renamed: print >>self.to_file, '*** renamed %s %r => %r' % (kind, old_path, new_path) if self.full_rename and kind == 'file': diff_file(self.old_label + old_path, self.old_tree.get_file(file_id).readlines(), DEVNULL, [], self.to_file) diff_file(DEVNULL, [], self.new_label + new_path, self.new_tree.get_file(file_id).readlines(), self.to_file) elif text_modified: diff_file(self.old_label + old_path, self.old_tree.get_file(file_id).readlines(), self.new_label + new_path, self.new_tree.get_file(file_id).readlines(), self.to_file) for path, file_id, kind in self.delta.modified: print >>self.to_file, '*** modified %s %r' % (kind, path) if kind == 'file': diff_file(self.old_label + path, self.old_tree.get_file(file_id).readlines(), self.new_label + path, self.new_tree.get_file(file_id).readlines(), self.to_file) def show_changeset(branch, revision=None, specific_files=None, external_diff_options=None, to_file=None, include_full_diff=False): from bzrlib.diff import compare_trees if to_file is None: import sys to_file = sys.stdout revisions = _canonicalize_revision(branch, revision) old_tree, new_tree = _get_trees(branch, revisions) delta = compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=specific_files) meta = MetaInfoHeader(branch, revisions, delta, external_diff_options=external_diff_options, old_tree=old_tree, new_tree=new_tree) meta.write_meta_info(to_file) M 644 inline contrib/plugins/changeset/read_changeset.py data 12587 #!/usr/bin/env python """\ Read in a changeset output, and process it into a Changeset object. """ import bzrlib, bzrlib.changeset import common class BadChangeset(Exception): pass class MalformedHeader(BadChangeset): pass class MalformedPatches(BadChangeset): pass class MalformedFooter(BadChangeset): pass def _unescape(name): """Now we want to find the filename effected. Unfortunately the filename is written out as repr(filename), which means that it surrounds the name with quotes which may be single or double (single is preferred unless there is a single quote in the filename). And some characters will be escaped. TODO: There has to be some pythonic way of undo-ing the representation of a string rather than using eval. """ delimiter = name[0] if name[-1] != delimiter: raise BadChangeset('Could not properly parse the' ' filename: %r' % name) # We need to handle escaped hexadecimals too. return name[1:-1].replace('\"', '"').replace("\'", "'") class ChangesetInfo(object): """This is the intermediate class that gets filled out as the file is read. """ def __init__(self): self.committer = None self.date = None self.message = None self.revno = None self.revision = None self.revision_sha1 = None self.precursor = None self.precursor_sha1 = None self.precursor_revno = None self.timestamp = None self.timezone = None self.tree_root_id = None self.file_ids = None self.old_file_ids = None self.actions = [] #this is the list of things that happened self.id2path = {} # A mapping from file id to path name self.path2id = {} # The reverse mapping self.id2parent = {} # A mapping from a given id to it's parent id self.old_id2path = {} self.old_path2id = {} self.old_id2parent = {} def __str__(self): import pprint return pprint.pformat(self.__dict__) def create_maps(self): """Go through the individual id sections, and generate the id2path and path2id maps. """ # Rather than use an empty path, the changeset code seems # to like to use "./." for the tree root. self.id2path[self.tree_root_id] = './.' self.path2id['./.'] = self.tree_root_id self.id2parent[self.tree_root_id] = bzrlib.changeset.NULL_ID self.old_id2path = self.id2path.copy() self.old_path2id = self.path2id.copy() self.old_id2parent = self.id2parent.copy() if self.file_ids: for info in self.file_ids: path, f_id, parent_id = info.split('\t') self.id2path[f_id] = path self.path2id[path] = f_id self.id2parent[f_id] = parent_id if self.old_file_ids: for info in self.old_file_ids: path, f_id, parent_id = info.split('\t') self.old_id2path[f_id] = path self.old_path2id[path] = f_id self.old_id2parent[f_id] = parent_id def get_changeset(self): """Create a changeset from the data contained within.""" from bzrlib.changeset import Changeset, ChangesetEntry, \ PatchApply, ReplaceContents cset = Changeset() entry = ChangesetEntry(self.tree_root_id, bzrlib.changeset.NULL_ID, './.') cset.add_entry(entry) for info, lines in self.actions: parts = info.split(' ') action = parts[0] kind = parts[1] extra = ' '.join(parts[2:]) if action == 'renamed': old_path, new_path = extra.split(' => ') old_path = _unescape(old_path) new_path = _unescape(new_path) new_id = self.path2id[new_path] old_id = self.old_path2id[old_path] assert old_id == new_id new_parent = self.id2parent[new_id] old_parent = self.old_id2parent[old_id] entry = ChangesetEntry(old_id, old_parent, old_path) entry.new_path = new_path entry.new_parent = new_parent if lines: entry.contents_change = PatchApply(''.join(lines)) elif action == 'removed': old_path = _unescape(extra) old_id = self.old_path2id[old_path] old_parent = self.old_id2parent[old_id] entry = ChangesetEntry(old_id, old_parent, old_path) entry.new_path = None entry.new_parent = None if lines: # Technically a removed should be a ReplaceContents() # Where you need to have the old contents # But at most we have a remove style patch. #entry.contents_change = ReplaceContents() pass elif action == 'added': new_path = _unescape(extra) new_id = self.path2id[new_path] new_parent = self.id2parent[new_id] entry = ChangesetEntry(new_id, new_parent, new_path) entry.path = None entry.parent = None if lines: # Technically an added should be a ReplaceContents() # Where you need to have the old contents # But at most we have an add style patch. #entry.contents_change = ReplaceContents() entry.contents_change = PatchApply(''.join(lines)) elif action == 'modified': new_path = _unescape(extra) new_id = self.path2id[new_path] new_parent = self.id2parent[new_id] entry = ChangesetEntry(new_id, new_parent, new_path) entry.path = None entry.parent = None if lines: # Technically an added should be a ReplaceContents() # Where you need to have the old contents # But at most we have an add style patch. #entry.contents_change = ReplaceContents() entry.contents_change = PatchApply(''.join(lines)) else: raise BadChangeset('Unrecognized action: %r' % action) cset.add_entry(entry) return cset class ChangesetReader(object): """This class reads in a changeset from a file, and returns a Changeset object, which can then be applied against a tree. """ def __init__(self, from_file): """Read in the changeset from the file. :param from_file: A file-like object (must have iterator support). """ object.__init__(self) self.from_file = from_file self.info = ChangesetInfo() # We put the actual inventory ids in the footer, so that the patch # is easier to read for humans. # Unfortunately, that means we need to read everything before we # can create a proper changeset. self._read_header() next_line = self._read_patches() if next_line is not None: self._read_footer(next_line) def get_info(self): """Create the actual changeset object. """ self.info.create_maps() return self.info def _read_header(self): """Read the bzr header""" header = common.get_header() for head_line, line in zip(header, self.from_file): if (line[:2] != '# ' or line[-1] != '\n' or line[2:-1] != head_line): raise MalformedHeader('Did not read the opening' ' header information.') for line in self.from_file: if self._handle_info_line(line) is not None: break def _handle_info_line(self, line, in_footer=False): """Handle reading a single line. This may call itself, in the case that we read_multi, and then had a dangling line on the end. """ # The bzr header is terminated with a blank line # which does not start with # next_line = None if line[:1] == '\n': return 'break' if line[:2] != '# ': raise MalformedHeader('Opening bzr header did not start with #') line = line[2:-1] # Remove the '# ' if not line: return # Ignore blank lines if in_footer and line in ('BEGIN BZR FOOTER', 'END BZR FOOTER'): return loc = line.find(': ') if loc != -1: key = line[:loc] value = line[loc+2:] if not value: value, next_line = self._read_many() else: if line[-1:] == ':': key = line[:-1] value, next_line = self._read_many() else: raise MalformedHeader('While looking for key: value pairs,' ' did not find the colon %r' % (line)) key = key.replace(' ', '_') if hasattr(self.info, key): if getattr(self.info, key) is None: setattr(self.info, key, value) else: raise MalformedHeader('Duplicated Key: %s' % key) else: # What do we do with a key we don't recognize raise MalformedHeader('Unknown Key: %s' % key) if next_line: self._handle_info_line(next_line, in_footer=in_footer) def _read_many(self): """If a line ends with no entry, that means that it should be followed with multiple lines of values. This detects the end of the list, because it will be a line that does not start with '# '. Because it has to read that extra line, it returns the tuple: (values, next_line) """ values = [] for line in self.from_file: if line[:5] != '# ': return values, line values.append(line[5:-1]) return values, None def _read_one_patch(self, first_line=None): """Read in one patch, return the complete patch, along with the next line. :return: action, lines, next_line, do_continue """ first = True action = None def parse_firstline(line): if line[:1] == '#': return None if line[:3] != '***': raise MalformedPatches('The first line of all patches' ' should be a bzr meta line "***"') return line[4:-1] if first_line is not None: action = parse_firstline(first_line) first = False if action is None: return None, [], first_line, False lines = [] for line in self.from_file: if first: action = parse_firstline(line) first = False if action is None: return None, [], line, False else: if line[:3] == '***': return action, lines, line, True elif line[:1] == '#': return action, lines, line, False lines.append(line) return action, lines, None, False def _read_patches(self): next_line = None do_continue = True while do_continue: action, lines, next_line, do_continue = \ self._read_one_patch(next_line) if action is not None: self.info.actions.append((action, lines)) return next_line def _read_footer(self, first_line=None): """Read the rest of the meta information. :param first_line: The previous step iterates past what it can handle. That extra line is given here. """ if first_line is not None: if self._handle_info_line(first_line, in_footer=True) is not None: return for line in self.from_file: if self._handle_info_line(line, in_footer=True) is not None: break def read_changeset(from_file): """Read in a changeset from a filelike object (must have "readline" support), and parse it into a Changeset object. """ cr = ChangesetReader(from_file) info = cr.get_info() return info commit refs/heads/master mark :758 committer Martin Pool 1119432839 +1000 data 19 - trace message fix from :757 M 644 inline bzrlib/plugin.py data 4660 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # This module implements plug-in support. # Any python module in $BZR_PLUGIN_PATH will be imported upon initialization # of bzrlib (and then forgotten about). In the plugin's main body, it should # update any bzrlib registries it wants to extend; for example, to add new # commands, import bzrlib.commands and add your new command to the # plugin_cmds variable. DEFAULT_PLUGIN_PATH = '~/.bzr.conf/plugins' all_plugins = [] def load_plugins(): """ Find all python plugins and load them. Loading a plugin means importing it into the python interpreter. The plugin is expected to make calls to register commands when it's loaded (or perhaps access other hooks in future.) A list of plugs is stored in bzrlib.plugin.all_plugins for future reference. The environment variable BZR_PLUGIN_PATH is considered a delimited set of paths to look through. Each entry is searched for *.py files (and whatever other extensions are used in the platform, such as *.pyd). """ import sys, os, imp try: set except NameError: from sets import Set as set # python2.3 from bzrlib.trace import log_error, mutter, log_exception from bzrlib.errors import BzrError bzrpath = os.environ.get('BZR_PLUGIN_PATH') if not bzrpath: bzrpath = os.path.expanduser(DEFAULT_PLUGIN_PATH) global all_plugins if all_plugins: raise BzrError("plugins already initialized") # The problem with imp.get_suffixes() is that it doesn't include # .pyo which is technically valid # It also means that "testmodule.so" will show up as both test and testmodule # though it is only valid as 'test' # but you should be careful, because "testmodule.py" loads as testmodule. suffixes = imp.get_suffixes() suffixes.append(('.pyo', 'rb', imp.PY_COMPILED)) package_entries = ['__init__.py', '__init__.pyc', '__init__.pyo'] for d in bzrpath.split(os.pathsep): # going through them one by one allows different plugins with the same # filename in different directories in the path mutter('looking for plugins in %s' % d) if not d: continue plugin_names = set() if not os.path.isdir(d): continue for f in os.listdir(d): path = os.path.join(d, f) if os.path.isdir(path): for entry in package_entries: # This directory should be a package, and thus added to # the list if os.path.isfile(os.path.join(path, entry)): break else: # This directory is not a package continue else: for suffix_info in suffixes: if f.endswith(suffix_info[0]): f = f[:-len(suffix_info[0])] if suffix_info[2] == imp.C_EXTENSION and f.endswith('module'): f = f[:-len('module')] break else: continue mutter('add plugin name %s' % f) plugin_names.add(f) plugin_names = list(plugin_names) plugin_names.sort() for name in plugin_names: try: plugin_info = imp.find_module(name, [d]) mutter('load plugin %r' % (plugin_info,)) try: plugin = imp.load_module('bzrlib.plugin.' + name, *plugin_info) all_plugins.append(plugin_info) finally: if plugin_info[0] is not None: plugin_info[0].close() except Exception, e: log_error('Unable to load plugin %r from %r' % (name, d)) log_error(str(e)) log_exception() commit refs/heads/master mark :759 committer Martin Pool 1119432851 +1000 data 33 - fix up register_command() names from :758 M 644 inline bzrlib/commands.py data 51491 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date plugin_cmds = {} def register_command(cmd): "Utility function to help register a command" global plugin_cmds k = cmd.__name__ if k.startswith("cmd_"): k_unsquished = _unsquish_command_name(k) else: k_unsquished = k if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = cmd else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r' % sys.modules[cmd.__module__]) def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _get_cmd_dict(plugins_override=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v # If we didn't load plugins, the plugin_cmds dict will be empty if plugins_override: d.update(plugin_cmds) else: d2 = plugin_cmds.copy() d2.update(d) d = d2 return d def get_all_cmds(plugins_override=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(plugins_override=plugins_override).iteritems(): yield k,v def get_cmd_class(cmd, plugins_override=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(plugins_override=plugins_override) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() for opt in self.takes_options: if not opt in OPTIONS: raise BzrError("Unknown option '%s' returned by external command %s" % (opt, path)) # TODO: Is there any way to check takes_args is valid here? self.takes_args = pipe.readline().split() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-usage'" % path) pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-help'" % path) def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: optname = name.replace('_','-') value = kargs[name] if OPTIONS.has_key(optname): # it's an option opts.append('--%s' % optname) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_mkdir(Command): """Create a new versioned directory. This is equivalent to creating the directory and then adding it. """ takes_args = ['dir+'] def run(self, dir_list): import os import bzrlib.branch b = None for d in dir_list: os.mkdir(d) if not b: b = bzrlib.branch.Branch(d) b.add([d], verbose=True) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import errno br_to = Branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if errno == errno.ENOENT: raise if location is None: if stored_loc is None: raise BzrCommandError("No pull location known or specified.") else: print "Using last location: %s" % stored_loc location = stored_loc from branch import find_branch, DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged. Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. To retrieve the branch as of a particular revision, supply the --revision parameter, as in "branch foo/bar -r 5". """ takes_args = ['from_location', 'to_location?'] takes_options = ['revision'] def run(self, from_location, to_location=None, revision=None): import errno from bzrlib.merge import merge from branch import find_branch, DivergedBranches, NoSuchRevision from shutil import rmtree try: br_from = find_branch(from_location) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location.rstrip("/\\")) try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) try: br_to.update_revisions(br_from, stop_revision=revision) except NoSuchRevision: rmtree(to_location) msg = "The branch %s has no revision %d." % (from_location, revision) raise BzrCommandError(msg) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False, ignore_zero=True) from_location = pull_loc(br_from) br_to.controlfile("x-pull", "wb").write(from_location + "\n") def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: raise BzrError("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: raise BzrError("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, exports to a directory (equivalent to --format=dir).""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format'] def run(self, dest, revision=None, format='dir'): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest, format) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit ## Warning: shadows builtin file() if not message and not file: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_upgrade(Command): """Upgrade branch storage to current format. This should normally be used only after the check command tells you to run it. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.upgrade import upgrade upgrade(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest return int(not selftest()) class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) class cmd_plugins(Command): """List plugins""" hidden = True def run(self): import bzrlib.plugin from pprint import pprint pprint(bzrlib.plugin.all_plugins) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'update': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) >>> parse_args('log -r 500'.split()) (['log'], {'revision': 500}) >>> parse_args('log -r500:600'.split()) (['log'], {'revision': [500, 600]}) >>> parse_args('log -vr500:600'.split()) (['log'], {'verbose': True, 'revision': [500, 600]}) >>> parse_args('log -rv500:600'.split()) #the r takes an argument Traceback (most recent call last): ... ValueError: invalid literal for int(): v500 """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: raise BzrError('unknown long option %r' % a) else: shortopt = a[1:] if shortopt in SHORT_OPTIONS: # Multi-character options must have a space to delimit # their value optname = SHORT_OPTIONS[shortopt] else: # Single character short options, can be chained, # and have their value appended to their name shortopt = a[1:2] if shortopt not in SHORT_OPTIONS: # We didn't find the multi-character name, and we # didn't find the single char name raise BzrError('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if a[2:]: # There are extra things on this option # see if it is the value, or if it is another # short option optargfn = OPTIONS[optname] if optargfn is None: # This option does not take an argument, so the # next entry is another short option, pack it back # into the list argv.insert(0, '-' + a[2:]) else: # This option takes an argument, so pack it # into the array optarg = a[2:] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? raise BzrError('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: raise BzrError('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: raise BzrError('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def _parse_master_args(argv): """Parse the arguments that always go with the original command. These are things like bzr --no-plugins, etc. There are now 2 types of option flags. Ones that come *before* the command, and ones that come *after* the command. Ones coming *before* the command are applied against all possible commands. And are generally applied before plugins are loaded. The current list are: --builtin Allow plugins to load, but don't let them override builtin commands, they will still be allowed if they do not override a builtin. --no-plugins Don't load any plugins. This lets you get back to official source behavior. --profile Enable the hotspot profile before running the command. For backwards compatibility, this is also a non-master option. --version Spit out the version of bzr that is running and exit. This is also a non-master option. --help Run help and exit, also a non-master option (I think that should stay, though) >>> argv, opts = _parse_master_args(['bzr', '--test']) Traceback (most recent call last): ... BzrCommandError: Invalid master option: 'test' >>> argv, opts = _parse_master_args(['bzr', '--version', 'command']) >>> print argv ['command'] >>> print opts['version'] True >>> argv, opts = _parse_master_args(['bzr', '--profile', 'command', '--more-options']) >>> print argv ['command', '--more-options'] >>> print opts['profile'] True >>> argv, opts = _parse_master_args(['bzr', '--no-plugins', 'command']) >>> print argv ['command'] >>> print opts['no-plugins'] True >>> print opts['profile'] False >>> argv, opts = _parse_master_args(['bzr', 'command', '--profile']) >>> print argv ['command', '--profile'] >>> print opts['profile'] False """ master_opts = {'builtin':False, 'no-plugins':False, 'version':False, 'profile':False, 'help':False } # This is the point where we could hook into argv[0] to determine # what front-end is supposed to be run # For now, we are just ignoring it. cmd_name = argv.pop(0) for arg in argv[:]: if arg[:2] != '--': # at the first non-option, we return the rest break arg = arg[2:] # Remove '--' if arg not in master_opts: # We could say that this is not an error, that we should # just let it be handled by the main section instead raise BzrCommandError('Invalid master option: %r' % arg) argv.pop(0) # We are consuming this entry master_opts[arg] = True return argv, master_opts def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: # some options like --builtin and --no-plugins have special effects argv, master_opts = _parse_master_args(argv) if not master_opts['no-plugins']: bzrlib.load_plugins() args, opts = parse_args(argv) if master_opts['help']: from bzrlib.help import help if argv: help(argv[0]) else: help() return 0 if 'help' in opts: from bzrlib.help import help if args: help(args[0]) else: help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 plugins_override = not (master_opts['builtin']) canonical_cmd, cmd_class = get_cmd_class(cmd, plugins_override=plugins_override) profile = master_opts['profile'] # For backwards compatibility, I would rather stick with --profile being a # master/global option if 'profile' in opts: profile = True del opts['profile'] # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline contrib/plugins/changeset/__init__.py data 3195 #!/usr/bin/env python """\ This is an attempt to take the internal delta object, and represent it as a single-file text-only changeset. This should have commands for both generating a changeset, and for applying a changeset. """ import bzrlib, bzrlib.commands class cmd_changeset(bzrlib.commands.Command): """Generate a bundled up changeset. This changeset contains all of the meta-information of a diff, rather than just containing the patch information. Right now, rollup changesets, or working tree changesets are not supported. This will only generate a changeset that has been committed. You can use "--revision" to specify a certain change to display. """ takes_options = ['revision', 'diff-options'] takes_args = ['file*'] aliases = ['cset'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib import find_branch import gen_changeset import sys if isinstance(revision, (list, tuple)): if len(revision) > 1: raise BzrCommandError('We do not support rollup-changesets yet.') revision = revision[0] if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = find_branch('.') gen_changeset.show_changeset(b, revision, specific_files=file_list, external_diff_options=diff_options, to_file=sys.stdout) class cmd_verify_changeset(bzrlib.commands.Command): """Read a written changeset, and make sure it is valid. """ takes_args = ['filename?'] def run(self, filename=None): import sys, read_changeset if filename is None or filename == '-': f = sys.stdin else: f = open(filename, 'rb') cset_info = read_changeset.read_changeset(f) print cset_info cset = cset_info.get_changeset() print cset.entries class cmd_apply_changeset(bzrlib.commands.Command): """Read in the given changeset, and apply it to the current tree. """ takes_args = ['filename?'] takes_options = [] def run(self, filename=None, reverse=False, auto_commit=False): from bzrlib import find_branch import sys import apply_changeset b = find_branch('.') # Make sure we are in a branch if filename is None or filename == '-': f = sys.stdin else: f = open(filename, 'rb') apply_changeset.apply_changeset(b, f, reverse=reverse, auto_commit=auto_commit) ##if hasattr(bzrlib.commands, 'register_plugin_cmd'): ## print dir(bzrlib.commands) bzrlib.commands.register_command(cmd_changeset) bzrlib.commands.register_command(cmd_verify_changeset) bzrlib.commands.register_command(cmd_apply_changeset) bzrlib.commands.OPTIONS['reverse'] = None bzrlib.commands.OPTIONS['auto-commit'] = None cmd_apply_changeset.takes_options.append('reverse') cmd_apply_changeset.takes_options.append('auto-commit') commit refs/heads/master mark :760 committer Martin Pool 1119432899 +1000 data 3 doc from :759 M 644 inline contrib/plugins/changeset/__init__.py data 3328 #!/usr/bin/env python """\ This is an attempt to take the internal delta object, and represent it as a single-file text-only changeset. This should have commands for both generating a changeset, and for applying a changeset. """ import bzrlib, bzrlib.commands class cmd_changeset(bzrlib.commands.Command): """Generate a bundled up changeset. This changeset contains all of the meta-information of a diff, rather than just containing the patch information. If a file is specified, only changes affecting that file are specified; this gives a changeset that is probably not useful. Right now, rollup changesets, or working tree changesets are not supported. This will only generate a changeset that has been committed. You can use "--revision" to specify a certain change to display. """ takes_options = ['revision', 'diff-options'] takes_args = ['file*'] aliases = ['cset'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib import find_branch import gen_changeset import sys if isinstance(revision, (list, tuple)): if len(revision) > 1: raise BzrCommandError('We do not support rollup-changesets yet.') revision = revision[0] if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = find_branch('.') gen_changeset.show_changeset(b, revision, specific_files=file_list, external_diff_options=diff_options, to_file=sys.stdout) class cmd_verify_changeset(bzrlib.commands.Command): """Read a written changeset, and make sure it is valid. """ takes_args = ['filename?'] def run(self, filename=None): import sys, read_changeset if filename is None or filename == '-': f = sys.stdin else: f = open(filename, 'rb') cset_info = read_changeset.read_changeset(f) print cset_info cset = cset_info.get_changeset() print cset.entries class cmd_apply_changeset(bzrlib.commands.Command): """Read in the given changeset, and apply it to the current tree. """ takes_args = ['filename?'] takes_options = [] def run(self, filename=None, reverse=False, auto_commit=False): from bzrlib import find_branch import sys import apply_changeset b = find_branch('.') # Make sure we are in a branch if filename is None or filename == '-': f = sys.stdin else: f = open(filename, 'rb') apply_changeset.apply_changeset(b, f, reverse=reverse, auto_commit=auto_commit) ##if hasattr(bzrlib.commands, 'register_plugin_cmd'): ## print dir(bzrlib.commands) bzrlib.commands.register_command(cmd_changeset) bzrlib.commands.register_command(cmd_verify_changeset) bzrlib.commands.register_command(cmd_apply_changeset) bzrlib.commands.OPTIONS['reverse'] = None bzrlib.commands.OPTIONS['auto-commit'] = None cmd_apply_changeset.takes_options.append('reverse') cmd_apply_changeset.takes_options.append('auto-commit') commit refs/heads/master mark :761 committer Martin Pool 1119432924 +1000 data 62 - move standard plugins from contrib/plugins to just ./plugins from :760 R contrib/plugins plugins commit refs/heads/master mark :762 committer Martin Pool 1119587747 +1000 data 62 - RemoteBranch.get_revision now goes through store abstraction from :761 M 644 inline bzrlib/remotebranch.py data 6888 #! /usr/bin/env python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Proxy object for access to remote branches. At the moment remote branches are only for HTTP and only for read access. """ import gzip from cStringIO import StringIO import urllib2 from errors import BzrError, BzrCheckError from branch import Branch, BZR_BRANCH_FORMAT from trace import mutter # velocitynet.com.au transparently proxies connections and thereby # breaks keep-alive -- sucks! ENABLE_URLGRABBER = True if ENABLE_URLGRABBER: import urlgrabber import urlgrabber.keepalive urlgrabber.keepalive.DEBUG = 0 def get_url(path, compressed=False): try: url = path if compressed: url += '.gz' mutter("grab url %s" % url) url_f = urlgrabber.urlopen(url, keepalive=1, close_connection=0) if not compressed: return url_f else: return gzip.GzipFile(fileobj=StringIO(url_f.read())) except urllib2.URLError, e: raise BzrError("remote fetch failed: %r: %s" % (url, e)) else: def get_url(url, compressed=False): import urllib2 if compressed: url += '.gz' mutter("get_url %s" % url) url_f = urllib2.urlopen(url) if compressed: return gzip.GzipFile(fileobj=StringIO(url_f.read())) else: return url_f def _find_remote_root(url): """Return the prefix URL that corresponds to the branch root.""" orig_url = url while True: try: ff = get_url(url + '/.bzr/branch-format') fmt = ff.read() ff.close() fmt = fmt.rstrip('\r\n') if fmt != BZR_BRANCH_FORMAT.rstrip('\r\n'): raise BzrError("sorry, branch format %r not supported at url %s" % (fmt, url)) return url except urllib2.URLError: pass try: idx = url.rindex('/') except ValueError: raise BzrError('no branch root found for URL %s' % orig_url) url = url[:idx] class RemoteBranch(Branch): def __init__(self, baseurl, find_root=True): """Create new proxy for a remote branch.""" if find_root: self.baseurl = _find_remote_root(baseurl) else: self.baseurl = baseurl self._check_format() self.inventory_store = RemoteStore(baseurl + '/.bzr/inventory-store/') self.text_store = RemoteStore(baseurl + '/.bzr/text-store/') self.revision_store = RemoteStore(baseurl + '/.bzr/revision-store/') def __str__(self): b = getattr(self, 'baseurl', 'undefined') return '%s(%r)' % (self.__class__.__name__, b) __repr__ = __str__ def controlfile(self, filename, mode): if mode not in ('rb', 'rt', 'r'): raise BzrError("file mode %r not supported for remote branches" % mode) return get_url(self.baseurl + '/.bzr/' + filename, False) def lock_read(self): # no locking for remote branches yet pass def lock_write(self): from errors import LockError raise LockError("write lock not supported for remote branch %s" % self.baseurl) def unlock(self): pass def relpath(self, path): if not path.startswith(self.baseurl): raise BzrError('path %r is not under base URL %r' % (path, self.baseurl)) pl = len(self.baseurl) return path[pl:].lstrip('/') def get_revision(self, revision_id): from revision import Revision revf = self.revision_store[revision_id] r = Revision.read_xml(revf) if r.revision_id != revision_id: raise BzrCheckError('revision stored as {%s} actually contains {%s}' % (revision_id, r.revision_id)) return r class RemoteStore(object): def __init__(self, baseurl): self._baseurl = baseurl def _path(self, name): if '/' in name: raise ValueError('invalid store id', name) return self._baseurl + '/' + name def __getitem__(self, fileid): p = self._path(fileid) return get_url(p, compressed=True) def simple_walk(): """For experimental purposes, traverse many parts of a remote branch""" from revision import Revision from branch import Branch from inventory import Inventory got_invs = {} got_texts = {} print 'read history' history = get_url('/.bzr/revision-history').readlines() num_revs = len(history) for i, rev_id in enumerate(history): rev_id = rev_id.rstrip() print 'read revision %d/%d' % (i, num_revs) # python gzip needs a seekable file (!!) but the HTTP response # isn't, so we need to buffer it rev_f = get_url('/.bzr/revision-store/%s' % rev_id, compressed=True) rev = Revision.read_xml(rev_f) print rev.message inv_id = rev.inventory_id if inv_id not in got_invs: print 'get inventory %s' % inv_id inv_f = get_url('/.bzr/inventory-store/%s' % inv_id, compressed=True) inv = Inventory.read_xml(inv_f) print '%4d inventory entries' % len(inv) for path, ie in inv.iter_entries(): text_id = ie.text_id if text_id == None: continue if text_id in got_texts: continue print ' fetch %s text {%s}' % (path, text_id) text_f = get_url('/.bzr/text-store/%s' % text_id, compressed=True) got_texts[text_id] = True got_invs.add[inv_id] = True print '----' def try_me(): BASE_URL = 'http://bazaar-ng.org/bzr/bzr.dev/' b = RemoteBranch(BASE_URL) ## print '\n'.join(b.revision_history()) from log import show_log show_log(b) if __name__ == '__main__': try_me() commit refs/heads/master mark :763 committer Martin Pool 1119588494 +1000 data 73 - Patch from Torsten Marek to take commit messages through an editor. from :762 M 644 inline NEWS data 8820 DEVELOPMENT HEAD NEW FEATURES: * Python plugins, automatically loaded from the directories on BZR_PLUGIN_PATH or ~/.bzr.conf/plugins by default. * New 'bzr mkdir' command. * Commit mesage is fetched from an editor if not given on the command line; patch from Torsten Marek. CHANGES: * New ``bzr upgrade`` command to upgrade the format of a branch, replacing ``bzr check --update``. * Files within store directories are no longer marked readonly on disk. bzr-0.0.5 2005-06-15 CHANGES: * ``bzr`` with no command now shows help rather than giving an error. Suggested by Michael Ellerman. * ``bzr status`` output format changed, because svn-style output doesn't really match the model of bzr. Now files are grouped by status and can be shown with their IDs. ``bzr status --all`` shows all versioned files and unknown files but not ignored files. * ``bzr log`` runs from most-recent to least-recent, the reverse of the previous order. The previous behaviour can be obtained with the ``--forward`` option. * ``bzr inventory`` by default shows only filenames, and also ids if ``--show-ids`` is given, in which case the id is the second field. ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New ``bzr ignore PATTERN`` command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.sw[nop] .git .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. File may be in a branch other than the working directory. * ``bzr log`` and ``bzr root`` can be given an http URL instead of a filename. * Commands can now be defined by external programs or scripts in a directory on $BZRPATH. * New "stat cache" avoids reading the contents of files if they haven't changed since the previous time. * If the Python interpreter is too old, try to find a better one or give an error. Based on a patch from Fredrik Lundh. * New optional parameter ``bzr info [BRANCH]``. * New form ``bzr commit SELECTED`` to commit only selected files. * New form ``bzr log -r FROM:TO`` shows changes in selected range; contributed by John A Meinel. * New option ``bzr diff --diff-options 'OPTS'`` allows passing options through to an external GNU diff. * New option ``bzr add --no-recurse`` to add a directory but not their contents. * ``bzr --version`` now shows more information if bzr is being run from a branch. BUG FIXES: * Fixed diff format so that added and removed files will be handled properly by patch. Fix from Lalo Martins. * Various fixes for files whose names contain spaces or other metacharacters. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. * testbzr requires python2.4, but can be used to test bzr running under a different version. * Tests added for many other changes in this release. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. * New developer commands ``added``, ``modified``. PORTABILITY: * Cope on Windows on python2.3 by using the weaker random seed. 2.4 is now only recommended. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 51904 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date plugin_cmds = {} def register_command(cmd): "Utility function to help register a command" global plugin_cmds k = cmd.__name__ if k.startswith("cmd_"): k_unsquished = _unsquish_command_name(k) else: k_unsquished = k if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = cmd else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r' % sys.modules[cmd.__module__]) def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _get_cmd_dict(plugins_override=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v # If we didn't load plugins, the plugin_cmds dict will be empty if plugins_override: d.update(plugin_cmds) else: d2 = plugin_cmds.copy() d2.update(d) d = d2 return d def get_all_cmds(plugins_override=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(plugins_override=plugins_override).iteritems(): yield k,v def get_cmd_class(cmd, plugins_override=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(plugins_override=plugins_override) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() for opt in self.takes_options: if not opt in OPTIONS: raise BzrError("Unknown option '%s' returned by external command %s" % (opt, path)) # TODO: Is there any way to check takes_args is valid here? self.takes_args = pipe.readline().split() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-usage'" % path) pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-help'" % path) def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: optname = name.replace('_','-') value = kargs[name] if OPTIONS.has_key(optname): # it's an option opts.append('--%s' % optname) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_mkdir(Command): """Create a new versioned directory. This is equivalent to creating the directory and then adding it. """ takes_args = ['dir+'] def run(self, dir_list): import os import bzrlib.branch b = None for d in dir_list: os.mkdir(d) if not b: b = bzrlib.branch.Branch(d) b.add([d], verbose=True) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import errno br_to = Branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if errno == errno.ENOENT: raise if location is None: if stored_loc is None: raise BzrCommandError("No pull location known or specified.") else: print "Using last location: %s" % stored_loc location = stored_loc from branch import find_branch, DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged. Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. To retrieve the branch as of a particular revision, supply the --revision parameter, as in "branch foo/bar -r 5". """ takes_args = ['from_location', 'to_location?'] takes_options = ['revision'] def run(self, from_location, to_location=None, revision=None): import errno from bzrlib.merge import merge from branch import find_branch, DivergedBranches, NoSuchRevision from shutil import rmtree try: br_from = find_branch(from_location) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location.rstrip("/\\")) try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) try: br_to.update_revisions(br_from, stop_revision=revision) except NoSuchRevision: rmtree(to_location) msg = "The branch %s has no revision %d." % (from_location, revision) raise BzrCommandError(msg) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False, ignore_zero=True) from_location = pull_loc(br_from) br_to.controlfile("x-pull", "wb").write(from_location + "\n") def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: raise BzrError("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: raise BzrError("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, exports to a directory (equivalent to --format=dir).""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format'] def run(self, dest, revision=None, format='dir'): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest, format) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit from bzrlib.osutils import get_text_message ## Warning: shadows builtin file() if not message and not file: import cStringIO stdout = sys.stdout catcher = cStringIO.StringIO() sys.stdout = catcher cmd_status({"file_list":selected_list}, {}) info = catcher.getvalue() sys.stdout = stdout message = get_text_message(info) if message is None: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_upgrade(Command): """Upgrade branch storage to current format. This should normally be used only after the check command tells you to run it. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.upgrade import upgrade upgrade(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest return int(not selftest()) class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) class cmd_plugins(Command): """List plugins""" hidden = True def run(self): import bzrlib.plugin from pprint import pprint pprint(bzrlib.plugin.all_plugins) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'update': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) >>> parse_args('log -r 500'.split()) (['log'], {'revision': 500}) >>> parse_args('log -r500:600'.split()) (['log'], {'revision': [500, 600]}) >>> parse_args('log -vr500:600'.split()) (['log'], {'verbose': True, 'revision': [500, 600]}) >>> parse_args('log -rv500:600'.split()) #the r takes an argument Traceback (most recent call last): ... ValueError: invalid literal for int(): v500 """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: raise BzrError('unknown long option %r' % a) else: shortopt = a[1:] if shortopt in SHORT_OPTIONS: # Multi-character options must have a space to delimit # their value optname = SHORT_OPTIONS[shortopt] else: # Single character short options, can be chained, # and have their value appended to their name shortopt = a[1:2] if shortopt not in SHORT_OPTIONS: # We didn't find the multi-character name, and we # didn't find the single char name raise BzrError('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if a[2:]: # There are extra things on this option # see if it is the value, or if it is another # short option optargfn = OPTIONS[optname] if optargfn is None: # This option does not take an argument, so the # next entry is another short option, pack it back # into the list argv.insert(0, '-' + a[2:]) else: # This option takes an argument, so pack it # into the array optarg = a[2:] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? raise BzrError('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: raise BzrError('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: raise BzrError('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def _parse_master_args(argv): """Parse the arguments that always go with the original command. These are things like bzr --no-plugins, etc. There are now 2 types of option flags. Ones that come *before* the command, and ones that come *after* the command. Ones coming *before* the command are applied against all possible commands. And are generally applied before plugins are loaded. The current list are: --builtin Allow plugins to load, but don't let them override builtin commands, they will still be allowed if they do not override a builtin. --no-plugins Don't load any plugins. This lets you get back to official source behavior. --profile Enable the hotspot profile before running the command. For backwards compatibility, this is also a non-master option. --version Spit out the version of bzr that is running and exit. This is also a non-master option. --help Run help and exit, also a non-master option (I think that should stay, though) >>> argv, opts = _parse_master_args(['bzr', '--test']) Traceback (most recent call last): ... BzrCommandError: Invalid master option: 'test' >>> argv, opts = _parse_master_args(['bzr', '--version', 'command']) >>> print argv ['command'] >>> print opts['version'] True >>> argv, opts = _parse_master_args(['bzr', '--profile', 'command', '--more-options']) >>> print argv ['command', '--more-options'] >>> print opts['profile'] True >>> argv, opts = _parse_master_args(['bzr', '--no-plugins', 'command']) >>> print argv ['command'] >>> print opts['no-plugins'] True >>> print opts['profile'] False >>> argv, opts = _parse_master_args(['bzr', 'command', '--profile']) >>> print argv ['command', '--profile'] >>> print opts['profile'] False """ master_opts = {'builtin':False, 'no-plugins':False, 'version':False, 'profile':False, 'help':False } # This is the point where we could hook into argv[0] to determine # what front-end is supposed to be run # For now, we are just ignoring it. cmd_name = argv.pop(0) for arg in argv[:]: if arg[:2] != '--': # at the first non-option, we return the rest break arg = arg[2:] # Remove '--' if arg not in master_opts: # We could say that this is not an error, that we should # just let it be handled by the main section instead raise BzrCommandError('Invalid master option: %r' % arg) argv.pop(0) # We are consuming this entry master_opts[arg] = True return argv, master_opts def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: # some options like --builtin and --no-plugins have special effects argv, master_opts = _parse_master_args(argv) if not master_opts['no-plugins']: bzrlib.load_plugins() args, opts = parse_args(argv) if master_opts['help']: from bzrlib.help import help if argv: help(argv[0]) else: help() return 0 if 'help' in opts: from bzrlib.help import help if args: help(args[0]) else: help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 plugins_override = not (master_opts['builtin']) canonical_cmd, cmd_class = get_cmd_class(cmd, plugins_override=plugins_override) profile = master_opts['profile'] # For backwards compatibility, I would rather stick with --profile being a # master/global option if 'profile' in opts: profile = True del opts['profile'] # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/osutils.py data 12492 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, types, re, time, errno, sys from stat import S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE from bzrlib.errors import BzrError from bzrlib.trace import mutter import bzrlib def make_readonly(filename): """Make a filename read-only.""" # TODO: probably needs to be fixed for windows mod = os.stat(filename).st_mode mod = mod & 0777555 os.chmod(filename, mod) def make_writable(filename): mod = os.stat(filename).st_mode mod = mod | 0200 os.chmod(filename, mod) _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])') def quotefn(f): """Return shell-quoted filename""" ## We could be a bit more terse by using double-quotes etc f = _QUOTE_RE.sub(r'\\\1', f) if f[0] == '~': f[0:1] = r'\~' return f def file_kind(f): mode = os.lstat(f)[ST_MODE] if S_ISREG(mode): return 'file' elif S_ISDIR(mode): return 'directory' elif S_ISLNK(mode): return 'symlink' else: raise BzrError("can't handle file kind with mode %o of %r" % (mode, f)) def kind_marker(kind): if kind == 'file': return '' elif kind == 'directory': return '/' elif kind == 'symlink': return '@' else: raise BzrError('invalid file kind %r' % kind) def isdir(f): """True if f is an accessible directory.""" try: return S_ISDIR(os.lstat(f)[ST_MODE]) except OSError: return False def isfile(f): """True if f is a regular file.""" try: return S_ISREG(os.lstat(f)[ST_MODE]) except OSError: return False def is_inside(dir, fname): """True if fname is inside dir. """ return os.path.commonprefix([dir, fname]) == dir def is_inside_any(dir_list, fname): """True if fname is inside any of given dirs.""" # quick scan for perfect match if fname in dir_list: return True for dirname in dir_list: if is_inside(dirname, fname): return True else: return False def pumpfile(fromfile, tofile): """Copy contents of one file to another.""" tofile.write(fromfile.read()) def uuid(): """Return a new UUID""" try: return file('/proc/sys/kernel/random/uuid').readline().rstrip('\n') except IOError: return chomp(os.popen('uuidgen').readline()) def sha_file(f): import sha if hasattr(f, 'tell'): assert f.tell() == 0 s = sha.new() BUFSIZE = 128<<10 while True: b = f.read(BUFSIZE) if not b: break s.update(b) return s.hexdigest() def sha_string(f): import sha s = sha.new() s.update(f) return s.hexdigest() def fingerprint_file(f): import sha s = sha.new() b = f.read() s.update(b) size = len(b) return {'size': size, 'sha1': s.hexdigest()} def config_dir(): """Return per-user configuration directory. By default this is ~/.bzr.conf/ TODO: Global option --config-dir to override this. """ return os.path.expanduser("~/.bzr.conf") def _auto_user_id(): """Calculate automatic user identification. Returns (realname, email). Only used when none is set in the environment or the id file. This previously used the FQDN as the default domain, but that can be very slow on machines where DNS is broken. So now we simply use the hostname. """ import socket # XXX: Any good way to get real user name on win32? try: import pwd uid = os.getuid() w = pwd.getpwuid(uid) gecos = w.pw_gecos.decode(bzrlib.user_encoding) username = w.pw_name.decode(bzrlib.user_encoding) comma = gecos.find(',') if comma == -1: realname = gecos else: realname = gecos[:comma] if not realname: realname = username except ImportError: import getpass realname = username = getpass.getuser().decode(bzrlib.user_encoding) return realname, (username + '@' + socket.gethostname()) def _get_user_id(): """Return the full user id from a file or environment variable. TODO: Allow taking this from a file in the branch directory too for per-branch ids.""" v = os.environ.get('BZREMAIL') if v: return v.decode(bzrlib.user_encoding) try: return (open(os.path.join(config_dir(), "email")) .read() .decode(bzrlib.user_encoding) .rstrip("\r\n")) except IOError, e: if e.errno != errno.ENOENT: raise e v = os.environ.get('EMAIL') if v: return v.decode(bzrlib.user_encoding) else: return None def username(): """Return email-style username. Something similar to 'Martin Pool ' TODO: Check it's reasonably well-formed. """ v = _get_user_id() if v: return v name, email = _auto_user_id() if name: return '%s <%s>' % (name, email) else: return email _EMAIL_RE = re.compile(r'[\w+.-]+@[\w+.-]+') def user_email(): """Return just the email component of a username.""" e = _get_user_id() if e: m = _EMAIL_RE.search(e) if not m: raise BzrError("%r doesn't seem to contain a reasonable email address" % e) return m.group(0) return _auto_user_id()[1] def compare_files(a, b): """Returns true if equal in contents""" BUFSIZE = 4096 while True: ai = a.read(BUFSIZE) bi = b.read(BUFSIZE) if ai != bi: return False if ai == '': return True def local_time_offset(t=None): """Return offset of local zone from GMT, either at present or at time t.""" # python2.3 localtime() can't take None if t == None: t = time.time() if time.localtime(t).tm_isdst and time.daylight: return -time.altzone else: return -time.timezone def format_date(t, offset=0, timezone='original'): ## TODO: Perhaps a global option to use either universal or local time? ## Or perhaps just let people set $TZ? assert isinstance(t, float) if timezone == 'utc': tt = time.gmtime(t) offset = 0 elif timezone == 'original': if offset == None: offset = 0 tt = time.gmtime(t + offset) elif timezone == 'local': tt = time.localtime(t) offset = local_time_offset(t) else: raise BzrError("unsupported timezone format %r", ['options are "utc", "original", "local"']) return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt) + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)) def compact_date(when): return time.strftime('%Y%m%d%H%M%S', time.gmtime(when)) def filesize(f): """Return size of given open file.""" return os.fstat(f.fileno())[ST_SIZE] if hasattr(os, 'urandom'): # python 2.4 and later rand_bytes = os.urandom elif sys.platform == 'linux2': rand_bytes = file('/dev/urandom', 'rb').read else: # not well seeded, but better than nothing def rand_bytes(n): import random s = '' while n: s += chr(random.randint(0, 255)) n -= 1 return s ## TODO: We could later have path objects that remember their list ## decomposition (might be too tricksy though.) def splitpath(p): """Turn string into list of parts. >>> splitpath('a') ['a'] >>> splitpath('a/b') ['a', 'b'] >>> splitpath('a/./b') ['a', 'b'] >>> splitpath('a/.b') ['a', '.b'] >>> splitpath('a/../b') Traceback (most recent call last): ... BzrError: sorry, '..' not allowed in path """ assert isinstance(p, types.StringTypes) # split on either delimiter because people might use either on # Windows ps = re.split(r'[\\/]', p) rps = [] for f in ps: if f == '..': raise BzrError("sorry, %r not allowed in path" % f) elif (f == '.') or (f == ''): pass else: rps.append(f) return rps def joinpath(p): assert isinstance(p, list) for f in p: if (f == '..') or (f == None) or (f == ''): raise BzrError("sorry, %r not allowed in path" % f) return os.path.join(*p) def appendpath(p1, p2): if p1 == '': return p2 else: return os.path.join(p1, p2) def extern_command(cmd, ignore_errors = False): mutter('external command: %s' % `cmd`) if os.system(cmd): if not ignore_errors: raise BzrError('command failed') def _read_config_value(name): """Read a config value from the file ~/.bzr.conf/ Return None if the file does not exist""" try: f = file(os.path.join(config_dir(), name), "r") return f.read().decode(bzrlib.user_encoding).rstrip("\r\n") except IOError, e: if e.errno == errno.ENOENT: return None raise def _get_editor(): """Return a sequence of possible editor binaries for the current platform""" e = _read_config_value("editor") if e is not None: yield e if os.name == "windows": yield "notepad.exe" elif os.name == "posix": try: yield os.environ["EDITOR"] except KeyError: yield "/usr/bin/vi" def _run_editor(filename): """Try to execute an editor to edit the commit message. Returns True on success, False on failure""" for e in _get_editor(): x = os.spawnvp(os.P_WAIT, e, (e, filename)) if x == 0: return True elif x == 127: continue else: break raise BzrError("Could not start any editor. Please specify $EDITOR or use ~/.bzr.conf/editor") return False def get_text_message(infotext, ignoreline = "default"): import tempfile if ignoreline == "default": ignoreline = "-- This line and the following will be ignored --" try: tmp_fileno, msgfilename = tempfile.mkstemp() msgfile = os.close(tmp_fileno) if infotext is not None and infotext != "": hasinfo = True msgfile = file(msgfilename, "w") msgfile.write("\n\n%s\n\n%s" % (ignoreline, infotext)) msgfile.close() else: hasinfo = False if not _run_editor(msgfilename): return None started = False msg = [] lastline, nlines = 0, 0 for line in file(msgfilename, "r"): stripped_line = line.strip() # strip empty line before the log message starts if not started: if stripped_line != "": started = True else: continue # check for the ignore line only if there # is additional information at the end if hasinfo and stripped_line == ignoreline: break nlines += 1 # keep track of the last line that had some content if stripped_line != "": lastline = nlines msg.append(line) if len(msg) == 0: return None # delete empty lines at the end del msg[lastline:] # add a newline at the end, if needed if not msg[-1].endswith("\n"): return "%s%s" % ("".join(msg), "\n") else: return "".join(msg) finally: # delete the msg file in any case try: os.unlink(msgfilename) except IOError: pass commit refs/heads/master mark :764 committer Martin Pool 1119591265 +1000 data 69 - log messages from a particular test are printed if that test fails from :763 M 644 inline bzrlib/selftest/__init__.py data 7637 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") raise class CommandFailed(Exception): pass class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr, so that we can run it # through a specified Python intepreter OVERRIDE_PYTHON = None # to run with alternative python 'python' BZRPATH = 'bzr' _log_buf = "" def formcmd(self, cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = self.BZRPATH if self.OVERRIDE_PYTHON: cmd.insert(0, self.OVERRIDE_PYTHON) self.log('$ %r' % cmd) return cmd def runcmd(self, cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = self.formcmd(cmd) self.log('$ ' + ' '.join(cmd)) actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(self, cmd, retcode=0): """Run a command and return its output""" cmd = self.formcmd(cmd) child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG) outd, errd = child.communicate() self.log(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def build_tree(self, shape): """Build a test tree according to a pattern. shape is a sequence of file specifications. If the final character is '/', a directory is created. This doesn't add anything to a branch. """ # XXX: It's OK to just create them using forward slashes on windows? import os for name in shape: assert isinstance(name, basestring) if name[-1] == '/': os.mkdir(name[:-1]) else: f = file(name, 'wt') print >>f, "contents of", name f.close() def log(self, msg): """Log a message to a progress file""" self._log_buf = self._log_buf + str(msg) + '\n' print >>self.TEST_LOG, msg class InTempDir(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.test_dir = os.path.join(self.TEST_ROOT, self.__class__.__name__) os.mkdir(self.test_dir) os.chdir(self.test_dir) def tearDown(self): import os os.chdir(self.TEST_ROOT) class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ def __init__(self, out): self.out = out TestResult.__init__(self) def startTest(self, test): # TODO: Maybe show test.shortDescription somewhere? print >>self.out, '%-60.60s' % test.id(), TestResult.startTest(self, test) def stopTest(self, test): # print TestResult.stopTest(self, test) def addError(self, test, err): print >>self.out, 'ERROR' TestResult.addError(self, test, err) def addFailure(self, test, err): print >>self.out, 'FAILURE' TestResult.addFailure(self, test, err) def addSuccess(self, test): print >>self.out, 'OK' TestResult.addSuccess(self, test) def selftest(): from unittest import TestLoader, TestSuite import bzrlib import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning from doctest import DocTestSuite import os import shutil import time import sys _setup_test_log() _setup_test_dir() print suite = TestSuite() tl = TestLoader() for m in bzrlib.selftest.whitebox, \ bzrlib.selftest.versioning: suite.addTest(tl.loadTestsFromModule(m)) suite.addTest(bzrlib.selftest.blackbox.suite()) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands: suite.addTest(DocTestSuite(m)) # save stdout & stderr so there's no leakage from code-under-test real_stdout = sys.stdout real_stderr = sys.stderr sys.stdout = sys.stderr = TestBase.TEST_LOG try: result = _MyResult(real_stdout) suite.run(result) finally: sys.stdout = real_stdout sys.stderr = real_stderr _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os log_filename = os.path.abspath('testbzr.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "bzr tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_ROOT = os.path.abspath("testbzr.tmp") print '%-30s %s' % ('running tests in', TestBase.TEST_ROOT) if os.path.exists(TestBase.TEST_ROOT): shutil.rmtree(TestBase.TEST_ROOT) os.mkdir(TestBase.TEST_ROOT) os.chdir(TestBase.TEST_ROOT) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_ROOT, '.bzr')) def _show_results(result): for case, tb in result.errors: _show_test_failure('ERROR', case, tb) for case, tb in result.failures: _show_test_failure('FAILURE', case, tb) print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, tb): print (kind + '! ').ljust(60, '-') print case desc = case.shortDescription() if desc: print ' (%s)' % desc print tb if isinstance(case, TestBase): print print 'log from this test:' print case._log_buf print ''.ljust(60, '-') commit refs/heads/master mark :765 committer Martin Pool 1119596706 +1000 data 39 - new test helper check_inventory_shape from :764 M 644 inline bzrlib/selftest/__init__.py data 8313 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") raise class CommandFailed(Exception): pass class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr, so that we can run it # through a specified Python intepreter OVERRIDE_PYTHON = None # to run with alternative python 'python' BZRPATH = 'bzr' _log_buf = "" def formcmd(self, cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = self.BZRPATH if self.OVERRIDE_PYTHON: cmd.insert(0, self.OVERRIDE_PYTHON) self.log('$ %r' % cmd) return cmd def runcmd(self, cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = self.formcmd(cmd) self.log('$ ' + ' '.join(cmd)) actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(self, cmd, retcode=0): """Run a command and return its output""" cmd = self.formcmd(cmd) child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG) outd, errd = child.communicate() self.log(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def build_tree(self, shape): """Build a test tree according to a pattern. shape is a sequence of file specifications. If the final character is '/', a directory is created. This doesn't add anything to a branch. """ # XXX: It's OK to just create them using forward slashes on windows? import os for name in shape: assert isinstance(name, basestring) if name[-1] == '/': os.mkdir(name[:-1]) else: f = file(name, 'wt') print >>f, "contents of", name f.close() def log(self, msg): """Log a message to a progress file""" self._log_buf = self._log_buf + str(msg) + '\n' print >>self.TEST_LOG, msg def check_inventory_shape(self, inv, shape): """ Compare an inventory to a list of expected names. Fail if they are not precisely equal. """ extras = [] shape = list(shape) # copy for path, ie in inv.entries(): name = path.replace('\\', '/') if ie.kind == 'dir': name = name + '/' if name in shape: shape.remove(name) else: extras.append(name) if shape: self.fail("expcted paths not found in inventory: %r" % shape) if extras: self.fail("unexpected paths found in inventory: %r" % extras) class InTempDir(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.test_dir = os.path.join(self.TEST_ROOT, self.__class__.__name__) os.mkdir(self.test_dir) os.chdir(self.test_dir) def tearDown(self): import os os.chdir(self.TEST_ROOT) class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ def __init__(self, out): self.out = out TestResult.__init__(self) def startTest(self, test): # TODO: Maybe show test.shortDescription somewhere? print >>self.out, '%-60.60s' % test.id(), TestResult.startTest(self, test) def stopTest(self, test): # print TestResult.stopTest(self, test) def addError(self, test, err): print >>self.out, 'ERROR' TestResult.addError(self, test, err) def addFailure(self, test, err): print >>self.out, 'FAILURE' TestResult.addFailure(self, test, err) def addSuccess(self, test): print >>self.out, 'OK' TestResult.addSuccess(self, test) def selftest(): from unittest import TestLoader, TestSuite import bzrlib import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning from doctest import DocTestSuite import os import shutil import time import sys _setup_test_log() _setup_test_dir() print suite = TestSuite() tl = TestLoader() for m in bzrlib.selftest.whitebox, \ bzrlib.selftest.versioning: suite.addTest(tl.loadTestsFromModule(m)) suite.addTest(bzrlib.selftest.blackbox.suite()) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands: suite.addTest(DocTestSuite(m)) # save stdout & stderr so there's no leakage from code-under-test real_stdout = sys.stdout real_stderr = sys.stderr sys.stdout = sys.stderr = TestBase.TEST_LOG try: result = _MyResult(real_stdout) suite.run(result) finally: sys.stdout = real_stdout sys.stderr = real_stderr _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os log_filename = os.path.abspath('testbzr.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "bzr tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_ROOT = os.path.abspath("testbzr.tmp") print '%-30s %s' % ('running tests in', TestBase.TEST_ROOT) if os.path.exists(TestBase.TEST_ROOT): shutil.rmtree(TestBase.TEST_ROOT) os.mkdir(TestBase.TEST_ROOT) os.chdir(TestBase.TEST_ROOT) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_ROOT, '.bzr')) def _show_results(result): for case, tb in result.errors: _show_test_failure('ERROR', case, tb) for case, tb in result.failures: _show_test_failure('FAILURE', case, tb) print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, tb): print (kind + '! ').ljust(60, '-') print case desc = case.shortDescription() if desc: print ' (%s)' % desc print tb if isinstance(case, TestBase): print print 'log from this test:' print case._log_buf print ''.ljust(60, '-') commit refs/heads/master mark :766 committer Martin Pool 1119596723 +1000 data 3 doc from :765 M 644 inline bzrlib/branch.py data 36038 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_file, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def _relpath(base, path): """Return path relative to base, or raise exception. The path may be either an absolute path or a path relative to the current working directory. Lifted out of Branch.relpath for ease of testing. os.path.commonprefix (python2.4) has a bad bug that it works just on string prefixes, assuming that '/u' is a prefix of '/u2'. This avoids that problem.""" rp = os.path.abspath(path) s = [] head = rp while len(head) >= len(base): if head == base: break head, tail = os.path.split(head) if tail: s.insert(0, tail) else: from errors import NotBranchError raise NotBranchError("path %r is not within branch %r" % (rp, base)) return os.sep.join(s) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head class DivergedBranches(Exception): def __init__(self, branch1, branch2): self.branch1 = branch1 self.branch2 = branch2 Exception.__init__(self, "These branches have diverged.") class NoSuchRevision(BzrError): def __init__(self, branch, revision): self.branch = branch self.revision = revision msg = "Branch %s has no revision %d" % (branch, revision) BzrError.__init__(self, msg) ###################################################################### # branch objects class Branch(object): """Branch holding a history of revisions. base Base directory of the branch. _lock_mode None, or 'r' or 'w' _lock_count If _lock_mode is true, a positive count of the number of times the lock has been taken. _lock Lock object from bzrlib.lock. """ base = None _lock_mode = None _lock_count = None _lock = None def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): from errors import NotBranchError raise NotBranchError("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def __del__(self): if self._lock_mode or self._lock: from warnings import warn warn("branch %r was not explicitly unlocked" % self) self._lock.unlock() def lock_write(self): if self._lock_mode: if self._lock_mode != 'w': from errors import LockError raise LockError("can't upgrade to a write lock from %r" % self._lock_mode) self._lock_count += 1 else: from bzrlib.lock import WriteLock self._lock = WriteLock(self.controlfilename('branch-lock')) self._lock_mode = 'w' self._lock_count = 1 def lock_read(self): if self._lock_mode: assert self._lock_mode in ('r', 'w'), \ "invalid lock mode %r" % self._lock_mode self._lock_count += 1 else: from bzrlib.lock import ReadLock self._lock = ReadLock(self.controlfilename('branch-lock')) self._lock_mode = 'r' self._lock_count = 1 def unlock(self): if not self._lock_mode: from errors import LockError raise LockError('branch %r is not locked' % (self)) if self._lock_count > 1: self._lock_count -= 1 else: self._lock.unlock() self._lock = None self._lock_mode = self._lock_count = None def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" return _relpath(self.base, path) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.\n") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: raise BzrError('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. self.lock_read() try: inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv finally: self.unlock() def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. files List of paths to add, relative to the base of the tree. ids If set, use these instead of automatically generated ids. Must be the same length as the list of files, but may contain None for ids that are to be autogenerated. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) self.lock_write() try: inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): raise BzrError("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: raise BzrError("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) finally: self.unlock() def print_file(self, file, revno): """Print `file` to stdout.""" self.lock_read() try: tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: raise BzrError("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) finally: self.unlock() def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] self.lock_write() try: tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: raise BzrError("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) finally: self.unlock() # FIXME: this doesn't need to be a branch method def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() tmprhname = self.controlfilename('revision-history.tmp') rhname = self.controlfilename('revision-history') f = file(tmprhname, 'wt') rev_history.append(revision_id) f.write('\n'.join(rev_history)) f.write('\n') f.close() if sys.platform == 'win32': os.remove(rhname) os.rename(tmprhname, rhname) def get_revision(self, revision_id): """Return the Revision object for a named revision""" if not revision_id or not isinstance(revision_id, basestring): raise ValueError('invalid revision-id: %r' % revision_id) r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_revision_sha1(self, revision_id): """Hash the stored value of a revision, and return it.""" # In the future, revision entries will be signed. At that # point, it is probably best *not* to include the signature # in the revision hash. Because that lets you re-sign # the revision, (add signatures/remove signatures) and still # have all hash pointers stay consistent. # But for now, just hash the contents. return sha_file(self.revision_store[revision_id]) def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_inventory_sha1(self, inventory_id): """Return the sha1 hash of the inventory entry """ return sha_file(self.inventory_store[inventory_id]) def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self.lock_read() try: return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] finally: self.unlock() def common_ancestor(self, other, self_revno=None, other_revno=None): """ >>> import commit >>> sb = ScratchBranch(files=['foo', 'foo~']) >>> sb.common_ancestor(sb) == (None, None) True >>> commit.commit(sb, "Committing first revision", verbose=False) >>> sb.common_ancestor(sb)[0] 1 >>> clone = sb.clone() >>> commit.commit(sb, "Committing second revision", verbose=False) >>> sb.common_ancestor(sb)[0] 2 >>> sb.common_ancestor(clone)[0] 1 >>> commit.commit(clone, "Committing divergent second revision", ... verbose=False) >>> sb.common_ancestor(clone)[0] 1 >>> sb.common_ancestor(clone) == clone.common_ancestor(sb) True >>> sb.common_ancestor(sb) != clone.common_ancestor(clone) True >>> clone2 = sb.clone() >>> sb.common_ancestor(clone2)[0] 2 >>> sb.common_ancestor(clone2, self_revno=1)[0] 1 >>> sb.common_ancestor(clone2, other_revno=1)[0] 1 """ my_history = self.revision_history() other_history = other.revision_history() if self_revno is None: self_revno = len(my_history) if other_revno is None: other_revno = len(other_history) indices = range(min((self_revno, other_revno))) indices.reverse() for r in indices: if my_history[r] == other_history[r]: return r+1, my_history[r] return None, None def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def missing_revisions(self, other, stop_revision=None): """ If self and other have not diverged, return a list of the revisions present in other, but missing from self. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch() >>> br2 = ScratchBranch() >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [u'REVISION-ID-1'] >>> br2.missing_revisions(br1) [] >>> commit(br1, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-2A") >>> br1.missing_revisions(br2) [u'REVISION-ID-2A'] >>> commit(br1, "lala!", rev_id="REVISION-ID-2B") >>> br1.missing_revisions(br2) Traceback (most recent call last): DivergedBranches: These branches have diverged. """ self_history = self.revision_history() self_len = len(self_history) other_history = other.revision_history() other_len = len(other_history) common_index = min(self_len, other_len) -1 if common_index >= 0 and \ self_history[common_index] != other_history[common_index]: raise DivergedBranches(self, other) if stop_revision is None: stop_revision = other_len elif stop_revision > other_len: raise NoSuchRevision(self, stop_revision) return other_history[self_len:stop_revision] def update_revisions(self, other, stop_revision=None): """Pull in all new revisions from other branch. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch(files=['foo', 'bar']) >>> br1.add('foo') >>> br1.add('bar') >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False) >>> br2 = ScratchBranch() >>> br2.update_revisions(br1) Added 2 texts. Added 1 inventories. Added 1 revisions. >>> br2.revision_history() [u'REVISION-ID-1'] >>> br2.update_revisions(br1) Added 0 texts. Added 0 inventories. Added 0 revisions. >>> br1.text_store.total_size() == br2.text_store.total_size() True """ from bzrlib.progress import ProgressBar pb = ProgressBar() pb.update('comparing histories') revision_ids = self.missing_revisions(other, stop_revision) revisions = [] needed_texts = sets.Set() i = 0 for rev_id in revision_ids: i += 1 pb.update('fetching revision', i, len(revision_ids)) rev = other.get_revision(rev_id) revisions.append(rev) inv = other.get_inventory(str(rev.inventory_id)) for key, entry in inv.iter_entries(): if entry.text_id is None: continue if entry.text_id not in self.text_store: needed_texts.add(entry.text_id) pb.clear() count = self.text_store.copy_multi(other.text_store, needed_texts) print "Added %d texts." % count inventory_ids = [ f.inventory_id for f in revisions ] count = self.inventory_store.copy_multi(other.inventory_store, inventory_ids) print "Added %d inventories." % count revision_ids = [ f.revision_id for f in revisions] count = self.revision_store.copy_multi(other.revision_store, revision_ids) for revision_id in revision_ids: self.append_revision(revision_id) print "Added %d revisions." % count def commit(self, *args, **kw): from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self.lock_write() try: tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): raise BzrError("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): raise BzrError("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: raise BzrError("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): raise BzrError("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': raise BzrError("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self.lock_write() try: ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): raise BzrError("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): raise BzrError("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': raise BzrError("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): raise BzrError("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): raise BzrError("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: raise BzrError("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): raise BzrError("destination %r already exists" % dest_path) if f_id in to_idpath: raise BzrError("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[], base=None): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ init = False if base is None: base = tempfile.mkdtemp() init = True Branch.__init__(self, base, init=init) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def clone(self): """ >>> orig = ScratchBranch(files=["file1", "file2"]) >>> clone = orig.clone() >>> os.path.samefile(orig.base, clone.base) False >>> os.path.isfile(os.path.join(clone.base, "file1")) True """ base = tempfile.mkdtemp() os.rmdir(base) shutil.copytree(self.base, base, symlinks=True) return ScratchBranch(base=base) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: if self.base: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :767 committer Martin Pool 1119596798 +1000 data 108 - files are only reported as modified if their name or parent has changed, not if their parent is renamed from :766 M 644 inline bzrlib/diff.py data 15087 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from trace import mutter from errors import BzrError # TODO: Rather than building a changeset object, we should probably # invoke callbacks on an object. That object can either accumulate a # list, write them out directly, etc etc. def internal_diff(old_label, oldlines, new_label, newlines, to_file): import difflib # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, fromfile=old_label, tofile=new_label) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') to_file.writelines(ud) if nonl: print >>to_file, "\\ No newline at end of file" print >>to_file def external_diff(old_label, oldlines, new_label, newlines, to_file, diff_opts): """Display a diff by calling out to the external diff program.""" import sys if to_file != sys.stdout: raise NotImplementedError("sorry, can't send external diff other than to stdout yet", to_file) # make sure our own output is properly ordered before the diff to_file.flush() from tempfile import NamedTemporaryFile import os oldtmpf = NamedTemporaryFile() newtmpf = NamedTemporaryFile() try: # TODO: perhaps a special case for comparing to or from the empty # sequence; can just use /dev/null on Unix # TODO: if either of the files being compared already exists as a # regular named file (e.g. in the working directory) then we can # compare directly to that, rather than copying it. oldtmpf.writelines(oldlines) newtmpf.writelines(newlines) oldtmpf.flush() newtmpf.flush() if not diff_opts: diff_opts = [] diffcmd = ['diff', '--label', old_label, oldtmpf.name, '--label', new_label, newtmpf.name] # diff only allows one style to be specified; they don't override. # note that some of these take optargs, and the optargs can be # directly appended to the options. # this is only an approximate parser; it doesn't properly understand # the grammar. for s in ['-c', '-u', '-C', '-U', '-e', '--ed', '-q', '--brief', '--normal', '-n', '--rcs', '-y', '--side-by-side', '-D', '--ifdef']: for j in diff_opts: if j.startswith(s): break else: continue break else: diffcmd.append('-u') if diff_opts: diffcmd.extend(diff_opts) rc = os.spawnvp(os.P_WAIT, 'diff', diffcmd) if rc != 0 and rc != 1: # returns 1 if files differ; that's OK if rc < 0: msg = 'signal %d' % (-rc) else: msg = 'exit code %d' % rc raise BzrError('external diff failed with %s; command: %r' % (rc, diffcmd)) finally: oldtmpf.close() # and delete newtmpf.close() def show_diff(b, revision, specific_files, external_diff_options=None): """Shortcut for showing the diff to the working tree. b Branch. revision None for each, or otherwise the old revision to compare against. The more general form is show_diff_trees(), where the caller supplies any two trees. """ import sys if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() show_diff_trees(old_tree, new_tree, sys.stdout, specific_files, external_diff_options) def show_diff_trees(old_tree, new_tree, to_file, specific_files=None, external_diff_options=None): """Show in text form the changes from one tree to another. to_files If set, include only changes to these files. external_diff_options If set, use an external GNU diff and pass these options. """ # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. if external_diff_options: assert isinstance(external_diff_options, basestring) opts = external_diff_options.split() def diff_file(olab, olines, nlab, nlines, to_file): external_diff(olab, olines, nlab, nlines, to_file, opts) else: diff_file = internal_diff delta = compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=specific_files) for path, file_id, kind in delta.removed: print >>to_file, '*** removed %s %r' % (kind, path) if kind == 'file': diff_file(old_label + path, old_tree.get_file(file_id).readlines(), DEVNULL, [], to_file) for path, file_id, kind in delta.added: print >>to_file, '*** added %s %r' % (kind, path) if kind == 'file': diff_file(DEVNULL, [], new_label + path, new_tree.get_file(file_id).readlines(), to_file) for old_path, new_path, file_id, kind, text_modified in delta.renamed: print >>to_file, '*** renamed %s %r => %r' % (kind, old_path, new_path) if text_modified: diff_file(old_label + old_path, old_tree.get_file(file_id).readlines(), new_label + new_path, new_tree.get_file(file_id).readlines(), to_file) for path, file_id, kind in delta.modified: print >>to_file, '*** modified %s %r' % (kind, path) if kind == 'file': diff_file(old_label + path, old_tree.get_file(file_id).readlines(), new_label + path, new_tree.get_file(file_id).readlines(), to_file) class TreeDelta(object): """Describes changes from one tree to another. Contains four lists: added (path, id, kind) removed (path, id, kind) renamed (oldpath, newpath, id, kind, text_modified) modified (path, id, kind) unchanged (path, id, kind) Each id is listed only once. Files that are both modified and renamed are listed only in renamed, with the text_modified flag true. Files are only considered renamed if their name has changed or their parent directory has changed. Renaming a directory does not count as renaming all its contents. The lists are normally sorted when the delta is created. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] self.unchanged = [] def __eq__(self, other): if not isinstance(other, TreeDelta): return False return self.added == other.added \ and self.removed == other.removed \ and self.renamed == other.renamed \ and self.modified == other.modified \ and self.unchanged == other.unchanged def __ne__(self, other): return not (self == other) def __repr__(self): return "TreeDelta(added=%r, removed=%r, renamed=%r, modified=%r," \ " unchanged=%r)" % (self.added, self.removed, self.renamed, self.modified, self.unchanged) def has_changed(self): changes = len(self.added) + len(self.removed) + len(self.renamed) changes += len(self.modified) return (changes != 0) def touches_file_id(self, file_id): """Return True if file_id is modified by this delta.""" for l in self.added, self.removed, self.modified: for v in l: if v[1] == file_id: return True for v in self.renamed: if v[2] == file_id: return True return False def show(self, to_file, show_ids=False, show_unchanged=False): def show_list(files): for path, fid, kind in files: if kind == 'directory': path += '/' elif kind == 'symlink': path += '@' if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if self.removed: print >>to_file, 'removed:' show_list(self.removed) if self.added: print >>to_file, 'added:' show_list(self.added) if self.renamed: print >>to_file, 'renamed:' for oldpath, newpath, fid, kind, text_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if self.modified: print >>to_file, 'modified:' show_list(self.modified) if show_unchanged and self.unchanged: print >>to_file, 'unchanged:' show_list(self.unchanged) def compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=None): """Describe changes from one tree to another. Returns a TreeDelta with details of added, modified, renamed, and deleted entries. The root entry is specifically exempt. This only considers versioned files. want_unchanged If true, also list files unchanged from one version to the next. specific_files If true, only check for changes to specified names or files within them. """ from osutils import is_inside_any old_inv = old_tree.inventory new_inv = new_tree.inventory delta = TreeDelta() mutter('start compare_trees') # TODO: match for specific files can be rather smarter by finding # the IDs of those files up front and then considering only that. for file_id in old_tree: if file_id in new_tree: kind = old_inv.get_file_kind(file_id) assert kind == new_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ 'invalid file kind %r' % kind if kind == 'root_directory': continue old_path = old_inv.id2path(file_id) new_path = new_inv.id2path(file_id) old_ie = old_inv[file_id] new_ie = new_inv[file_id] if specific_files: if (not is_inside_any(specific_files, old_path) and not is_inside_any(specific_files, new_path)): continue if kind == 'file': old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False # TODO: Can possibly avoid calculating path strings if the # two files are unchanged and their names and parents are # the same and the parents are unchanged all the way up. # May not be worthwhile. if (old_ie.name != new_ie.name or old_ie.parent_id != new_ie.parent_id): delta.renamed.append((old_path, new_path, file_id, kind, text_modified)) elif text_modified: delta.modified.append((new_path, file_id, kind)) elif want_unchanged: delta.unchanged.append((new_path, file_id, kind)) else: kind = old_inv.get_file_kind(file_id) old_path = old_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, old_path): continue delta.removed.append((old_path, file_id, kind)) mutter('start looking for new files') for file_id in new_inv: if file_id in old_inv: continue new_path = new_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, new_path): continue kind = new_inv.get_file_kind(file_id) delta.added.append((new_path, file_id, kind)) delta.removed.sort() delta.added.sort() delta.renamed.sort() delta.modified.sort() delta.unchanged.sort() return delta commit refs/heads/master mark :768 committer Martin Pool 1119603173 +1000 data 40 - start some tests for directory renames from :767 M 644 inline bzrlib/selftest/whitebox.py data 2749 #! /usr/bin/python import os import unittest from bzrlib.selftest import InTempDir, TestBase from bzrlib.branch import ScratchBranch, Branch from bzrlib.errors import NotBranchError class RenameDirs(InTempDir): """Test renaming directories and the files within them.""" def runTest(self): from bzrlib.commit import commit b = Branch('.', init=True) self.build_tree(['dir/', 'dir/sub/', 'dir/sub/file']) b.add(['dir', 'dir/sub', 'dir/sub/file']) b.commit('create initial state') # TODO: lift out to a test helper that checks the shape of # an inventory revid = b.revision_history()[0] self.log('first revision_id is {%s}' % revid) inv = b.get_revision_inventory(revid) self.log('contents of inventory: %r' % inv.entries()) self.check_inventory_shape(inv, ['dir', 'dir/sub', 'dir/sub/file']) class BranchPathTestCase(TestBase): """test for branch path lookups Branch.relpath and bzrlib.branch._relpath do a simple but subtle job: given a path (either relative to cwd or absolute), work out if it is inside a branch and return the path relative to the base. """ def runTest(self): from bzrlib.branch import _relpath import tempfile, shutil savedir = os.getcwdu() dtmp = tempfile.mkdtemp() def rp(p): return _relpath(dtmp, p) try: # check paths inside dtmp while standing outside it self.assertEqual(rp(os.path.join(dtmp, 'foo')), 'foo') # root = nothing self.assertEqual(rp(dtmp), '') self.assertRaises(NotBranchError, rp, '/etc') # now some near-miss operations -- note that # os.path.commonprefix gets these wrong! self.assertRaises(NotBranchError, rp, dtmp.rstrip('\\/') + '2') self.assertRaises(NotBranchError, rp, dtmp.rstrip('\\/') + '2/foo') # now operations based on relpath of files in current # directory, or nearby os.chdir(dtmp) self.assertEqual(rp('foo/bar/quux'), 'foo/bar/quux') self.assertEqual(rp('foo'), 'foo') self.assertEqual(rp('./foo'), 'foo') self.assertEqual(rp(os.path.abspath('foo')), 'foo') self.assertRaises(NotBranchError, rp, '../foo') finally: os.chdir(savedir) shutil.rmtree(dtmp) commit refs/heads/master mark :769 committer Martin Pool 1119603635 +1000 data 52 - append to branch revision history using AtomicFile from :768 M 644 inline bzrlib/branch.py data 35918 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_file, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def _relpath(base, path): """Return path relative to base, or raise exception. The path may be either an absolute path or a path relative to the current working directory. Lifted out of Branch.relpath for ease of testing. os.path.commonprefix (python2.4) has a bad bug that it works just on string prefixes, assuming that '/u' is a prefix of '/u2'. This avoids that problem.""" rp = os.path.abspath(path) s = [] head = rp while len(head) >= len(base): if head == base: break head, tail = os.path.split(head) if tail: s.insert(0, tail) else: from errors import NotBranchError raise NotBranchError("path %r is not within branch %r" % (rp, base)) return os.sep.join(s) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head class DivergedBranches(Exception): def __init__(self, branch1, branch2): self.branch1 = branch1 self.branch2 = branch2 Exception.__init__(self, "These branches have diverged.") class NoSuchRevision(BzrError): def __init__(self, branch, revision): self.branch = branch self.revision = revision msg = "Branch %s has no revision %d" % (branch, revision) BzrError.__init__(self, msg) ###################################################################### # branch objects class Branch(object): """Branch holding a history of revisions. base Base directory of the branch. _lock_mode None, or 'r' or 'w' _lock_count If _lock_mode is true, a positive count of the number of times the lock has been taken. _lock Lock object from bzrlib.lock. """ base = None _lock_mode = None _lock_count = None _lock = None def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): from errors import NotBranchError raise NotBranchError("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def __del__(self): if self._lock_mode or self._lock: from warnings import warn warn("branch %r was not explicitly unlocked" % self) self._lock.unlock() def lock_write(self): if self._lock_mode: if self._lock_mode != 'w': from errors import LockError raise LockError("can't upgrade to a write lock from %r" % self._lock_mode) self._lock_count += 1 else: from bzrlib.lock import WriteLock self._lock = WriteLock(self.controlfilename('branch-lock')) self._lock_mode = 'w' self._lock_count = 1 def lock_read(self): if self._lock_mode: assert self._lock_mode in ('r', 'w'), \ "invalid lock mode %r" % self._lock_mode self._lock_count += 1 else: from bzrlib.lock import ReadLock self._lock = ReadLock(self.controlfilename('branch-lock')) self._lock_mode = 'r' self._lock_count = 1 def unlock(self): if not self._lock_mode: from errors import LockError raise LockError('branch %r is not locked' % (self)) if self._lock_count > 1: self._lock_count -= 1 else: self._lock.unlock() self._lock = None self._lock_mode = self._lock_count = None def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" return _relpath(self.base, path) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.\n") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: raise BzrError('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. self.lock_read() try: inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv finally: self.unlock() def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ ## TODO: factor out to atomicfile? is rename safe on windows? ## TODO: Maybe some kind of clean/dirty marker on inventory? tmpfname = self.controlfilename('inventory.tmp') tmpf = file(tmpfname, 'wb') inv.write_xml(tmpf) tmpf.close() inv_fname = self.controlfilename('inventory') if sys.platform == 'win32': os.remove(inv_fname) os.rename(tmpfname, inv_fname) mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. files List of paths to add, relative to the base of the tree. ids If set, use these instead of automatically generated ids. Must be the same length as the list of files, but may contain None for ids that are to be autogenerated. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) self.lock_write() try: inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): raise BzrError("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: raise BzrError("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) finally: self.unlock() def print_file(self, file, revno): """Print `file` to stdout.""" self.lock_read() try: tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: raise BzrError("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) finally: self.unlock() def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] self.lock_write() try: tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: raise BzrError("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) finally: self.unlock() # FIXME: this doesn't need to be a branch method def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): from bzrlib.atomicfile import AtomicFile mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() + [revision_id] f = AtomicFile(self.controlfilename('revision-history')) try: for rev_id in rev_history: print >>f, rev_id f.commit() finally: f.close() def get_revision(self, revision_id): """Return the Revision object for a named revision""" if not revision_id or not isinstance(revision_id, basestring): raise ValueError('invalid revision-id: %r' % revision_id) r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_revision_sha1(self, revision_id): """Hash the stored value of a revision, and return it.""" # In the future, revision entries will be signed. At that # point, it is probably best *not* to include the signature # in the revision hash. Because that lets you re-sign # the revision, (add signatures/remove signatures) and still # have all hash pointers stay consistent. # But for now, just hash the contents. return sha_file(self.revision_store[revision_id]) def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_inventory_sha1(self, inventory_id): """Return the sha1 hash of the inventory entry """ return sha_file(self.inventory_store[inventory_id]) def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self.lock_read() try: return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] finally: self.unlock() def common_ancestor(self, other, self_revno=None, other_revno=None): """ >>> import commit >>> sb = ScratchBranch(files=['foo', 'foo~']) >>> sb.common_ancestor(sb) == (None, None) True >>> commit.commit(sb, "Committing first revision", verbose=False) >>> sb.common_ancestor(sb)[0] 1 >>> clone = sb.clone() >>> commit.commit(sb, "Committing second revision", verbose=False) >>> sb.common_ancestor(sb)[0] 2 >>> sb.common_ancestor(clone)[0] 1 >>> commit.commit(clone, "Committing divergent second revision", ... verbose=False) >>> sb.common_ancestor(clone)[0] 1 >>> sb.common_ancestor(clone) == clone.common_ancestor(sb) True >>> sb.common_ancestor(sb) != clone.common_ancestor(clone) True >>> clone2 = sb.clone() >>> sb.common_ancestor(clone2)[0] 2 >>> sb.common_ancestor(clone2, self_revno=1)[0] 1 >>> sb.common_ancestor(clone2, other_revno=1)[0] 1 """ my_history = self.revision_history() other_history = other.revision_history() if self_revno is None: self_revno = len(my_history) if other_revno is None: other_revno = len(other_history) indices = range(min((self_revno, other_revno))) indices.reverse() for r in indices: if my_history[r] == other_history[r]: return r+1, my_history[r] return None, None def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def missing_revisions(self, other, stop_revision=None): """ If self and other have not diverged, return a list of the revisions present in other, but missing from self. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch() >>> br2 = ScratchBranch() >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [u'REVISION-ID-1'] >>> br2.missing_revisions(br1) [] >>> commit(br1, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-2A") >>> br1.missing_revisions(br2) [u'REVISION-ID-2A'] >>> commit(br1, "lala!", rev_id="REVISION-ID-2B") >>> br1.missing_revisions(br2) Traceback (most recent call last): DivergedBranches: These branches have diverged. """ self_history = self.revision_history() self_len = len(self_history) other_history = other.revision_history() other_len = len(other_history) common_index = min(self_len, other_len) -1 if common_index >= 0 and \ self_history[common_index] != other_history[common_index]: raise DivergedBranches(self, other) if stop_revision is None: stop_revision = other_len elif stop_revision > other_len: raise NoSuchRevision(self, stop_revision) return other_history[self_len:stop_revision] def update_revisions(self, other, stop_revision=None): """Pull in all new revisions from other branch. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch(files=['foo', 'bar']) >>> br1.add('foo') >>> br1.add('bar') >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False) >>> br2 = ScratchBranch() >>> br2.update_revisions(br1) Added 2 texts. Added 1 inventories. Added 1 revisions. >>> br2.revision_history() [u'REVISION-ID-1'] >>> br2.update_revisions(br1) Added 0 texts. Added 0 inventories. Added 0 revisions. >>> br1.text_store.total_size() == br2.text_store.total_size() True """ from bzrlib.progress import ProgressBar pb = ProgressBar() pb.update('comparing histories') revision_ids = self.missing_revisions(other, stop_revision) revisions = [] needed_texts = sets.Set() i = 0 for rev_id in revision_ids: i += 1 pb.update('fetching revision', i, len(revision_ids)) rev = other.get_revision(rev_id) revisions.append(rev) inv = other.get_inventory(str(rev.inventory_id)) for key, entry in inv.iter_entries(): if entry.text_id is None: continue if entry.text_id not in self.text_store: needed_texts.add(entry.text_id) pb.clear() count = self.text_store.copy_multi(other.text_store, needed_texts) print "Added %d texts." % count inventory_ids = [ f.inventory_id for f in revisions ] count = self.inventory_store.copy_multi(other.inventory_store, inventory_ids) print "Added %d inventories." % count revision_ids = [ f.revision_id for f in revisions] count = self.revision_store.copy_multi(other.revision_store, revision_ids) for revision_id in revision_ids: self.append_revision(revision_id) print "Added %d revisions." % count def commit(self, *args, **kw): from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self.lock_write() try: tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): raise BzrError("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): raise BzrError("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: raise BzrError("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): raise BzrError("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': raise BzrError("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self.lock_write() try: ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): raise BzrError("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): raise BzrError("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': raise BzrError("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): raise BzrError("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): raise BzrError("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: raise BzrError("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): raise BzrError("destination %r already exists" % dest_path) if f_id in to_idpath: raise BzrError("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[], base=None): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ init = False if base is None: base = tempfile.mkdtemp() init = True Branch.__init__(self, base, init=init) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def clone(self): """ >>> orig = ScratchBranch(files=["file1", "file2"]) >>> clone = orig.clone() >>> os.path.samefile(orig.base, clone.base) False >>> os.path.isfile(os.path.join(clone.base, "file1")) True """ base = tempfile.mkdtemp() os.rmdir(base) shutil.copytree(self.base, base, symlinks=True) return ScratchBranch(base=base) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: if self.base: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :770 committer Martin Pool 1119603800 +1000 data 46 - write new working inventory using AtomicFile from :769 M 644 inline bzrlib/branch.py data 35811 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_file, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def _relpath(base, path): """Return path relative to base, or raise exception. The path may be either an absolute path or a path relative to the current working directory. Lifted out of Branch.relpath for ease of testing. os.path.commonprefix (python2.4) has a bad bug that it works just on string prefixes, assuming that '/u' is a prefix of '/u2'. This avoids that problem.""" rp = os.path.abspath(path) s = [] head = rp while len(head) >= len(base): if head == base: break head, tail = os.path.split(head) if tail: s.insert(0, tail) else: from errors import NotBranchError raise NotBranchError("path %r is not within branch %r" % (rp, base)) return os.sep.join(s) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head class DivergedBranches(Exception): def __init__(self, branch1, branch2): self.branch1 = branch1 self.branch2 = branch2 Exception.__init__(self, "These branches have diverged.") class NoSuchRevision(BzrError): def __init__(self, branch, revision): self.branch = branch self.revision = revision msg = "Branch %s has no revision %d" % (branch, revision) BzrError.__init__(self, msg) ###################################################################### # branch objects class Branch(object): """Branch holding a history of revisions. base Base directory of the branch. _lock_mode None, or 'r' or 'w' _lock_count If _lock_mode is true, a positive count of the number of times the lock has been taken. _lock Lock object from bzrlib.lock. """ base = None _lock_mode = None _lock_count = None _lock = None def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): from errors import NotBranchError raise NotBranchError("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def __del__(self): if self._lock_mode or self._lock: from warnings import warn warn("branch %r was not explicitly unlocked" % self) self._lock.unlock() def lock_write(self): if self._lock_mode: if self._lock_mode != 'w': from errors import LockError raise LockError("can't upgrade to a write lock from %r" % self._lock_mode) self._lock_count += 1 else: from bzrlib.lock import WriteLock self._lock = WriteLock(self.controlfilename('branch-lock')) self._lock_mode = 'w' self._lock_count = 1 def lock_read(self): if self._lock_mode: assert self._lock_mode in ('r', 'w'), \ "invalid lock mode %r" % self._lock_mode self._lock_count += 1 else: from bzrlib.lock import ReadLock self._lock = ReadLock(self.controlfilename('branch-lock')) self._lock_mode = 'r' self._lock_count = 1 def unlock(self): if not self._lock_mode: from errors import LockError raise LockError('branch %r is not locked' % (self)) if self._lock_count > 1: self._lock_count -= 1 else: self._lock.unlock() self._lock = None self._lock_mode = self._lock_count = None def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" return _relpath(self.base, path) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.\n") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: raise BzrError('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. self.lock_read() try: inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv finally: self.unlock() def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self.lock_write() try: from bzrlib.atomicfile import AtomicFile f = AtomicFile(self.controlfilename('inventory'), 'wb') try: inv.write_xml(f) f.commit() finally: f.close() finally: self.unlock() mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. files List of paths to add, relative to the base of the tree. ids If set, use these instead of automatically generated ids. Must be the same length as the list of files, but may contain None for ids that are to be autogenerated. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) self.lock_write() try: inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): raise BzrError("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: raise BzrError("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: show_status('A', kind, quotefn(f)) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) finally: self.unlock() def print_file(self, file, revno): """Print `file` to stdout.""" self.lock_read() try: tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: raise BzrError("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) finally: self.unlock() def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] self.lock_write() try: tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: raise BzrError("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) finally: self.unlock() # FIXME: this doesn't need to be a branch method def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): from bzrlib.atomicfile import AtomicFile mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() + [revision_id] f = AtomicFile(self.controlfilename('revision-history')) try: for rev_id in rev_history: print >>f, rev_id f.commit() finally: f.close() def get_revision(self, revision_id): """Return the Revision object for a named revision""" if not revision_id or not isinstance(revision_id, basestring): raise ValueError('invalid revision-id: %r' % revision_id) r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_revision_sha1(self, revision_id): """Hash the stored value of a revision, and return it.""" # In the future, revision entries will be signed. At that # point, it is probably best *not* to include the signature # in the revision hash. Because that lets you re-sign # the revision, (add signatures/remove signatures) and still # have all hash pointers stay consistent. # But for now, just hash the contents. return sha_file(self.revision_store[revision_id]) def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_inventory_sha1(self, inventory_id): """Return the sha1 hash of the inventory entry """ return sha_file(self.inventory_store[inventory_id]) def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self.lock_read() try: return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] finally: self.unlock() def common_ancestor(self, other, self_revno=None, other_revno=None): """ >>> import commit >>> sb = ScratchBranch(files=['foo', 'foo~']) >>> sb.common_ancestor(sb) == (None, None) True >>> commit.commit(sb, "Committing first revision", verbose=False) >>> sb.common_ancestor(sb)[0] 1 >>> clone = sb.clone() >>> commit.commit(sb, "Committing second revision", verbose=False) >>> sb.common_ancestor(sb)[0] 2 >>> sb.common_ancestor(clone)[0] 1 >>> commit.commit(clone, "Committing divergent second revision", ... verbose=False) >>> sb.common_ancestor(clone)[0] 1 >>> sb.common_ancestor(clone) == clone.common_ancestor(sb) True >>> sb.common_ancestor(sb) != clone.common_ancestor(clone) True >>> clone2 = sb.clone() >>> sb.common_ancestor(clone2)[0] 2 >>> sb.common_ancestor(clone2, self_revno=1)[0] 1 >>> sb.common_ancestor(clone2, other_revno=1)[0] 1 """ my_history = self.revision_history() other_history = other.revision_history() if self_revno is None: self_revno = len(my_history) if other_revno is None: other_revno = len(other_history) indices = range(min((self_revno, other_revno))) indices.reverse() for r in indices: if my_history[r] == other_history[r]: return r+1, my_history[r] return None, None def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def missing_revisions(self, other, stop_revision=None): """ If self and other have not diverged, return a list of the revisions present in other, but missing from self. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch() >>> br2 = ScratchBranch() >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [u'REVISION-ID-1'] >>> br2.missing_revisions(br1) [] >>> commit(br1, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-2A") >>> br1.missing_revisions(br2) [u'REVISION-ID-2A'] >>> commit(br1, "lala!", rev_id="REVISION-ID-2B") >>> br1.missing_revisions(br2) Traceback (most recent call last): DivergedBranches: These branches have diverged. """ self_history = self.revision_history() self_len = len(self_history) other_history = other.revision_history() other_len = len(other_history) common_index = min(self_len, other_len) -1 if common_index >= 0 and \ self_history[common_index] != other_history[common_index]: raise DivergedBranches(self, other) if stop_revision is None: stop_revision = other_len elif stop_revision > other_len: raise NoSuchRevision(self, stop_revision) return other_history[self_len:stop_revision] def update_revisions(self, other, stop_revision=None): """Pull in all new revisions from other branch. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch(files=['foo', 'bar']) >>> br1.add('foo') >>> br1.add('bar') >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False) >>> br2 = ScratchBranch() >>> br2.update_revisions(br1) Added 2 texts. Added 1 inventories. Added 1 revisions. >>> br2.revision_history() [u'REVISION-ID-1'] >>> br2.update_revisions(br1) Added 0 texts. Added 0 inventories. Added 0 revisions. >>> br1.text_store.total_size() == br2.text_store.total_size() True """ from bzrlib.progress import ProgressBar pb = ProgressBar() pb.update('comparing histories') revision_ids = self.missing_revisions(other, stop_revision) revisions = [] needed_texts = sets.Set() i = 0 for rev_id in revision_ids: i += 1 pb.update('fetching revision', i, len(revision_ids)) rev = other.get_revision(rev_id) revisions.append(rev) inv = other.get_inventory(str(rev.inventory_id)) for key, entry in inv.iter_entries(): if entry.text_id is None: continue if entry.text_id not in self.text_store: needed_texts.add(entry.text_id) pb.clear() count = self.text_store.copy_multi(other.text_store, needed_texts) print "Added %d texts." % count inventory_ids = [ f.inventory_id for f in revisions ] count = self.inventory_store.copy_multi(other.inventory_store, inventory_ids) print "Added %d inventories." % count revision_ids = [ f.revision_id for f in revisions] count = self.revision_store.copy_multi(other.revision_store, revision_ids) for revision_id in revision_ids: self.append_revision(revision_id) print "Added %d revisions." % count def commit(self, *args, **kw): from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self.lock_write() try: tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): raise BzrError("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): raise BzrError("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: raise BzrError("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): raise BzrError("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': raise BzrError("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self.lock_write() try: ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): raise BzrError("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): raise BzrError("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': raise BzrError("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): raise BzrError("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): raise BzrError("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: raise BzrError("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): raise BzrError("destination %r already exists" % dest_path) if f_id in to_idpath: raise BzrError("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[], base=None): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ init = False if base is None: base = tempfile.mkdtemp() init = True Branch.__init__(self, base, init=init) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def clone(self): """ >>> orig = ScratchBranch(files=["file1", "file2"]) >>> clone = orig.clone() >>> os.path.samefile(orig.base, clone.base) False >>> os.path.isfile(os.path.join(clone.base, "file1")) True """ base = tempfile.mkdtemp() os.rmdir(base) shutil.copytree(self.base, base, symlinks=True) return ScratchBranch(base=base) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: if self.base: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :771 committer Martin Pool 1119604142 +1000 data 33 - more tests of directory renames from :770 M 644 inline bzrlib/selftest/whitebox.py data 3140 #! /usr/bin/python import os import unittest from bzrlib.selftest import InTempDir, TestBase from bzrlib.branch import ScratchBranch, Branch from bzrlib.errors import NotBranchError class RenameDirs(InTempDir): """Test renaming directories and the files within them.""" def runTest(self): from bzrlib.commit import commit b = Branch('.', init=True) self.build_tree(['dir/', 'dir/sub/', 'dir/sub/file']) b.add(['dir', 'dir/sub', 'dir/sub/file']) b.commit('create initial state') # TODO: lift out to a test helper that checks the shape of # an inventory revid = b.revision_history()[0] self.log('first revision_id is {%s}' % revid) inv = b.get_revision_inventory(revid) self.log('contents of inventory: %r' % inv.entries()) self.check_inventory_shape(inv, ['dir', 'dir/sub', 'dir/sub/file']) b.rename_one('dir', 'newdir') self.check_inventory_shape(b.inventory, ['newdir', 'newdir/sub', 'newdir/sub/file']) b.rename_one('newdir/sub', 'newdir/newsub') self.check_inventory_shape(b.inventory, ['newdir', 'newdir/newsub', 'newdir/newsub/file']) class BranchPathTestCase(TestBase): """test for branch path lookups Branch.relpath and bzrlib.branch._relpath do a simple but subtle job: given a path (either relative to cwd or absolute), work out if it is inside a branch and return the path relative to the base. """ def runTest(self): from bzrlib.branch import _relpath import tempfile, shutil savedir = os.getcwdu() dtmp = tempfile.mkdtemp() def rp(p): return _relpath(dtmp, p) try: # check paths inside dtmp while standing outside it self.assertEqual(rp(os.path.join(dtmp, 'foo')), 'foo') # root = nothing self.assertEqual(rp(dtmp), '') self.assertRaises(NotBranchError, rp, '/etc') # now some near-miss operations -- note that # os.path.commonprefix gets these wrong! self.assertRaises(NotBranchError, rp, dtmp.rstrip('\\/') + '2') self.assertRaises(NotBranchError, rp, dtmp.rstrip('\\/') + '2/foo') # now operations based on relpath of files in current # directory, or nearby os.chdir(dtmp) self.assertEqual(rp('foo/bar/quux'), 'foo/bar/quux') self.assertEqual(rp('foo'), 'foo') self.assertEqual(rp('./foo'), 'foo') self.assertEqual(rp(os.path.abspath('foo')), 'foo') self.assertRaises(NotBranchError, rp, '../foo') finally: os.chdir(savedir) shutil.rmtree(dtmp) commit refs/heads/master mark :772 committer Martin Pool 1119604712 +1000 data 36 - fix verbose output from Branch.add from :771 M 644 inline bzrlib/branch.py data 35802 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_file, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def _relpath(base, path): """Return path relative to base, or raise exception. The path may be either an absolute path or a path relative to the current working directory. Lifted out of Branch.relpath for ease of testing. os.path.commonprefix (python2.4) has a bad bug that it works just on string prefixes, assuming that '/u' is a prefix of '/u2'. This avoids that problem.""" rp = os.path.abspath(path) s = [] head = rp while len(head) >= len(base): if head == base: break head, tail = os.path.split(head) if tail: s.insert(0, tail) else: from errors import NotBranchError raise NotBranchError("path %r is not within branch %r" % (rp, base)) return os.sep.join(s) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head class DivergedBranches(Exception): def __init__(self, branch1, branch2): self.branch1 = branch1 self.branch2 = branch2 Exception.__init__(self, "These branches have diverged.") class NoSuchRevision(BzrError): def __init__(self, branch, revision): self.branch = branch self.revision = revision msg = "Branch %s has no revision %d" % (branch, revision) BzrError.__init__(self, msg) ###################################################################### # branch objects class Branch(object): """Branch holding a history of revisions. base Base directory of the branch. _lock_mode None, or 'r' or 'w' _lock_count If _lock_mode is true, a positive count of the number of times the lock has been taken. _lock Lock object from bzrlib.lock. """ base = None _lock_mode = None _lock_count = None _lock = None def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): from errors import NotBranchError raise NotBranchError("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def __del__(self): if self._lock_mode or self._lock: from warnings import warn warn("branch %r was not explicitly unlocked" % self) self._lock.unlock() def lock_write(self): if self._lock_mode: if self._lock_mode != 'w': from errors import LockError raise LockError("can't upgrade to a write lock from %r" % self._lock_mode) self._lock_count += 1 else: from bzrlib.lock import WriteLock self._lock = WriteLock(self.controlfilename('branch-lock')) self._lock_mode = 'w' self._lock_count = 1 def lock_read(self): if self._lock_mode: assert self._lock_mode in ('r', 'w'), \ "invalid lock mode %r" % self._lock_mode self._lock_count += 1 else: from bzrlib.lock import ReadLock self._lock = ReadLock(self.controlfilename('branch-lock')) self._lock_mode = 'r' self._lock_count = 1 def unlock(self): if not self._lock_mode: from errors import LockError raise LockError('branch %r is not locked' % (self)) if self._lock_count > 1: self._lock_count -= 1 else: self._lock.unlock() self._lock = None self._lock_mode = self._lock_count = None def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" return _relpath(self.base, path) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.\n") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: raise BzrError('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. self.lock_read() try: inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv finally: self.unlock() def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self.lock_write() try: from bzrlib.atomicfile import AtomicFile f = AtomicFile(self.controlfilename('inventory'), 'wb') try: inv.write_xml(f) f.commit() finally: f.close() finally: self.unlock() mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. files List of paths to add, relative to the base of the tree. ids If set, use these instead of automatically generated ids. Must be the same length as the list of files, but may contain None for ids that are to be autogenerated. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) self.lock_write() try: inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): raise BzrError("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: raise BzrError("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: print 'added', quotefn(f) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) finally: self.unlock() def print_file(self, file, revno): """Print `file` to stdout.""" self.lock_read() try: tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: raise BzrError("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) finally: self.unlock() def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] self.lock_write() try: tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: raise BzrError("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) finally: self.unlock() # FIXME: this doesn't need to be a branch method def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): from bzrlib.atomicfile import AtomicFile mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() + [revision_id] f = AtomicFile(self.controlfilename('revision-history')) try: for rev_id in rev_history: print >>f, rev_id f.commit() finally: f.close() def get_revision(self, revision_id): """Return the Revision object for a named revision""" if not revision_id or not isinstance(revision_id, basestring): raise ValueError('invalid revision-id: %r' % revision_id) r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_revision_sha1(self, revision_id): """Hash the stored value of a revision, and return it.""" # In the future, revision entries will be signed. At that # point, it is probably best *not* to include the signature # in the revision hash. Because that lets you re-sign # the revision, (add signatures/remove signatures) and still # have all hash pointers stay consistent. # But for now, just hash the contents. return sha_file(self.revision_store[revision_id]) def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_inventory_sha1(self, inventory_id): """Return the sha1 hash of the inventory entry """ return sha_file(self.inventory_store[inventory_id]) def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self.lock_read() try: return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] finally: self.unlock() def common_ancestor(self, other, self_revno=None, other_revno=None): """ >>> import commit >>> sb = ScratchBranch(files=['foo', 'foo~']) >>> sb.common_ancestor(sb) == (None, None) True >>> commit.commit(sb, "Committing first revision", verbose=False) >>> sb.common_ancestor(sb)[0] 1 >>> clone = sb.clone() >>> commit.commit(sb, "Committing second revision", verbose=False) >>> sb.common_ancestor(sb)[0] 2 >>> sb.common_ancestor(clone)[0] 1 >>> commit.commit(clone, "Committing divergent second revision", ... verbose=False) >>> sb.common_ancestor(clone)[0] 1 >>> sb.common_ancestor(clone) == clone.common_ancestor(sb) True >>> sb.common_ancestor(sb) != clone.common_ancestor(clone) True >>> clone2 = sb.clone() >>> sb.common_ancestor(clone2)[0] 2 >>> sb.common_ancestor(clone2, self_revno=1)[0] 1 >>> sb.common_ancestor(clone2, other_revno=1)[0] 1 """ my_history = self.revision_history() other_history = other.revision_history() if self_revno is None: self_revno = len(my_history) if other_revno is None: other_revno = len(other_history) indices = range(min((self_revno, other_revno))) indices.reverse() for r in indices: if my_history[r] == other_history[r]: return r+1, my_history[r] return None, None def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def missing_revisions(self, other, stop_revision=None): """ If self and other have not diverged, return a list of the revisions present in other, but missing from self. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch() >>> br2 = ScratchBranch() >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [u'REVISION-ID-1'] >>> br2.missing_revisions(br1) [] >>> commit(br1, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-2A") >>> br1.missing_revisions(br2) [u'REVISION-ID-2A'] >>> commit(br1, "lala!", rev_id="REVISION-ID-2B") >>> br1.missing_revisions(br2) Traceback (most recent call last): DivergedBranches: These branches have diverged. """ self_history = self.revision_history() self_len = len(self_history) other_history = other.revision_history() other_len = len(other_history) common_index = min(self_len, other_len) -1 if common_index >= 0 and \ self_history[common_index] != other_history[common_index]: raise DivergedBranches(self, other) if stop_revision is None: stop_revision = other_len elif stop_revision > other_len: raise NoSuchRevision(self, stop_revision) return other_history[self_len:stop_revision] def update_revisions(self, other, stop_revision=None): """Pull in all new revisions from other branch. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch(files=['foo', 'bar']) >>> br1.add('foo') >>> br1.add('bar') >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False) >>> br2 = ScratchBranch() >>> br2.update_revisions(br1) Added 2 texts. Added 1 inventories. Added 1 revisions. >>> br2.revision_history() [u'REVISION-ID-1'] >>> br2.update_revisions(br1) Added 0 texts. Added 0 inventories. Added 0 revisions. >>> br1.text_store.total_size() == br2.text_store.total_size() True """ from bzrlib.progress import ProgressBar pb = ProgressBar() pb.update('comparing histories') revision_ids = self.missing_revisions(other, stop_revision) revisions = [] needed_texts = sets.Set() i = 0 for rev_id in revision_ids: i += 1 pb.update('fetching revision', i, len(revision_ids)) rev = other.get_revision(rev_id) revisions.append(rev) inv = other.get_inventory(str(rev.inventory_id)) for key, entry in inv.iter_entries(): if entry.text_id is None: continue if entry.text_id not in self.text_store: needed_texts.add(entry.text_id) pb.clear() count = self.text_store.copy_multi(other.text_store, needed_texts) print "Added %d texts." % count inventory_ids = [ f.inventory_id for f in revisions ] count = self.inventory_store.copy_multi(other.inventory_store, inventory_ids) print "Added %d inventories." % count revision_ids = [ f.revision_id for f in revisions] count = self.revision_store.copy_multi(other.revision_store, revision_ids) for revision_id in revision_ids: self.append_revision(revision_id) print "Added %d revisions." % count def commit(self, *args, **kw): from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self.lock_write() try: tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): raise BzrError("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): raise BzrError("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: raise BzrError("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): raise BzrError("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': raise BzrError("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self.lock_write() try: ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): raise BzrError("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): raise BzrError("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': raise BzrError("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): raise BzrError("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): raise BzrError("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: raise BzrError("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): raise BzrError("destination %r already exists" % dest_path) if f_id in to_idpath: raise BzrError("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[], base=None): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ init = False if base is None: base = tempfile.mkdtemp() init = True Branch.__init__(self, base, init=init) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def clone(self): """ >>> orig = ScratchBranch(files=["file1", "file2"]) >>> clone = orig.clone() >>> os.path.samefile(orig.base, clone.base) False >>> os.path.isfile(os.path.join(clone.base, "file1")) True """ base = tempfile.mkdtemp() os.rmdir(base) shutil.copytree(self.base, base, symlinks=True) return ScratchBranch(base=base) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: if self.base: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) commit refs/heads/master mark :773 committer Martin Pool 1119604723 +1000 data 24 - remove done TODO items from :772 M 644 inline TODO data 12955 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * -r option should take a revision-id as well as a revno. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * ``bzr log DIR`` should give changes to any files within DIR. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. Perhaps ~/.bzr/http-cache. Baz has a fairly simple cache under ~/.arch-cache, containing revision information encoded almost as a bunch of archives. Perhaps we could simply store full paths. * Maybe also store directories in the statcache so that we can quickly identify that they still exist. * Diff should show timestamps; for files from the working directory we can use the file itself; for files from a revision we should use the commit time of the revision. * Perhaps split command infrastructure from the actual command definitions. * Cleaner support for negative boolean options like --no-recurse. * Statcache should possibly map all file paths to / separators * quotefn doubles all backslashes on Windows; this is probably not the best thing to do. What would be a better way to safely represent filenames? Perhaps we could doublequote things containing spaces, on the principle that filenames containing quotes are unlikely? Nice for humans; less good for machine parsing. * Patches should probably use only forward slashes, even on Windows, otherwise Unix patch can't apply them. (?) * Branch.update_revisions() inefficiently fetches revisions from the remote server twice; once to find out what text and inventory they need and then again to actually get the thing. This is a bit inefficient. One complicating factor here is that we don't really want to have revisions present in the revision-store until all their constituent parts are also stored. The basic problem is that RemoteBranch.get_revision() and similar methods return object, but what we really want is the raw XML, which can be popped into our own store. That needs to be refactored. * ``bzr status FOO`` where foo is ignored should say so. * ``bzr mkdir A...`` should just create and add A. Medium things ------------- * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. We should be able to just get the id for the selected files, look up their location and diff just those files. No need to traverse the entire inventories. * ``bzr status DIR`` or ``bzr diff DIR`` should report on all changes under that directory. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from ElementTree to an object when it is read in, but rather wait until the program actually wants to know about that node. * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. - Selected-file commit - Impossible selected-file commit: adding things in non-versioned directories, crossing renames, etc. * Write a reproducible benchmark, perhaps importing various kernel versions. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Paranoid mode where we never trust SHA-1 matches. * Don't commit if there are no changes unless forced. * --dry-run mode for commit? (Or maybe just run with check-command=false?) * Generally, be a bit more verbose unless --silent is specified. * Function that finds all changes to files under a given directory; perhaps log should use this if a directory is given. * XML attributes might have trouble with filenames containing \n and \r. Do we really want to support this? I think perhaps not. * Remember execute bits, so that exports will work OK. * Unify smart_add and plain Branch.add(); perhaps smart_add should just build a list of files to add and pass that to the regular add function. * Function to list a directory, saying in which revision each file was last modified. Useful for web and gui interfaces, and slow to compute one file at a time. * unittest is standard, but the results are kind of ugly; would be nice to make it cleaner. * Check locking is correct during merge-related operations. * Perhaps attempts to get locks should timeout after some period of time, or at least display a progress message. * Split out upgrade functionality from check command into a separate ``bzr upgrade``. * Don't pass around command classes but rather pass objects. This'd make it cleaner to construct objects wrapping external commands. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. Possibly this should be done by splitting the commit function into several parts (under a single interface). It is already rather large. Decomposition: - find tree modifications and prepare in-memory inventory - export that inventory to a temporary directory - run the test in that temporary directory - if that succeeded, continue to actually finish the commit What should be done with the text of modified files while this is underway? I don't think we want to count on holding them in memory and we can't trust the working files to stay in one place so I suppose we need to move them into the text store, or otherwise into a temporary directory. If the commit does not actually complete, we would rather the content was not left behind in the stores. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :774 committer Martin Pool 1119604971 +1000 data 39 - show test errors as soon as they fail from :773 M 644 inline bzrlib/selftest/__init__.py data 8308 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") raise class CommandFailed(Exception): pass class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr, so that we can run it # through a specified Python intepreter OVERRIDE_PYTHON = None # to run with alternative python 'python' BZRPATH = 'bzr' _log_buf = "" def formcmd(self, cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = self.BZRPATH if self.OVERRIDE_PYTHON: cmd.insert(0, self.OVERRIDE_PYTHON) self.log('$ %r' % cmd) return cmd def runcmd(self, cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = self.formcmd(cmd) self.log('$ ' + ' '.join(cmd)) actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(self, cmd, retcode=0): """Run a command and return its output""" cmd = self.formcmd(cmd) child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG) outd, errd = child.communicate() self.log(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def build_tree(self, shape): """Build a test tree according to a pattern. shape is a sequence of file specifications. If the final character is '/', a directory is created. This doesn't add anything to a branch. """ # XXX: It's OK to just create them using forward slashes on windows? import os for name in shape: assert isinstance(name, basestring) if name[-1] == '/': os.mkdir(name[:-1]) else: f = file(name, 'wt') print >>f, "contents of", name f.close() def log(self, msg): """Log a message to a progress file""" self._log_buf = self._log_buf + str(msg) + '\n' print >>self.TEST_LOG, msg def check_inventory_shape(self, inv, shape): """ Compare an inventory to a list of expected names. Fail if they are not precisely equal. """ extras = [] shape = list(shape) # copy for path, ie in inv.entries(): name = path.replace('\\', '/') if ie.kind == 'dir': name = name + '/' if name in shape: shape.remove(name) else: extras.append(name) if shape: self.fail("expcted paths not found in inventory: %r" % shape) if extras: self.fail("unexpected paths found in inventory: %r" % extras) class InTempDir(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.test_dir = os.path.join(self.TEST_ROOT, self.__class__.__name__) os.mkdir(self.test_dir) os.chdir(self.test_dir) def tearDown(self): import os os.chdir(self.TEST_ROOT) class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ def __init__(self, out): self.out = out TestResult.__init__(self) def startTest(self, test): # TODO: Maybe show test.shortDescription somewhere? print >>self.out, '%-60.60s' % test.id(), TestResult.startTest(self, test) def stopTest(self, test): # print TestResult.stopTest(self, test) def addError(self, test, err): print >>self.out, 'ERROR' TestResult.addError(self, test, err) _show_test_failure('error', test, err, self.out) def addFailure(self, test, err): print >>self.out, 'FAILURE' TestResult.addFailure(self, test, err) _show_test_failure('failure', test, err, self.out) def addSuccess(self, test): print >>self.out, 'OK' TestResult.addSuccess(self, test) def selftest(): from unittest import TestLoader, TestSuite import bzrlib import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning from doctest import DocTestSuite import os import shutil import time import sys _setup_test_log() _setup_test_dir() print suite = TestSuite() tl = TestLoader() for m in bzrlib.selftest.whitebox, \ bzrlib.selftest.versioning: suite.addTest(tl.loadTestsFromModule(m)) suite.addTest(bzrlib.selftest.blackbox.suite()) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands: suite.addTest(DocTestSuite(m)) # save stdout & stderr so there's no leakage from code-under-test real_stdout = sys.stdout real_stderr = sys.stderr sys.stdout = sys.stderr = TestBase.TEST_LOG try: result = _MyResult(real_stdout) suite.run(result) finally: sys.stdout = real_stdout sys.stderr = real_stderr _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os log_filename = os.path.abspath('testbzr.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "bzr tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_ROOT = os.path.abspath("testbzr.tmp") print '%-30s %s' % ('running tests in', TestBase.TEST_ROOT) if os.path.exists(TestBase.TEST_ROOT): shutil.rmtree(TestBase.TEST_ROOT) os.mkdir(TestBase.TEST_ROOT) os.chdir(TestBase.TEST_ROOT) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_ROOT, '.bzr')) def _show_results(result): print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, tb, out): print >>out, (kind + '! ').ljust(60, '-') print >>out, case desc = case.shortDescription() if desc: print >>out, ' (%s)' % desc print >>out, tb if isinstance(case, TestBase): print >>out print >>out, 'log from this test:' print >>out, case._log_buf print >>out, ''.ljust(60, '-') commit refs/heads/master mark :775 committer Martin Pool 1119605905 +1000 data 41 - New helper TestBase.check_file_contents from :774 M 644 inline bzrlib/selftest/__init__.py data 8663 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") raise class CommandFailed(Exception): pass class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr, so that we can run it # through a specified Python intepreter OVERRIDE_PYTHON = None # to run with alternative python 'python' BZRPATH = 'bzr' _log_buf = "" def formcmd(self, cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = self.BZRPATH if self.OVERRIDE_PYTHON: cmd.insert(0, self.OVERRIDE_PYTHON) self.log('$ %r' % cmd) return cmd def runcmd(self, cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = self.formcmd(cmd) self.log('$ ' + ' '.join(cmd)) actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(self, cmd, retcode=0): """Run a command and return its output""" cmd = self.formcmd(cmd) child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG) outd, errd = child.communicate() self.log(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def build_tree(self, shape): """Build a test tree according to a pattern. shape is a sequence of file specifications. If the final character is '/', a directory is created. This doesn't add anything to a branch. """ # XXX: It's OK to just create them using forward slashes on windows? import os for name in shape: assert isinstance(name, basestring) if name[-1] == '/': os.mkdir(name[:-1]) else: f = file(name, 'wt') print >>f, "contents of", name f.close() def log(self, msg): """Log a message to a progress file""" self._log_buf = self._log_buf + str(msg) + '\n' print >>self.TEST_LOG, msg def check_inventory_shape(self, inv, shape): """ Compare an inventory to a list of expected names. Fail if they are not precisely equal. """ extras = [] shape = list(shape) # copy for path, ie in inv.entries(): name = path.replace('\\', '/') if ie.kind == 'dir': name = name + '/' if name in shape: shape.remove(name) else: extras.append(name) if shape: self.fail("expected paths not found in inventory: %r" % shape) if extras: self.fail("unexpected paths found in inventory: %r" % extras) def check_file_contents(self, filename, expect): self.log("check contents of file %s" % filename) contents = file(filename, 'r').read() if contents != expect: self.log("expected: %r" % expected) self.log("actually: %r" % contents) self.fail("contents of %s not as expected") class InTempDir(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.test_dir = os.path.join(self.TEST_ROOT, self.__class__.__name__) os.mkdir(self.test_dir) os.chdir(self.test_dir) def tearDown(self): import os os.chdir(self.TEST_ROOT) class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ def __init__(self, out): self.out = out TestResult.__init__(self) def startTest(self, test): # TODO: Maybe show test.shortDescription somewhere? print >>self.out, '%-60.60s' % test.id(), TestResult.startTest(self, test) def stopTest(self, test): # print TestResult.stopTest(self, test) def addError(self, test, err): print >>self.out, 'ERROR' TestResult.addError(self, test, err) _show_test_failure('error', test, err, self.out) def addFailure(self, test, err): print >>self.out, 'FAILURE' TestResult.addFailure(self, test, err) _show_test_failure('failure', test, err, self.out) def addSuccess(self, test): print >>self.out, 'OK' TestResult.addSuccess(self, test) def selftest(): from unittest import TestLoader, TestSuite import bzrlib import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning from doctest import DocTestSuite import os import shutil import time import sys _setup_test_log() _setup_test_dir() print suite = TestSuite() tl = TestLoader() for m in bzrlib.selftest.whitebox, \ bzrlib.selftest.versioning: suite.addTest(tl.loadTestsFromModule(m)) suite.addTest(bzrlib.selftest.blackbox.suite()) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands: suite.addTest(DocTestSuite(m)) # save stdout & stderr so there's no leakage from code-under-test real_stdout = sys.stdout real_stderr = sys.stderr sys.stdout = sys.stderr = TestBase.TEST_LOG try: result = _MyResult(real_stdout) suite.run(result) finally: sys.stdout = real_stdout sys.stderr = real_stderr _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os log_filename = os.path.abspath('testbzr.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "bzr tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_ROOT = os.path.abspath("testbzr.tmp") print '%-30s %s' % ('running tests in', TestBase.TEST_ROOT) if os.path.exists(TestBase.TEST_ROOT): shutil.rmtree(TestBase.TEST_ROOT) os.mkdir(TestBase.TEST_ROOT) os.chdir(TestBase.TEST_ROOT) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_ROOT, '.bzr')) def _show_results(result): print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, tb, out): print >>out, (kind + '! ').ljust(60, '-') print >>out, case desc = case.shortDescription() if desc: print >>out, ' (%s)' % desc print >>out, tb if isinstance(case, TestBase): print >>out print >>out, 'log from this test:' print >>out, case._log_buf print >>out, ''.ljust(60, '-') commit refs/heads/master mark :776 committer Martin Pool 1119605961 +1000 data 30 - Better test progress display from :775 M 644 inline bzrlib/selftest/__init__.py data 8688 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") raise class CommandFailed(Exception): pass class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr, so that we can run it # through a specified Python intepreter OVERRIDE_PYTHON = None # to run with alternative python 'python' BZRPATH = 'bzr' _log_buf = "" def formcmd(self, cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = self.BZRPATH if self.OVERRIDE_PYTHON: cmd.insert(0, self.OVERRIDE_PYTHON) self.log('$ %r' % cmd) return cmd def runcmd(self, cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = self.formcmd(cmd) self.log('$ ' + ' '.join(cmd)) actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(self, cmd, retcode=0): """Run a command and return its output""" cmd = self.formcmd(cmd) child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG) outd, errd = child.communicate() self.log(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def build_tree(self, shape): """Build a test tree according to a pattern. shape is a sequence of file specifications. If the final character is '/', a directory is created. This doesn't add anything to a branch. """ # XXX: It's OK to just create them using forward slashes on windows? import os for name in shape: assert isinstance(name, basestring) if name[-1] == '/': os.mkdir(name[:-1]) else: f = file(name, 'wt') print >>f, "contents of", name f.close() def log(self, msg): """Log a message to a progress file""" self._log_buf = self._log_buf + str(msg) + '\n' print >>self.TEST_LOG, msg def check_inventory_shape(self, inv, shape): """ Compare an inventory to a list of expected names. Fail if they are not precisely equal. """ extras = [] shape = list(shape) # copy for path, ie in inv.entries(): name = path.replace('\\', '/') if ie.kind == 'dir': name = name + '/' if name in shape: shape.remove(name) else: extras.append(name) if shape: self.fail("expected paths not found in inventory: %r" % shape) if extras: self.fail("unexpected paths found in inventory: %r" % extras) def check_file_contents(self, filename, expect): self.log("check contents of file %s" % filename) contents = file(filename, 'r').read() if contents != expect: self.log("expected: %r" % expected) self.log("actually: %r" % contents) self.fail("contents of %s not as expected") class InTempDir(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.test_dir = os.path.join(self.TEST_ROOT, self.__class__.__name__) os.mkdir(self.test_dir) os.chdir(self.test_dir) def tearDown(self): import os os.chdir(self.TEST_ROOT) class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ def __init__(self, out): self.out = out TestResult.__init__(self) def startTest(self, test): # TODO: Maybe show test.shortDescription somewhere? print >>self.out, '%-60.60s' % test.id(), self.out.flush() TestResult.startTest(self, test) def stopTest(self, test): # print TestResult.stopTest(self, test) def addError(self, test, err): print >>self.out, 'ERROR' TestResult.addError(self, test, err) _show_test_failure('error', test, err, self.out) def addFailure(self, test, err): print >>self.out, 'FAILURE' TestResult.addFailure(self, test, err) _show_test_failure('failure', test, err, self.out) def addSuccess(self, test): print >>self.out, 'OK' TestResult.addSuccess(self, test) def selftest(): from unittest import TestLoader, TestSuite import bzrlib import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning from doctest import DocTestSuite import os import shutil import time import sys _setup_test_log() _setup_test_dir() print suite = TestSuite() tl = TestLoader() for m in bzrlib.selftest.whitebox, \ bzrlib.selftest.versioning: suite.addTest(tl.loadTestsFromModule(m)) suite.addTest(bzrlib.selftest.blackbox.suite()) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands: suite.addTest(DocTestSuite(m)) # save stdout & stderr so there's no leakage from code-under-test real_stdout = sys.stdout real_stderr = sys.stderr sys.stdout = sys.stderr = TestBase.TEST_LOG try: result = _MyResult(real_stdout) suite.run(result) finally: sys.stdout = real_stdout sys.stderr = real_stderr _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os log_filename = os.path.abspath('testbzr.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "bzr tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_ROOT = os.path.abspath("testbzr.tmp") print '%-30s %s' % ('running tests in', TestBase.TEST_ROOT) if os.path.exists(TestBase.TEST_ROOT): shutil.rmtree(TestBase.TEST_ROOT) os.mkdir(TestBase.TEST_ROOT) os.chdir(TestBase.TEST_ROOT) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_ROOT, '.bzr')) def _show_results(result): print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, tb, out): print >>out, (kind + '! ').ljust(60, '-') print >>out, case desc = case.shortDescription() if desc: print >>out, ' (%s)' % desc print >>out, tb if isinstance(case, TestBase): print >>out print >>out, 'log from this test:' print >>out, case._log_buf print >>out, ''.ljust(60, '-') commit refs/heads/master mark :777 committer Martin Pool 1119606284 +1000 data 43 - better display of test failure tracebacks from :776 M 644 inline bzrlib/selftest/__init__.py data 8774 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") raise class CommandFailed(Exception): pass class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr, so that we can run it # through a specified Python intepreter OVERRIDE_PYTHON = None # to run with alternative python 'python' BZRPATH = 'bzr' _log_buf = "" def formcmd(self, cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = self.BZRPATH if self.OVERRIDE_PYTHON: cmd.insert(0, self.OVERRIDE_PYTHON) self.log('$ %r' % cmd) return cmd def runcmd(self, cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = self.formcmd(cmd) self.log('$ ' + ' '.join(cmd)) actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(self, cmd, retcode=0): """Run a command and return its output""" cmd = self.formcmd(cmd) child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG) outd, errd = child.communicate() self.log(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def build_tree(self, shape): """Build a test tree according to a pattern. shape is a sequence of file specifications. If the final character is '/', a directory is created. This doesn't add anything to a branch. """ # XXX: It's OK to just create them using forward slashes on windows? import os for name in shape: assert isinstance(name, basestring) if name[-1] == '/': os.mkdir(name[:-1]) else: f = file(name, 'wt') print >>f, "contents of", name f.close() def log(self, msg): """Log a message to a progress file""" self._log_buf = self._log_buf + str(msg) + '\n' print >>self.TEST_LOG, msg def check_inventory_shape(self, inv, shape): """ Compare an inventory to a list of expected names. Fail if they are not precisely equal. """ extras = [] shape = list(shape) # copy for path, ie in inv.entries(): name = path.replace('\\', '/') if ie.kind == 'dir': name = name + '/' if name in shape: shape.remove(name) else: extras.append(name) if shape: self.fail("expected paths not found in inventory: %r" % shape) if extras: self.fail("unexpected paths found in inventory: %r" % extras) def check_file_contents(self, filename, expect): self.log("check contents of file %s" % filename) contents = file(filename, 'r').read() if contents != expect: self.log("expected: %r" % expected) self.log("actually: %r" % contents) self.fail("contents of %s not as expected") class InTempDir(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.test_dir = os.path.join(self.TEST_ROOT, self.__class__.__name__) os.mkdir(self.test_dir) os.chdir(self.test_dir) def tearDown(self): import os os.chdir(self.TEST_ROOT) class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ def __init__(self, out): self.out = out TestResult.__init__(self) def startTest(self, test): # TODO: Maybe show test.shortDescription somewhere? print >>self.out, '%-60.60s' % test.id(), self.out.flush() TestResult.startTest(self, test) def stopTest(self, test): # print TestResult.stopTest(self, test) def addError(self, test, err): print >>self.out, 'ERROR' TestResult.addError(self, test, err) _show_test_failure('error', test, err, self.out) def addFailure(self, test, err): print >>self.out, 'FAILURE' TestResult.addFailure(self, test, err) _show_test_failure('failure', test, err, self.out) def addSuccess(self, test): print >>self.out, 'OK' TestResult.addSuccess(self, test) def selftest(): from unittest import TestLoader, TestSuite import bzrlib import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning from doctest import DocTestSuite import os import shutil import time import sys _setup_test_log() _setup_test_dir() print suite = TestSuite() tl = TestLoader() for m in bzrlib.selftest.whitebox, \ bzrlib.selftest.versioning: suite.addTest(tl.loadTestsFromModule(m)) suite.addTest(bzrlib.selftest.blackbox.suite()) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands: suite.addTest(DocTestSuite(m)) # save stdout & stderr so there's no leakage from code-under-test real_stdout = sys.stdout real_stderr = sys.stderr sys.stdout = sys.stderr = TestBase.TEST_LOG try: result = _MyResult(real_stdout) suite.run(result) finally: sys.stdout = real_stdout sys.stderr = real_stderr _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os log_filename = os.path.abspath('testbzr.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "bzr tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_ROOT = os.path.abspath("testbzr.tmp") print '%-30s %s' % ('running tests in', TestBase.TEST_ROOT) if os.path.exists(TestBase.TEST_ROOT): shutil.rmtree(TestBase.TEST_ROOT) os.mkdir(TestBase.TEST_ROOT) os.chdir(TestBase.TEST_ROOT) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_ROOT, '.bzr')) def _show_results(result): print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, exc_info, out): from traceback import print_exception print >>out, '-' * 60 print >>out, case desc = case.shortDescription() if desc: print >>out, ' (%s)' % desc print_exception(exc_info[0], exc_info[1], exc_info[2], None, out) if isinstance(case, TestBase): print >>out print >>out, 'log from this test:' print >>out, case._log_buf print >>out, '-' * 60 commit refs/heads/master mark :778 committer Martin Pool 1119609767 +1000 data 54 - simple revert of text files - test cases for this from :777 M 644 inline bzrlib/branch.py data 37075 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_file, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def _relpath(base, path): """Return path relative to base, or raise exception. The path may be either an absolute path or a path relative to the current working directory. Lifted out of Branch.relpath for ease of testing. os.path.commonprefix (python2.4) has a bad bug that it works just on string prefixes, assuming that '/u' is a prefix of '/u2'. This avoids that problem.""" rp = os.path.abspath(path) s = [] head = rp while len(head) >= len(base): if head == base: break head, tail = os.path.split(head) if tail: s.insert(0, tail) else: from errors import NotBranchError raise NotBranchError("path %r is not within branch %r" % (rp, base)) return os.sep.join(s) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head class DivergedBranches(Exception): def __init__(self, branch1, branch2): self.branch1 = branch1 self.branch2 = branch2 Exception.__init__(self, "These branches have diverged.") class NoSuchRevision(BzrError): def __init__(self, branch, revision): self.branch = branch self.revision = revision msg = "Branch %s has no revision %d" % (branch, revision) BzrError.__init__(self, msg) ###################################################################### # branch objects class Branch(object): """Branch holding a history of revisions. base Base directory of the branch. _lock_mode None, or 'r' or 'w' _lock_count If _lock_mode is true, a positive count of the number of times the lock has been taken. _lock Lock object from bzrlib.lock. """ base = None _lock_mode = None _lock_count = None _lock = None def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): from errors import NotBranchError raise NotBranchError("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def __del__(self): if self._lock_mode or self._lock: from warnings import warn warn("branch %r was not explicitly unlocked" % self) self._lock.unlock() def lock_write(self): if self._lock_mode: if self._lock_mode != 'w': from errors import LockError raise LockError("can't upgrade to a write lock from %r" % self._lock_mode) self._lock_count += 1 else: from bzrlib.lock import WriteLock self._lock = WriteLock(self.controlfilename('branch-lock')) self._lock_mode = 'w' self._lock_count = 1 def lock_read(self): if self._lock_mode: assert self._lock_mode in ('r', 'w'), \ "invalid lock mode %r" % self._lock_mode self._lock_count += 1 else: from bzrlib.lock import ReadLock self._lock = ReadLock(self.controlfilename('branch-lock')) self._lock_mode = 'r' self._lock_count = 1 def unlock(self): if not self._lock_mode: from errors import LockError raise LockError('branch %r is not locked' % (self)) if self._lock_count > 1: self._lock_count -= 1 else: self._lock.unlock() self._lock = None self._lock_mode = self._lock_count = None def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" return _relpath(self.base, path) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.\n") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: raise BzrError('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. self.lock_read() try: inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv finally: self.unlock() def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self.lock_write() try: from bzrlib.atomicfile import AtomicFile f = AtomicFile(self.controlfilename('inventory'), 'wb') try: inv.write_xml(f) f.commit() finally: f.close() finally: self.unlock() mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. files List of paths to add, relative to the base of the tree. ids If set, use these instead of automatically generated ids. Must be the same length as the list of files, but may contain None for ids that are to be autogenerated. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) self.lock_write() try: inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): raise BzrError("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: raise BzrError("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: print 'added', quotefn(f) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) finally: self.unlock() def print_file(self, file, revno): """Print `file` to stdout.""" self.lock_read() try: tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: raise BzrError("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) finally: self.unlock() def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] self.lock_write() try: tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: raise BzrError("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) finally: self.unlock() # FIXME: this doesn't need to be a branch method def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): from bzrlib.atomicfile import AtomicFile mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() + [revision_id] f = AtomicFile(self.controlfilename('revision-history')) try: for rev_id in rev_history: print >>f, rev_id f.commit() finally: f.close() def get_revision(self, revision_id): """Return the Revision object for a named revision""" if not revision_id or not isinstance(revision_id, basestring): raise ValueError('invalid revision-id: %r' % revision_id) r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_revision_sha1(self, revision_id): """Hash the stored value of a revision, and return it.""" # In the future, revision entries will be signed. At that # point, it is probably best *not* to include the signature # in the revision hash. Because that lets you re-sign # the revision, (add signatures/remove signatures) and still # have all hash pointers stay consistent. # But for now, just hash the contents. return sha_file(self.revision_store[revision_id]) def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_inventory_sha1(self, inventory_id): """Return the sha1 hash of the inventory entry """ return sha_file(self.inventory_store[inventory_id]) def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self.lock_read() try: return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] finally: self.unlock() def common_ancestor(self, other, self_revno=None, other_revno=None): """ >>> import commit >>> sb = ScratchBranch(files=['foo', 'foo~']) >>> sb.common_ancestor(sb) == (None, None) True >>> commit.commit(sb, "Committing first revision", verbose=False) >>> sb.common_ancestor(sb)[0] 1 >>> clone = sb.clone() >>> commit.commit(sb, "Committing second revision", verbose=False) >>> sb.common_ancestor(sb)[0] 2 >>> sb.common_ancestor(clone)[0] 1 >>> commit.commit(clone, "Committing divergent second revision", ... verbose=False) >>> sb.common_ancestor(clone)[0] 1 >>> sb.common_ancestor(clone) == clone.common_ancestor(sb) True >>> sb.common_ancestor(sb) != clone.common_ancestor(clone) True >>> clone2 = sb.clone() >>> sb.common_ancestor(clone2)[0] 2 >>> sb.common_ancestor(clone2, self_revno=1)[0] 1 >>> sb.common_ancestor(clone2, other_revno=1)[0] 1 """ my_history = self.revision_history() other_history = other.revision_history() if self_revno is None: self_revno = len(my_history) if other_revno is None: other_revno = len(other_history) indices = range(min((self_revno, other_revno))) indices.reverse() for r in indices: if my_history[r] == other_history[r]: return r+1, my_history[r] return None, None def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def missing_revisions(self, other, stop_revision=None): """ If self and other have not diverged, return a list of the revisions present in other, but missing from self. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch() >>> br2 = ScratchBranch() >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [u'REVISION-ID-1'] >>> br2.missing_revisions(br1) [] >>> commit(br1, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-2A") >>> br1.missing_revisions(br2) [u'REVISION-ID-2A'] >>> commit(br1, "lala!", rev_id="REVISION-ID-2B") >>> br1.missing_revisions(br2) Traceback (most recent call last): DivergedBranches: These branches have diverged. """ self_history = self.revision_history() self_len = len(self_history) other_history = other.revision_history() other_len = len(other_history) common_index = min(self_len, other_len) -1 if common_index >= 0 and \ self_history[common_index] != other_history[common_index]: raise DivergedBranches(self, other) if stop_revision is None: stop_revision = other_len elif stop_revision > other_len: raise NoSuchRevision(self, stop_revision) return other_history[self_len:stop_revision] def update_revisions(self, other, stop_revision=None): """Pull in all new revisions from other branch. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch(files=['foo', 'bar']) >>> br1.add('foo') >>> br1.add('bar') >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False) >>> br2 = ScratchBranch() >>> br2.update_revisions(br1) Added 2 texts. Added 1 inventories. Added 1 revisions. >>> br2.revision_history() [u'REVISION-ID-1'] >>> br2.update_revisions(br1) Added 0 texts. Added 0 inventories. Added 0 revisions. >>> br1.text_store.total_size() == br2.text_store.total_size() True """ from bzrlib.progress import ProgressBar pb = ProgressBar() pb.update('comparing histories') revision_ids = self.missing_revisions(other, stop_revision) revisions = [] needed_texts = sets.Set() i = 0 for rev_id in revision_ids: i += 1 pb.update('fetching revision', i, len(revision_ids)) rev = other.get_revision(rev_id) revisions.append(rev) inv = other.get_inventory(str(rev.inventory_id)) for key, entry in inv.iter_entries(): if entry.text_id is None: continue if entry.text_id not in self.text_store: needed_texts.add(entry.text_id) pb.clear() count = self.text_store.copy_multi(other.text_store, needed_texts) print "Added %d texts." % count inventory_ids = [ f.inventory_id for f in revisions ] count = self.inventory_store.copy_multi(other.inventory_store, inventory_ids) print "Added %d inventories." % count revision_ids = [ f.revision_id for f in revisions] count = self.revision_store.copy_multi(other.revision_store, revision_ids) for revision_id in revision_ids: self.append_revision(revision_id) print "Added %d revisions." % count def commit(self, *args, **kw): from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self.lock_write() try: tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): raise BzrError("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): raise BzrError("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: raise BzrError("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): raise BzrError("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': raise BzrError("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self.lock_write() try: ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): raise BzrError("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): raise BzrError("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': raise BzrError("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): raise BzrError("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): raise BzrError("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: raise BzrError("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): raise BzrError("destination %r already exists" % dest_path) if f_id in to_idpath: raise BzrError("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() def revert(self, filenames, old_tree=None): """Restore selected files to the versions from a previous tree. """ from bzrlib.errors import NotVersionedError, BzrError from bzrlib.atomicfile import AtomicFile inv = self.read_working_inventory() if old_tree is None: old_tree = self.basis_tree() old_inv = old_tree.inventory nids = [] for fn in filenames: file_id = inv.path2id(fn) if not file_id: raise NotVersionedError("not a versioned file", fn) nids.append((fn, file_id)) # TODO: Rename back if it was previously at a different location # TODO: If given a directory, restore the entire contents from # the previous version. # TODO: Make a backup to a temporary file. # TODO: If the file previously didn't exist, delete it? for fn, file_id in nids: if not old_inv.has_id(file_id): raise BzrError("file not present in old tree", fn, file_id) f = AtomicFile(fn, 'wb') try: f.write(old_tree.get_file(file_id).read()) f.commit() finally: f.close() class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[], base=None): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ init = False if base is None: base = tempfile.mkdtemp() init = True Branch.__init__(self, base, init=init) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def clone(self): """ >>> orig = ScratchBranch(files=["file1", "file2"]) >>> clone = orig.clone() >>> os.path.samefile(orig.base, clone.base) False >>> os.path.isfile(os.path.join(clone.base, "file1")) True """ base = tempfile.mkdtemp() os.rmdir(base) shutil.copytree(self.base, base, symlinks=True) return ScratchBranch(base=base) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: if self.base: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/commands.py data 52222 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date plugin_cmds = {} def register_command(cmd): "Utility function to help register a command" global plugin_cmds k = cmd.__name__ if k.startswith("cmd_"): k_unsquished = _unsquish_command_name(k) else: k_unsquished = k if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = cmd else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r' % sys.modules[cmd.__module__]) def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _get_cmd_dict(plugins_override=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v # If we didn't load plugins, the plugin_cmds dict will be empty if plugins_override: d.update(plugin_cmds) else: d2 = plugin_cmds.copy() d2.update(d) d = d2 return d def get_all_cmds(plugins_override=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(plugins_override=plugins_override).iteritems(): yield k,v def get_cmd_class(cmd, plugins_override=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(plugins_override=plugins_override) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() for opt in self.takes_options: if not opt in OPTIONS: raise BzrError("Unknown option '%s' returned by external command %s" % (opt, path)) # TODO: Is there any way to check takes_args is valid here? self.takes_args = pipe.readline().split() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-usage'" % path) pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-help'" % path) def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: optname = name.replace('_','-') value = kargs[name] if OPTIONS.has_key(optname): # it's an option opts.append('--%s' % optname) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_mkdir(Command): """Create a new versioned directory. This is equivalent to creating the directory and then adding it. """ takes_args = ['dir+'] def run(self, dir_list): import os import bzrlib.branch b = None for d in dir_list: os.mkdir(d) if not b: b = bzrlib.branch.Branch(d) b.add([d], verbose=True) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import errno br_to = Branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if errno == errno.ENOENT: raise if location is None: if stored_loc is None: raise BzrCommandError("No pull location known or specified.") else: print "Using last location: %s" % stored_loc location = stored_loc from branch import find_branch, DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged. Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. To retrieve the branch as of a particular revision, supply the --revision parameter, as in "branch foo/bar -r 5". """ takes_args = ['from_location', 'to_location?'] takes_options = ['revision'] def run(self, from_location, to_location=None, revision=None): import errno from bzrlib.merge import merge from branch import find_branch, DivergedBranches, NoSuchRevision from shutil import rmtree try: br_from = find_branch(from_location) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location.rstrip("/\\")) try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) try: br_to.update_revisions(br_from, stop_revision=revision) except NoSuchRevision: rmtree(to_location) msg = "The branch %s has no revision %d." % (from_location, revision) raise BzrCommandError(msg) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False, ignore_zero=True) from_location = pull_loc(br_from) br_to.controlfile("x-pull", "wb").write(from_location + "\n") def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: raise BzrError("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: raise BzrError("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, exports to a directory (equivalent to --format=dir).""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format'] def run(self, dest, revision=None, format='dir'): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest, format) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit from bzrlib.osutils import get_text_message ## Warning: shadows builtin file() if not message and not file: import cStringIO stdout = sys.stdout catcher = cStringIO.StringIO() sys.stdout = catcher cmd_status({"file_list":selected_list}, {}) info = catcher.getvalue() sys.stdout = stdout message = get_text_message(info) if message is None: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_upgrade(Command): """Upgrade branch storage to current format. This should normally be used only after the check command tells you to run it. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.upgrade import upgrade upgrade(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest return int(not selftest()) class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_simple_revert(Command): """Restore selected files from a previous revision. """ takes_args = ['file+'] def run(self, file_list): if not file_list: file_list = ['.'] b = find_branch(file_list[0]) b.revert([b.relpath(f) for f in file_list]) class cmd_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) class cmd_plugins(Command): """List plugins""" hidden = True def run(self): import bzrlib.plugin from pprint import pprint pprint(bzrlib.plugin.all_plugins) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'update': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) >>> parse_args('log -r 500'.split()) (['log'], {'revision': 500}) >>> parse_args('log -r500:600'.split()) (['log'], {'revision': [500, 600]}) >>> parse_args('log -vr500:600'.split()) (['log'], {'verbose': True, 'revision': [500, 600]}) >>> parse_args('log -rv500:600'.split()) #the r takes an argument Traceback (most recent call last): ... ValueError: invalid literal for int(): v500 """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: raise BzrError('unknown long option %r' % a) else: shortopt = a[1:] if shortopt in SHORT_OPTIONS: # Multi-character options must have a space to delimit # their value optname = SHORT_OPTIONS[shortopt] else: # Single character short options, can be chained, # and have their value appended to their name shortopt = a[1:2] if shortopt not in SHORT_OPTIONS: # We didn't find the multi-character name, and we # didn't find the single char name raise BzrError('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if a[2:]: # There are extra things on this option # see if it is the value, or if it is another # short option optargfn = OPTIONS[optname] if optargfn is None: # This option does not take an argument, so the # next entry is another short option, pack it back # into the list argv.insert(0, '-' + a[2:]) else: # This option takes an argument, so pack it # into the array optarg = a[2:] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? raise BzrError('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: raise BzrError('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: raise BzrError('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def _parse_master_args(argv): """Parse the arguments that always go with the original command. These are things like bzr --no-plugins, etc. There are now 2 types of option flags. Ones that come *before* the command, and ones that come *after* the command. Ones coming *before* the command are applied against all possible commands. And are generally applied before plugins are loaded. The current list are: --builtin Allow plugins to load, but don't let them override builtin commands, they will still be allowed if they do not override a builtin. --no-plugins Don't load any plugins. This lets you get back to official source behavior. --profile Enable the hotspot profile before running the command. For backwards compatibility, this is also a non-master option. --version Spit out the version of bzr that is running and exit. This is also a non-master option. --help Run help and exit, also a non-master option (I think that should stay, though) >>> argv, opts = _parse_master_args(['bzr', '--test']) Traceback (most recent call last): ... BzrCommandError: Invalid master option: 'test' >>> argv, opts = _parse_master_args(['bzr', '--version', 'command']) >>> print argv ['command'] >>> print opts['version'] True >>> argv, opts = _parse_master_args(['bzr', '--profile', 'command', '--more-options']) >>> print argv ['command', '--more-options'] >>> print opts['profile'] True >>> argv, opts = _parse_master_args(['bzr', '--no-plugins', 'command']) >>> print argv ['command'] >>> print opts['no-plugins'] True >>> print opts['profile'] False >>> argv, opts = _parse_master_args(['bzr', 'command', '--profile']) >>> print argv ['command', '--profile'] >>> print opts['profile'] False """ master_opts = {'builtin':False, 'no-plugins':False, 'version':False, 'profile':False, 'help':False } # This is the point where we could hook into argv[0] to determine # what front-end is supposed to be run # For now, we are just ignoring it. cmd_name = argv.pop(0) for arg in argv[:]: if arg[:2] != '--': # at the first non-option, we return the rest break arg = arg[2:] # Remove '--' if arg not in master_opts: # We could say that this is not an error, that we should # just let it be handled by the main section instead raise BzrCommandError('Invalid master option: %r' % arg) argv.pop(0) # We are consuming this entry master_opts[arg] = True return argv, master_opts def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: # some options like --builtin and --no-plugins have special effects argv, master_opts = _parse_master_args(argv) if not master_opts['no-plugins']: bzrlib.load_plugins() args, opts = parse_args(argv) if master_opts['help']: from bzrlib.help import help if argv: help(argv[0]) else: help() return 0 if 'help' in opts: from bzrlib.help import help if args: help(args[0]) else: help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 plugins_override = not (master_opts['builtin']) canonical_cmd, cmd_class = get_cmd_class(cmd, plugins_override=plugins_override) profile = master_opts['profile'] # For backwards compatibility, I would rather stick with --profile being a # master/global option if 'profile' in opts: profile = True del opts['profile'] # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/selftest/whitebox.py data 3972 #! /usr/bin/python import os import unittest from bzrlib.selftest import InTempDir, TestBase from bzrlib.branch import ScratchBranch, Branch from bzrlib.errors import NotBranchError, NotVersionedError class Revert(InTempDir): """Test selected-file revert""" def runTest(self): b = Branch('.', init=True) self.build_tree(['hello.txt']) file('hello.txt', 'w').write('initial hello') self.assertRaises(NotVersionedError, b.revert, ['hello.txt']) b.add(['hello.txt']) b.commit('create initial hello.txt') self.check_file_contents('hello.txt', 'initial hello') file('hello.txt', 'w').write('new hello') self.check_file_contents('hello.txt', 'new hello') # revert file modified since last revision b.revert(['hello.txt']) self.check_file_contents('hello.txt', 'initial hello') # reverting again causes no change b.revert(['hello.txt']) self.check_file_contents('hello.txt', 'initial hello') class RenameDirs(InTempDir): """Test renaming directories and the files within them.""" def runTest(self): b = Branch('.', init=True) self.build_tree(['dir/', 'dir/sub/', 'dir/sub/file']) b.add(['dir', 'dir/sub', 'dir/sub/file']) b.commit('create initial state') # TODO: lift out to a test helper that checks the shape of # an inventory revid = b.revision_history()[0] self.log('first revision_id is {%s}' % revid) inv = b.get_revision_inventory(revid) self.log('contents of inventory: %r' % inv.entries()) self.check_inventory_shape(inv, ['dir', 'dir/sub', 'dir/sub/file']) b.rename_one('dir', 'newdir') self.check_inventory_shape(b.inventory, ['newdir', 'newdir/sub', 'newdir/sub/file']) b.rename_one('newdir/sub', 'newdir/newsub') self.check_inventory_shape(b.inventory, ['newdir', 'newdir/newsub', 'newdir/newsub/file']) class BranchPathTestCase(TestBase): """test for branch path lookups Branch.relpath and bzrlib.branch._relpath do a simple but subtle job: given a path (either relative to cwd or absolute), work out if it is inside a branch and return the path relative to the base. """ def runTest(self): from bzrlib.branch import _relpath import tempfile, shutil savedir = os.getcwdu() dtmp = tempfile.mkdtemp() def rp(p): return _relpath(dtmp, p) try: # check paths inside dtmp while standing outside it self.assertEqual(rp(os.path.join(dtmp, 'foo')), 'foo') # root = nothing self.assertEqual(rp(dtmp), '') self.assertRaises(NotBranchError, rp, '/etc') # now some near-miss operations -- note that # os.path.commonprefix gets these wrong! self.assertRaises(NotBranchError, rp, dtmp.rstrip('\\/') + '2') self.assertRaises(NotBranchError, rp, dtmp.rstrip('\\/') + '2/foo') # now operations based on relpath of files in current # directory, or nearby os.chdir(dtmp) self.assertEqual(rp('foo/bar/quux'), 'foo/bar/quux') self.assertEqual(rp('foo'), 'foo') self.assertEqual(rp('./foo'), 'foo') self.assertEqual(rp(os.path.abspath('foo')), 'foo') self.assertRaises(NotBranchError, rp, '../foo') finally: os.chdir(savedir) shutil.rmtree(dtmp) commit refs/heads/master mark :779 committer Martin Pool 1119611095 +1000 data 130 - better quotefn for windows: use doublequotes for strings with strange characters, not backslashes - new backup_file() routine from :778 M 644 inline bzrlib/osutils.py data 13019 # Bazaar-NG -- distributed version control # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, types, re, time, errno, sys from stat import S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE from bzrlib.errors import BzrError from bzrlib.trace import mutter import bzrlib def make_readonly(filename): """Make a filename read-only.""" # TODO: probably needs to be fixed for windows mod = os.stat(filename).st_mode mod = mod & 0777555 os.chmod(filename, mod) def make_writable(filename): mod = os.stat(filename).st_mode mod = mod | 0200 os.chmod(filename, mod) _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])') def quotefn(f): """Return a quoted filename filename This previously used backslash quoting, but that works poorly on Windows.""" # TODO: I'm not really sure this is the best format either.x if _QUOTE_RE.search(f): return '"' + f + '"' else: return f def file_kind(f): mode = os.lstat(f)[ST_MODE] if S_ISREG(mode): return 'file' elif S_ISDIR(mode): return 'directory' elif S_ISLNK(mode): return 'symlink' else: raise BzrError("can't handle file kind with mode %o of %r" % (mode, f)) def kind_marker(kind): if kind == 'file': return '' elif kind == 'directory': return '/' elif kind == 'symlink': return '@' else: raise BzrError('invalid file kind %r' % kind) def backup_file(fn): """Copy a file to a backup. Backups are named in GNU-style, with a ~ suffix. If the file is already a backup, it's not copied. """ import os if fn[-1] == '~': return bfn = fn + '~' inf = file(fn, 'rb') try: content = inf.read() finally: inf.close() outf = file(bfn, 'wb') try: outf.write(content) finally: outf.close() def isdir(f): """True if f is an accessible directory.""" try: return S_ISDIR(os.lstat(f)[ST_MODE]) except OSError: return False def isfile(f): """True if f is a regular file.""" try: return S_ISREG(os.lstat(f)[ST_MODE]) except OSError: return False def is_inside(dir, fname): """True if fname is inside dir. """ return os.path.commonprefix([dir, fname]) == dir def is_inside_any(dir_list, fname): """True if fname is inside any of given dirs.""" # quick scan for perfect match if fname in dir_list: return True for dirname in dir_list: if is_inside(dirname, fname): return True else: return False def pumpfile(fromfile, tofile): """Copy contents of one file to another.""" tofile.write(fromfile.read()) def uuid(): """Return a new UUID""" try: return file('/proc/sys/kernel/random/uuid').readline().rstrip('\n') except IOError: return chomp(os.popen('uuidgen').readline()) def sha_file(f): import sha if hasattr(f, 'tell'): assert f.tell() == 0 s = sha.new() BUFSIZE = 128<<10 while True: b = f.read(BUFSIZE) if not b: break s.update(b) return s.hexdigest() def sha_string(f): import sha s = sha.new() s.update(f) return s.hexdigest() def fingerprint_file(f): import sha s = sha.new() b = f.read() s.update(b) size = len(b) return {'size': size, 'sha1': s.hexdigest()} def config_dir(): """Return per-user configuration directory. By default this is ~/.bzr.conf/ TODO: Global option --config-dir to override this. """ return os.path.expanduser("~/.bzr.conf") def _auto_user_id(): """Calculate automatic user identification. Returns (realname, email). Only used when none is set in the environment or the id file. This previously used the FQDN as the default domain, but that can be very slow on machines where DNS is broken. So now we simply use the hostname. """ import socket # XXX: Any good way to get real user name on win32? try: import pwd uid = os.getuid() w = pwd.getpwuid(uid) gecos = w.pw_gecos.decode(bzrlib.user_encoding) username = w.pw_name.decode(bzrlib.user_encoding) comma = gecos.find(',') if comma == -1: realname = gecos else: realname = gecos[:comma] if not realname: realname = username except ImportError: import getpass realname = username = getpass.getuser().decode(bzrlib.user_encoding) return realname, (username + '@' + socket.gethostname()) def _get_user_id(): """Return the full user id from a file or environment variable. TODO: Allow taking this from a file in the branch directory too for per-branch ids.""" v = os.environ.get('BZREMAIL') if v: return v.decode(bzrlib.user_encoding) try: return (open(os.path.join(config_dir(), "email")) .read() .decode(bzrlib.user_encoding) .rstrip("\r\n")) except IOError, e: if e.errno != errno.ENOENT: raise e v = os.environ.get('EMAIL') if v: return v.decode(bzrlib.user_encoding) else: return None def username(): """Return email-style username. Something similar to 'Martin Pool ' TODO: Check it's reasonably well-formed. """ v = _get_user_id() if v: return v name, email = _auto_user_id() if name: return '%s <%s>' % (name, email) else: return email _EMAIL_RE = re.compile(r'[\w+.-]+@[\w+.-]+') def user_email(): """Return just the email component of a username.""" e = _get_user_id() if e: m = _EMAIL_RE.search(e) if not m: raise BzrError("%r doesn't seem to contain a reasonable email address" % e) return m.group(0) return _auto_user_id()[1] def compare_files(a, b): """Returns true if equal in contents""" BUFSIZE = 4096 while True: ai = a.read(BUFSIZE) bi = b.read(BUFSIZE) if ai != bi: return False if ai == '': return True def local_time_offset(t=None): """Return offset of local zone from GMT, either at present or at time t.""" # python2.3 localtime() can't take None if t == None: t = time.time() if time.localtime(t).tm_isdst and time.daylight: return -time.altzone else: return -time.timezone def format_date(t, offset=0, timezone='original'): ## TODO: Perhaps a global option to use either universal or local time? ## Or perhaps just let people set $TZ? assert isinstance(t, float) if timezone == 'utc': tt = time.gmtime(t) offset = 0 elif timezone == 'original': if offset == None: offset = 0 tt = time.gmtime(t + offset) elif timezone == 'local': tt = time.localtime(t) offset = local_time_offset(t) else: raise BzrError("unsupported timezone format %r", ['options are "utc", "original", "local"']) return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt) + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)) def compact_date(when): return time.strftime('%Y%m%d%H%M%S', time.gmtime(when)) def filesize(f): """Return size of given open file.""" return os.fstat(f.fileno())[ST_SIZE] if hasattr(os, 'urandom'): # python 2.4 and later rand_bytes = os.urandom elif sys.platform == 'linux2': rand_bytes = file('/dev/urandom', 'rb').read else: # not well seeded, but better than nothing def rand_bytes(n): import random s = '' while n: s += chr(random.randint(0, 255)) n -= 1 return s ## TODO: We could later have path objects that remember their list ## decomposition (might be too tricksy though.) def splitpath(p): """Turn string into list of parts. >>> splitpath('a') ['a'] >>> splitpath('a/b') ['a', 'b'] >>> splitpath('a/./b') ['a', 'b'] >>> splitpath('a/.b') ['a', '.b'] >>> splitpath('a/../b') Traceback (most recent call last): ... BzrError: sorry, '..' not allowed in path """ assert isinstance(p, types.StringTypes) # split on either delimiter because people might use either on # Windows ps = re.split(r'[\\/]', p) rps = [] for f in ps: if f == '..': raise BzrError("sorry, %r not allowed in path" % f) elif (f == '.') or (f == ''): pass else: rps.append(f) return rps def joinpath(p): assert isinstance(p, list) for f in p: if (f == '..') or (f == None) or (f == ''): raise BzrError("sorry, %r not allowed in path" % f) return os.path.join(*p) def appendpath(p1, p2): if p1 == '': return p2 else: return os.path.join(p1, p2) def extern_command(cmd, ignore_errors = False): mutter('external command: %s' % `cmd`) if os.system(cmd): if not ignore_errors: raise BzrError('command failed') def _read_config_value(name): """Read a config value from the file ~/.bzr.conf/ Return None if the file does not exist""" try: f = file(os.path.join(config_dir(), name), "r") return f.read().decode(bzrlib.user_encoding).rstrip("\r\n") except IOError, e: if e.errno == errno.ENOENT: return None raise def _get_editor(): """Return a sequence of possible editor binaries for the current platform""" e = _read_config_value("editor") if e is not None: yield e if os.name == "windows": yield "notepad.exe" elif os.name == "posix": try: yield os.environ["EDITOR"] except KeyError: yield "/usr/bin/vi" def _run_editor(filename): """Try to execute an editor to edit the commit message. Returns True on success, False on failure""" for e in _get_editor(): x = os.spawnvp(os.P_WAIT, e, (e, filename)) if x == 0: return True elif x == 127: continue else: break raise BzrError("Could not start any editor. Please specify $EDITOR or use ~/.bzr.conf/editor") return False def get_text_message(infotext, ignoreline = "default"): import tempfile if ignoreline == "default": ignoreline = "-- This line and the following will be ignored --" try: tmp_fileno, msgfilename = tempfile.mkstemp() msgfile = os.close(tmp_fileno) if infotext is not None and infotext != "": hasinfo = True msgfile = file(msgfilename, "w") msgfile.write("\n\n%s\n\n%s" % (ignoreline, infotext)) msgfile.close() else: hasinfo = False if not _run_editor(msgfilename): return None started = False msg = [] lastline, nlines = 0, 0 for line in file(msgfilename, "r"): stripped_line = line.strip() # strip empty line before the log message starts if not started: if stripped_line != "": started = True else: continue # check for the ignore line only if there # is additional information at the end if hasinfo and stripped_line == ignoreline: break nlines += 1 # keep track of the last line that had some content if stripped_line != "": lastline = nlines msg.append(line) if len(msg) == 0: return None # delete empty lines at the end del msg[lastline:] # add a newline at the end, if needed if not msg[-1].endswith("\n"): return "%s%s" % ("".join(msg), "\n") else: return "".join(msg) finally: # delete the msg file in any case try: os.unlink(msgfilename) except IOError: pass commit refs/heads/master mark :780 committer Martin Pool 1119611118 +1000 data 26 - test message improvement from :779 M 644 inline bzrlib/selftest/blackbox.py data 11217 # Copyright (C) 2005 by Canonical Ltd # -*- coding: utf-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Black-box tests for bzr. These check that it behaves properly when it's invoked through the regular command-line interface. This always reinvokes bzr through a new Python interpreter, which is a bit inefficient but arguably tests in a way more representative of how it's normally invoked. """ # this code was previously in testbzr from unittest import TestCase from bzrlib.selftest import TestBase, InTempDir class TestVersion(TestBase): def runTest(self): # output is intentionally passed through to stdout so that we # can see the version being tested self.runcmd(['bzr', 'version']) class HelpCommands(TestBase): def runTest(self): self.runcmd('bzr --help') self.runcmd('bzr help') self.runcmd('bzr help commands') self.runcmd('bzr help help') self.runcmd('bzr commit -h') class InitBranch(InTempDir): def runTest(self): import os self.runcmd(['bzr', 'init']) class UserIdentity(InTempDir): def runTest(self): # this should always identify something, if only "john@localhost" self.runcmd("bzr whoami") self.runcmd("bzr whoami --email") self.assertEquals(self.backtick("bzr whoami --email").count('@'), 1) class InvalidCommands(InTempDir): def runTest(self): self.runcmd("bzr pants", retcode=1) self.runcmd("bzr --pants off", retcode=1) self.runcmd("bzr diff --message foo", retcode=1) class OldTests(InTempDir): # old tests moved from ./testbzr def runTest(self): from os import chdir, mkdir from os.path import exists import os runcmd = self.runcmd backtick = self.backtick progress = self.log progress("basic branch creation") runcmd(['mkdir', 'branch1']) chdir('branch1') runcmd('bzr init') self.assertEquals(backtick('bzr root').rstrip(), os.path.join(self.test_dir, 'branch1')) progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") self.assertEquals(out, 'test.txt\n') out = backtick("bzr status") assert out == 'unknown:\n test.txt\n' out = backtick("bzr status --all") assert out == "unknown:\n test.txt\n" out = backtick("bzr status test.txt --all") assert out == "unknown:\n test.txt\n" f = file('test2.txt', 'wt') f.write('goodbye cruel world...\n') f.close() out = backtick("bzr status test.txt") assert out == "unknown:\n test.txt\n" out = backtick("bzr status") assert out == ("unknown:\n" " test.txt\n" " test2.txt\n") os.unlink('test2.txt') progress("command aliases") out = backtick("bzr st --all") assert out == ("unknown:\n" " test.txt\n") out = backtick("bzr stat") assert out == ("unknown:\n" " test.txt\n") progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == ("added:\n" " test.txt\n") progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == os.path.join("sub2", "hello.txt\n") assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) chdir('sub1/sub2') self.assertEquals(backtick('bzr root')[:-1], os.path.join(self.test_dir, 'branch1')) runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') runcmd(['bzr', 'commit', '-m', 'move to parent directory']) chdir('..') assert backtick('bzr relpath sub2/hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') runcmd('bzr log') runcmd('bzr log -v') progress("file with spaces in name") mkdir('sub directory') file('sub directory/file with spaces ', 'wt').write('see how this works\n') runcmd('bzr add .') runcmd('bzr diff') runcmd('bzr commit -m add-spaces') runcmd('bzr check') runcmd('bzr log') runcmd('bzr log --forward') runcmd('bzr info') chdir('..') chdir('..') progress('branch') # Can't create a branch if it already exists runcmd('bzr branch branch1', retcode=1) # Can't create a branch if its parent doesn't exist runcmd('bzr branch /unlikely/to/exist', retcode=1) runcmd('bzr branch branch1 branch2') progress("pull") chdir('branch1') runcmd('bzr pull', retcode=1) runcmd('bzr pull ../branch2') chdir('.bzr') runcmd('bzr pull') runcmd('bzr commit -m empty') runcmd('bzr pull') chdir('../../branch2') runcmd('bzr pull') runcmd('bzr commit -m empty') chdir('../branch1') runcmd('bzr commit -m empty') runcmd('bzr pull', retcode=1) chdir ('..') progress('status after remove') mkdir('status-after-remove') # see mail from William Dodé, 2005-05-25 # $ bzr init; touch a; bzr add a; bzr commit -m "add a" # * looking for changes... # added a # * commited r1 # $ bzr remove a # $ bzr status # bzr: local variable 'kind' referenced before assignment # at /vrac/python/bazaar-ng/bzrlib/diff.py:286 in compare_trees() # see ~/.bzr.log for debug information chdir('status-after-remove') runcmd('bzr init') file('a', 'w').write('foo') runcmd('bzr add a') runcmd(['bzr', 'commit', '-m', 'add a']) runcmd('bzr remove a') runcmd('bzr status') chdir('..') progress('ignore patterns') mkdir('ignorebranch') chdir('ignorebranch') runcmd('bzr init') assert backtick('bzr unknowns') == '' file('foo.tmp', 'wt').write('tmp files are ignored') assert backtick('bzr unknowns') == '' file('foo.c', 'wt').write('int main() {}') assert backtick('bzr unknowns') == 'foo.c\n' runcmd('bzr add foo.c') assert backtick('bzr unknowns') == '' # 'ignore' works when creating the .bzignore file file('foo.blah', 'wt').write('blah') assert backtick('bzr unknowns') == 'foo.blah\n' runcmd('bzr ignore *.blah') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\n' # 'ignore' works when then .bzrignore file already exists file('garh', 'wt').write('garh') assert backtick('bzr unknowns') == 'garh\n' runcmd('bzr ignore garh') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\ngarh\n' chdir('..') progress("recursive and non-recursive add") mkdir('no-recurse') chdir('no-recurse') runcmd('bzr init') mkdir('foo') fp = os.path.join('foo', 'test.txt') f = file(fp, 'w') f.write('hello!\n') f.close() runcmd('bzr add --no-recurse foo') runcmd('bzr file-id foo') runcmd('bzr file-id ' + fp, 1) # not versioned yet runcmd('bzr commit -m add-dir-only') runcmd('bzr file-id ' + fp, 1) # still not versioned runcmd('bzr add foo') runcmd('bzr file-id ' + fp) runcmd('bzr commit -m add-sub-file') chdir('..') # lists all tests from this module in the best order to run them. we # do it this way rather than just discovering them all because it # allows us to test more basic functions first where failures will be # easiest to understand. def suite(): from unittest import TestSuite s = TestSuite() s.addTests([TestVersion(), InitBranch(), HelpCommands(), UserIdentity(), InvalidCommands(), OldTests()]) return s commit refs/heads/master mark :781 committer Martin Pool 1119611230 +1000 data 49 - start of simple test for unknown-file reporting from :780 M 644 inline bzrlib/selftest/whitebox.py data 4226 #! /usr/bin/python import os import unittest from bzrlib.selftest import InTempDir, TestBase from bzrlib.branch import ScratchBranch, Branch from bzrlib.errors import NotBranchError, NotVersionedError class Unknowns(InTempDir): def runTest(self): b = Branch('.', init=True) self.build_tree(['hello.txt', 'hello.txt~']) self.assertEquals(list(b.unknowns()), ['hello.txt']) class Revert(InTempDir): """Test selected-file revert""" def runTest(self): b = Branch('.', init=True) self.build_tree(['hello.txt']) file('hello.txt', 'w').write('initial hello') self.assertRaises(NotVersionedError, b.revert, ['hello.txt']) b.add(['hello.txt']) b.commit('create initial hello.txt') self.check_file_contents('hello.txt', 'initial hello') file('hello.txt', 'w').write('new hello') self.check_file_contents('hello.txt', 'new hello') # revert file modified since last revision b.revert(['hello.txt']) self.check_file_contents('hello.txt', 'initial hello') # reverting again causes no change b.revert(['hello.txt']) self.check_file_contents('hello.txt', 'initial hello') class RenameDirs(InTempDir): """Test renaming directories and the files within them.""" def runTest(self): b = Branch('.', init=True) self.build_tree(['dir/', 'dir/sub/', 'dir/sub/file']) b.add(['dir', 'dir/sub', 'dir/sub/file']) b.commit('create initial state') # TODO: lift out to a test helper that checks the shape of # an inventory revid = b.revision_history()[0] self.log('first revision_id is {%s}' % revid) inv = b.get_revision_inventory(revid) self.log('contents of inventory: %r' % inv.entries()) self.check_inventory_shape(inv, ['dir', 'dir/sub', 'dir/sub/file']) b.rename_one('dir', 'newdir') self.check_inventory_shape(b.inventory, ['newdir', 'newdir/sub', 'newdir/sub/file']) b.rename_one('newdir/sub', 'newdir/newsub') self.check_inventory_shape(b.inventory, ['newdir', 'newdir/newsub', 'newdir/newsub/file']) class BranchPathTestCase(TestBase): """test for branch path lookups Branch.relpath and bzrlib.branch._relpath do a simple but subtle job: given a path (either relative to cwd or absolute), work out if it is inside a branch and return the path relative to the base. """ def runTest(self): from bzrlib.branch import _relpath import tempfile, shutil savedir = os.getcwdu() dtmp = tempfile.mkdtemp() def rp(p): return _relpath(dtmp, p) try: # check paths inside dtmp while standing outside it self.assertEqual(rp(os.path.join(dtmp, 'foo')), 'foo') # root = nothing self.assertEqual(rp(dtmp), '') self.assertRaises(NotBranchError, rp, '/etc') # now some near-miss operations -- note that # os.path.commonprefix gets these wrong! self.assertRaises(NotBranchError, rp, dtmp.rstrip('\\/') + '2') self.assertRaises(NotBranchError, rp, dtmp.rstrip('\\/') + '2/foo') # now operations based on relpath of files in current # directory, or nearby os.chdir(dtmp) self.assertEqual(rp('foo/bar/quux'), 'foo/bar/quux') self.assertEqual(rp('foo'), 'foo') self.assertEqual(rp('./foo'), 'foo') self.assertEqual(rp(os.path.abspath('foo')), 'foo') self.assertRaises(NotBranchError, rp, '../foo') finally: os.chdir(savedir) shutil.rmtree(dtmp) commit refs/heads/master mark :782 committer Martin Pool 1119611376 +1000 data 61 - Branch.revert copies files to backups before reverting them from :781 M 644 inline bzrlib/branch.py data 37286 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_file, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def _relpath(base, path): """Return path relative to base, or raise exception. The path may be either an absolute path or a path relative to the current working directory. Lifted out of Branch.relpath for ease of testing. os.path.commonprefix (python2.4) has a bad bug that it works just on string prefixes, assuming that '/u' is a prefix of '/u2'. This avoids that problem.""" rp = os.path.abspath(path) s = [] head = rp while len(head) >= len(base): if head == base: break head, tail = os.path.split(head) if tail: s.insert(0, tail) else: from errors import NotBranchError raise NotBranchError("path %r is not within branch %r" % (rp, base)) return os.sep.join(s) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head class DivergedBranches(Exception): def __init__(self, branch1, branch2): self.branch1 = branch1 self.branch2 = branch2 Exception.__init__(self, "These branches have diverged.") class NoSuchRevision(BzrError): def __init__(self, branch, revision): self.branch = branch self.revision = revision msg = "Branch %s has no revision %d" % (branch, revision) BzrError.__init__(self, msg) ###################################################################### # branch objects class Branch(object): """Branch holding a history of revisions. base Base directory of the branch. _lock_mode None, or 'r' or 'w' _lock_count If _lock_mode is true, a positive count of the number of times the lock has been taken. _lock Lock object from bzrlib.lock. """ base = None _lock_mode = None _lock_count = None _lock = None def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): from errors import NotBranchError raise NotBranchError("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def __del__(self): if self._lock_mode or self._lock: from warnings import warn warn("branch %r was not explicitly unlocked" % self) self._lock.unlock() def lock_write(self): if self._lock_mode: if self._lock_mode != 'w': from errors import LockError raise LockError("can't upgrade to a write lock from %r" % self._lock_mode) self._lock_count += 1 else: from bzrlib.lock import WriteLock self._lock = WriteLock(self.controlfilename('branch-lock')) self._lock_mode = 'w' self._lock_count = 1 def lock_read(self): if self._lock_mode: assert self._lock_mode in ('r', 'w'), \ "invalid lock mode %r" % self._lock_mode self._lock_count += 1 else: from bzrlib.lock import ReadLock self._lock = ReadLock(self.controlfilename('branch-lock')) self._lock_mode = 'r' self._lock_count = 1 def unlock(self): if not self._lock_mode: from errors import LockError raise LockError('branch %r is not locked' % (self)) if self._lock_count > 1: self._lock_count -= 1 else: self._lock.unlock() self._lock = None self._lock_mode = self._lock_count = None def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" return _relpath(self.base, path) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.\n") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: raise BzrError('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. self.lock_read() try: inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv finally: self.unlock() def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self.lock_write() try: from bzrlib.atomicfile import AtomicFile f = AtomicFile(self.controlfilename('inventory'), 'wb') try: inv.write_xml(f) f.commit() finally: f.close() finally: self.unlock() mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. files List of paths to add, relative to the base of the tree. ids If set, use these instead of automatically generated ids. Must be the same length as the list of files, but may contain None for ids that are to be autogenerated. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) self.lock_write() try: inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): raise BzrError("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: raise BzrError("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: print 'added', quotefn(f) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) finally: self.unlock() def print_file(self, file, revno): """Print `file` to stdout.""" self.lock_read() try: tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: raise BzrError("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) finally: self.unlock() def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] self.lock_write() try: tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: raise BzrError("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) finally: self.unlock() # FIXME: this doesn't need to be a branch method def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): from bzrlib.atomicfile import AtomicFile mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() + [revision_id] f = AtomicFile(self.controlfilename('revision-history')) try: for rev_id in rev_history: print >>f, rev_id f.commit() finally: f.close() def get_revision(self, revision_id): """Return the Revision object for a named revision""" if not revision_id or not isinstance(revision_id, basestring): raise ValueError('invalid revision-id: %r' % revision_id) r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_revision_sha1(self, revision_id): """Hash the stored value of a revision, and return it.""" # In the future, revision entries will be signed. At that # point, it is probably best *not* to include the signature # in the revision hash. Because that lets you re-sign # the revision, (add signatures/remove signatures) and still # have all hash pointers stay consistent. # But for now, just hash the contents. return sha_file(self.revision_store[revision_id]) def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_inventory_sha1(self, inventory_id): """Return the sha1 hash of the inventory entry """ return sha_file(self.inventory_store[inventory_id]) def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self.lock_read() try: return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] finally: self.unlock() def common_ancestor(self, other, self_revno=None, other_revno=None): """ >>> import commit >>> sb = ScratchBranch(files=['foo', 'foo~']) >>> sb.common_ancestor(sb) == (None, None) True >>> commit.commit(sb, "Committing first revision", verbose=False) >>> sb.common_ancestor(sb)[0] 1 >>> clone = sb.clone() >>> commit.commit(sb, "Committing second revision", verbose=False) >>> sb.common_ancestor(sb)[0] 2 >>> sb.common_ancestor(clone)[0] 1 >>> commit.commit(clone, "Committing divergent second revision", ... verbose=False) >>> sb.common_ancestor(clone)[0] 1 >>> sb.common_ancestor(clone) == clone.common_ancestor(sb) True >>> sb.common_ancestor(sb) != clone.common_ancestor(clone) True >>> clone2 = sb.clone() >>> sb.common_ancestor(clone2)[0] 2 >>> sb.common_ancestor(clone2, self_revno=1)[0] 1 >>> sb.common_ancestor(clone2, other_revno=1)[0] 1 """ my_history = self.revision_history() other_history = other.revision_history() if self_revno is None: self_revno = len(my_history) if other_revno is None: other_revno = len(other_history) indices = range(min((self_revno, other_revno))) indices.reverse() for r in indices: if my_history[r] == other_history[r]: return r+1, my_history[r] return None, None def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def missing_revisions(self, other, stop_revision=None): """ If self and other have not diverged, return a list of the revisions present in other, but missing from self. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch() >>> br2 = ScratchBranch() >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [u'REVISION-ID-1'] >>> br2.missing_revisions(br1) [] >>> commit(br1, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-2A") >>> br1.missing_revisions(br2) [u'REVISION-ID-2A'] >>> commit(br1, "lala!", rev_id="REVISION-ID-2B") >>> br1.missing_revisions(br2) Traceback (most recent call last): DivergedBranches: These branches have diverged. """ self_history = self.revision_history() self_len = len(self_history) other_history = other.revision_history() other_len = len(other_history) common_index = min(self_len, other_len) -1 if common_index >= 0 and \ self_history[common_index] != other_history[common_index]: raise DivergedBranches(self, other) if stop_revision is None: stop_revision = other_len elif stop_revision > other_len: raise NoSuchRevision(self, stop_revision) return other_history[self_len:stop_revision] def update_revisions(self, other, stop_revision=None): """Pull in all new revisions from other branch. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch(files=['foo', 'bar']) >>> br1.add('foo') >>> br1.add('bar') >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False) >>> br2 = ScratchBranch() >>> br2.update_revisions(br1) Added 2 texts. Added 1 inventories. Added 1 revisions. >>> br2.revision_history() [u'REVISION-ID-1'] >>> br2.update_revisions(br1) Added 0 texts. Added 0 inventories. Added 0 revisions. >>> br1.text_store.total_size() == br2.text_store.total_size() True """ from bzrlib.progress import ProgressBar pb = ProgressBar() pb.update('comparing histories') revision_ids = self.missing_revisions(other, stop_revision) revisions = [] needed_texts = sets.Set() i = 0 for rev_id in revision_ids: i += 1 pb.update('fetching revision', i, len(revision_ids)) rev = other.get_revision(rev_id) revisions.append(rev) inv = other.get_inventory(str(rev.inventory_id)) for key, entry in inv.iter_entries(): if entry.text_id is None: continue if entry.text_id not in self.text_store: needed_texts.add(entry.text_id) pb.clear() count = self.text_store.copy_multi(other.text_store, needed_texts) print "Added %d texts." % count inventory_ids = [ f.inventory_id for f in revisions ] count = self.inventory_store.copy_multi(other.inventory_store, inventory_ids) print "Added %d inventories." % count revision_ids = [ f.revision_id for f in revisions] count = self.revision_store.copy_multi(other.revision_store, revision_ids) for revision_id in revision_ids: self.append_revision(revision_id) print "Added %d revisions." % count def commit(self, *args, **kw): from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self.lock_write() try: tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): raise BzrError("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): raise BzrError("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: raise BzrError("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): raise BzrError("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': raise BzrError("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self.lock_write() try: ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): raise BzrError("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): raise BzrError("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': raise BzrError("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): raise BzrError("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): raise BzrError("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: raise BzrError("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): raise BzrError("destination %r already exists" % dest_path) if f_id in to_idpath: raise BzrError("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() def revert(self, filenames, old_tree=None, backups=True): """Restore selected files to the versions from a previous tree. backups If true (default) backups are made of files before they're renamed. """ from bzrlib.errors import NotVersionedError, BzrError from bzrlib.atomicfile import AtomicFile from bzrlib.osutils import backup_file inv = self.read_working_inventory() if old_tree is None: old_tree = self.basis_tree() old_inv = old_tree.inventory nids = [] for fn in filenames: file_id = inv.path2id(fn) if not file_id: raise NotVersionedError("not a versioned file", fn) if not old_inv.has_id(file_id): raise BzrError("file not present in old tree", fn, file_id) nids.append((fn, file_id)) # TODO: Rename back if it was previously at a different location # TODO: If given a directory, restore the entire contents from # the previous version. # TODO: Make a backup to a temporary file. # TODO: If the file previously didn't exist, delete it? for fn, file_id in nids: backup_file(fn) f = AtomicFile(fn, 'wb') try: f.write(old_tree.get_file(file_id).read()) f.commit() finally: f.close() class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[], base=None): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ init = False if base is None: base = tempfile.mkdtemp() init = True Branch.__init__(self, base, init=init) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def clone(self): """ >>> orig = ScratchBranch(files=["file1", "file2"]) >>> clone = orig.clone() >>> os.path.samefile(orig.base, clone.base) False >>> os.path.isfile(os.path.join(clone.base, "file1")) True """ base = tempfile.mkdtemp() os.rmdir(base) shutil.copytree(self.base, base, symlinks=True) return ScratchBranch(base=base) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: if self.base: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/selftest/whitebox.py data 4353 #! /usr/bin/python import os import unittest from bzrlib.selftest import InTempDir, TestBase from bzrlib.branch import ScratchBranch, Branch from bzrlib.errors import NotBranchError, NotVersionedError class Unknowns(InTempDir): def runTest(self): b = Branch('.', init=True) self.build_tree(['hello.txt', 'hello.txt~']) self.assertEquals(list(b.unknowns()), ['hello.txt']) class Revert(InTempDir): """Test selected-file revert""" def runTest(self): b = Branch('.', init=True) self.build_tree(['hello.txt']) file('hello.txt', 'w').write('initial hello') self.assertRaises(NotVersionedError, b.revert, ['hello.txt']) b.add(['hello.txt']) b.commit('create initial hello.txt') self.check_file_contents('hello.txt', 'initial hello') file('hello.txt', 'w').write('new hello') self.check_file_contents('hello.txt', 'new hello') # revert file modified since last revision b.revert(['hello.txt']) self.check_file_contents('hello.txt', 'initial hello') self.check_file_contents('hello.txt~', 'new hello') # reverting again clobbers the backup b.revert(['hello.txt']) self.check_file_contents('hello.txt', 'initial hello') self.check_file_contents('hello.txt~', 'initial hello') class RenameDirs(InTempDir): """Test renaming directories and the files within them.""" def runTest(self): b = Branch('.', init=True) self.build_tree(['dir/', 'dir/sub/', 'dir/sub/file']) b.add(['dir', 'dir/sub', 'dir/sub/file']) b.commit('create initial state') # TODO: lift out to a test helper that checks the shape of # an inventory revid = b.revision_history()[0] self.log('first revision_id is {%s}' % revid) inv = b.get_revision_inventory(revid) self.log('contents of inventory: %r' % inv.entries()) self.check_inventory_shape(inv, ['dir', 'dir/sub', 'dir/sub/file']) b.rename_one('dir', 'newdir') self.check_inventory_shape(b.inventory, ['newdir', 'newdir/sub', 'newdir/sub/file']) b.rename_one('newdir/sub', 'newdir/newsub') self.check_inventory_shape(b.inventory, ['newdir', 'newdir/newsub', 'newdir/newsub/file']) class BranchPathTestCase(TestBase): """test for branch path lookups Branch.relpath and bzrlib.branch._relpath do a simple but subtle job: given a path (either relative to cwd or absolute), work out if it is inside a branch and return the path relative to the base. """ def runTest(self): from bzrlib.branch import _relpath import tempfile, shutil savedir = os.getcwdu() dtmp = tempfile.mkdtemp() def rp(p): return _relpath(dtmp, p) try: # check paths inside dtmp while standing outside it self.assertEqual(rp(os.path.join(dtmp, 'foo')), 'foo') # root = nothing self.assertEqual(rp(dtmp), '') self.assertRaises(NotBranchError, rp, '/etc') # now some near-miss operations -- note that # os.path.commonprefix gets these wrong! self.assertRaises(NotBranchError, rp, dtmp.rstrip('\\/') + '2') self.assertRaises(NotBranchError, rp, dtmp.rstrip('\\/') + '2/foo') # now operations based on relpath of files in current # directory, or nearby os.chdir(dtmp) self.assertEqual(rp('foo/bar/quux'), 'foo/bar/quux') self.assertEqual(rp('foo'), 'foo') self.assertEqual(rp('./foo'), 'foo') self.assertEqual(rp(os.path.abspath('foo')), 'foo') self.assertRaises(NotBranchError, rp, '../foo') finally: os.chdir(savedir) shutil.rmtree(dtmp) commit refs/heads/master mark :783 committer Martin Pool 1119611475 +1000 data 46 - run blackbox tests last because they're slow from :782 M 644 inline bzrlib/selftest/__init__.py data 8774 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") raise class CommandFailed(Exception): pass class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr, so that we can run it # through a specified Python intepreter OVERRIDE_PYTHON = None # to run with alternative python 'python' BZRPATH = 'bzr' _log_buf = "" def formcmd(self, cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = self.BZRPATH if self.OVERRIDE_PYTHON: cmd.insert(0, self.OVERRIDE_PYTHON) self.log('$ %r' % cmd) return cmd def runcmd(self, cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = self.formcmd(cmd) self.log('$ ' + ' '.join(cmd)) actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(self, cmd, retcode=0): """Run a command and return its output""" cmd = self.formcmd(cmd) child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG) outd, errd = child.communicate() self.log(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def build_tree(self, shape): """Build a test tree according to a pattern. shape is a sequence of file specifications. If the final character is '/', a directory is created. This doesn't add anything to a branch. """ # XXX: It's OK to just create them using forward slashes on windows? import os for name in shape: assert isinstance(name, basestring) if name[-1] == '/': os.mkdir(name[:-1]) else: f = file(name, 'wt') print >>f, "contents of", name f.close() def log(self, msg): """Log a message to a progress file""" self._log_buf = self._log_buf + str(msg) + '\n' print >>self.TEST_LOG, msg def check_inventory_shape(self, inv, shape): """ Compare an inventory to a list of expected names. Fail if they are not precisely equal. """ extras = [] shape = list(shape) # copy for path, ie in inv.entries(): name = path.replace('\\', '/') if ie.kind == 'dir': name = name + '/' if name in shape: shape.remove(name) else: extras.append(name) if shape: self.fail("expected paths not found in inventory: %r" % shape) if extras: self.fail("unexpected paths found in inventory: %r" % extras) def check_file_contents(self, filename, expect): self.log("check contents of file %s" % filename) contents = file(filename, 'r').read() if contents != expect: self.log("expected: %r" % expected) self.log("actually: %r" % contents) self.fail("contents of %s not as expected") class InTempDir(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.test_dir = os.path.join(self.TEST_ROOT, self.__class__.__name__) os.mkdir(self.test_dir) os.chdir(self.test_dir) def tearDown(self): import os os.chdir(self.TEST_ROOT) class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ def __init__(self, out): self.out = out TestResult.__init__(self) def startTest(self, test): # TODO: Maybe show test.shortDescription somewhere? print >>self.out, '%-60.60s' % test.id(), self.out.flush() TestResult.startTest(self, test) def stopTest(self, test): # print TestResult.stopTest(self, test) def addError(self, test, err): print >>self.out, 'ERROR' TestResult.addError(self, test, err) _show_test_failure('error', test, err, self.out) def addFailure(self, test, err): print >>self.out, 'FAILURE' TestResult.addFailure(self, test, err) _show_test_failure('failure', test, err, self.out) def addSuccess(self, test): print >>self.out, 'OK' TestResult.addSuccess(self, test) def selftest(): from unittest import TestLoader, TestSuite import bzrlib import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning from doctest import DocTestSuite import os import shutil import time import sys _setup_test_log() _setup_test_dir() print suite = TestSuite() tl = TestLoader() for m in bzrlib.selftest.whitebox, \ bzrlib.selftest.versioning: suite.addTest(tl.loadTestsFromModule(m)) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands: suite.addTest(DocTestSuite(m)) suite.addTest(bzrlib.selftest.blackbox.suite()) # save stdout & stderr so there's no leakage from code-under-test real_stdout = sys.stdout real_stderr = sys.stderr sys.stdout = sys.stderr = TestBase.TEST_LOG try: result = _MyResult(real_stdout) suite.run(result) finally: sys.stdout = real_stdout sys.stderr = real_stderr _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os log_filename = os.path.abspath('testbzr.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "bzr tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_ROOT = os.path.abspath("testbzr.tmp") print '%-30s %s' % ('running tests in', TestBase.TEST_ROOT) if os.path.exists(TestBase.TEST_ROOT): shutil.rmtree(TestBase.TEST_ROOT) os.mkdir(TestBase.TEST_ROOT) os.chdir(TestBase.TEST_ROOT) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_ROOT, '.bzr')) def _show_results(result): print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, exc_info, out): from traceback import print_exception print >>out, '-' * 60 print >>out, case desc = case.shortDescription() if desc: print >>out, ' (%s)' % desc print_exception(exc_info[0], exc_info[1], exc_info[2], None, out) if isinstance(case, TestBase): print >>out print >>out, 'log from this test:' print >>out, case._log_buf print >>out, '-' * 60 commit refs/heads/master mark :784 committer Martin Pool 1119611531 +1000 data 97 - rename merge-based revert command to 'merge-revert' - do simple file-based revert with 'revert' from :783 M 644 inline bzrlib/commands.py data 52221 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date plugin_cmds = {} def register_command(cmd): "Utility function to help register a command" global plugin_cmds k = cmd.__name__ if k.startswith("cmd_"): k_unsquished = _unsquish_command_name(k) else: k_unsquished = k if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = cmd else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r' % sys.modules[cmd.__module__]) def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _get_cmd_dict(plugins_override=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v # If we didn't load plugins, the plugin_cmds dict will be empty if plugins_override: d.update(plugin_cmds) else: d2 = plugin_cmds.copy() d2.update(d) d = d2 return d def get_all_cmds(plugins_override=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(plugins_override=plugins_override).iteritems(): yield k,v def get_cmd_class(cmd, plugins_override=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(plugins_override=plugins_override) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() for opt in self.takes_options: if not opt in OPTIONS: raise BzrError("Unknown option '%s' returned by external command %s" % (opt, path)) # TODO: Is there any way to check takes_args is valid here? self.takes_args = pipe.readline().split() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-usage'" % path) pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-help'" % path) def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: optname = name.replace('_','-') value = kargs[name] if OPTIONS.has_key(optname): # it's an option opts.append('--%s' % optname) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_mkdir(Command): """Create a new versioned directory. This is equivalent to creating the directory and then adding it. """ takes_args = ['dir+'] def run(self, dir_list): import os import bzrlib.branch b = None for d in dir_list: os.mkdir(d) if not b: b = bzrlib.branch.Branch(d) b.add([d], verbose=True) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import errno br_to = Branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if errno == errno.ENOENT: raise if location is None: if stored_loc is None: raise BzrCommandError("No pull location known or specified.") else: print "Using last location: %s" % stored_loc location = stored_loc from branch import find_branch, DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged. Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. To retrieve the branch as of a particular revision, supply the --revision parameter, as in "branch foo/bar -r 5". """ takes_args = ['from_location', 'to_location?'] takes_options = ['revision'] def run(self, from_location, to_location=None, revision=None): import errno from bzrlib.merge import merge from branch import find_branch, DivergedBranches, NoSuchRevision from shutil import rmtree try: br_from = find_branch(from_location) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location.rstrip("/\\")) try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) try: br_to.update_revisions(br_from, stop_revision=revision) except NoSuchRevision: rmtree(to_location) msg = "The branch %s has no revision %d." % (from_location, revision) raise BzrCommandError(msg) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False, ignore_zero=True) from_location = pull_loc(br_from) br_to.controlfile("x-pull", "wb").write(from_location + "\n") def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: raise BzrError("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: raise BzrError("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, exports to a directory (equivalent to --format=dir).""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format'] def run(self, dest, revision=None, format='dir'): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest, format) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit from bzrlib.osutils import get_text_message ## Warning: shadows builtin file() if not message and not file: import cStringIO stdout = sys.stdout catcher = cStringIO.StringIO() sys.stdout = catcher cmd_status({"file_list":selected_list}, {}) info = catcher.getvalue() sys.stdout = stdout message = get_text_message(info) if message is None: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_upgrade(Command): """Upgrade branch storage to current format. This should normally be used only after the check command tells you to run it. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.upgrade import upgrade upgrade(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest return int(not selftest()) class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Restore selected files from a previous revision. """ takes_args = ['file+'] def run(self, file_list): if not file_list: file_list = ['.'] b = find_branch(file_list[0]) b.revert([b.relpath(f) for f in file_list]) class cmd_merge_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) class cmd_plugins(Command): """List plugins""" hidden = True def run(self): import bzrlib.plugin from pprint import pprint pprint(bzrlib.plugin.all_plugins) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'update': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) >>> parse_args('log -r 500'.split()) (['log'], {'revision': 500}) >>> parse_args('log -r500:600'.split()) (['log'], {'revision': [500, 600]}) >>> parse_args('log -vr500:600'.split()) (['log'], {'verbose': True, 'revision': [500, 600]}) >>> parse_args('log -rv500:600'.split()) #the r takes an argument Traceback (most recent call last): ... ValueError: invalid literal for int(): v500 """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: raise BzrError('unknown long option %r' % a) else: shortopt = a[1:] if shortopt in SHORT_OPTIONS: # Multi-character options must have a space to delimit # their value optname = SHORT_OPTIONS[shortopt] else: # Single character short options, can be chained, # and have their value appended to their name shortopt = a[1:2] if shortopt not in SHORT_OPTIONS: # We didn't find the multi-character name, and we # didn't find the single char name raise BzrError('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if a[2:]: # There are extra things on this option # see if it is the value, or if it is another # short option optargfn = OPTIONS[optname] if optargfn is None: # This option does not take an argument, so the # next entry is another short option, pack it back # into the list argv.insert(0, '-' + a[2:]) else: # This option takes an argument, so pack it # into the array optarg = a[2:] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? raise BzrError('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: raise BzrError('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: raise BzrError('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def _parse_master_args(argv): """Parse the arguments that always go with the original command. These are things like bzr --no-plugins, etc. There are now 2 types of option flags. Ones that come *before* the command, and ones that come *after* the command. Ones coming *before* the command are applied against all possible commands. And are generally applied before plugins are loaded. The current list are: --builtin Allow plugins to load, but don't let them override builtin commands, they will still be allowed if they do not override a builtin. --no-plugins Don't load any plugins. This lets you get back to official source behavior. --profile Enable the hotspot profile before running the command. For backwards compatibility, this is also a non-master option. --version Spit out the version of bzr that is running and exit. This is also a non-master option. --help Run help and exit, also a non-master option (I think that should stay, though) >>> argv, opts = _parse_master_args(['bzr', '--test']) Traceback (most recent call last): ... BzrCommandError: Invalid master option: 'test' >>> argv, opts = _parse_master_args(['bzr', '--version', 'command']) >>> print argv ['command'] >>> print opts['version'] True >>> argv, opts = _parse_master_args(['bzr', '--profile', 'command', '--more-options']) >>> print argv ['command', '--more-options'] >>> print opts['profile'] True >>> argv, opts = _parse_master_args(['bzr', '--no-plugins', 'command']) >>> print argv ['command'] >>> print opts['no-plugins'] True >>> print opts['profile'] False >>> argv, opts = _parse_master_args(['bzr', 'command', '--profile']) >>> print argv ['command', '--profile'] >>> print opts['profile'] False """ master_opts = {'builtin':False, 'no-plugins':False, 'version':False, 'profile':False, 'help':False } # This is the point where we could hook into argv[0] to determine # what front-end is supposed to be run # For now, we are just ignoring it. cmd_name = argv.pop(0) for arg in argv[:]: if arg[:2] != '--': # at the first non-option, we return the rest break arg = arg[2:] # Remove '--' if arg not in master_opts: # We could say that this is not an error, that we should # just let it be handled by the main section instead raise BzrCommandError('Invalid master option: %r' % arg) argv.pop(0) # We are consuming this entry master_opts[arg] = True return argv, master_opts def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: # some options like --builtin and --no-plugins have special effects argv, master_opts = _parse_master_args(argv) if not master_opts['no-plugins']: bzrlib.load_plugins() args, opts = parse_args(argv) if master_opts['help']: from bzrlib.help import help if argv: help(argv[0]) else: help() return 0 if 'help' in opts: from bzrlib.help import help if args: help(args[0]) else: help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 plugins_override = not (master_opts['builtin']) canonical_cmd, cmd_class = get_cmd_class(cmd, plugins_override=plugins_override) profile = master_opts['profile'] # For backwards compatibility, I would rather stick with --profile being a # master/global option if 'profile' in opts: profile = True del opts['profile'] # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :785 committer Martin Pool 1119611737 +1000 data 38 - add test for external revert command from :784 M 644 inline bzrlib/selftest/blackbox.py data 11623 # Copyright (C) 2005 by Canonical Ltd # -*- coding: utf-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Black-box tests for bzr. These check that it behaves properly when it's invoked through the regular command-line interface. This always reinvokes bzr through a new Python interpreter, which is a bit inefficient but arguably tests in a way more representative of how it's normally invoked. """ # this code was previously in testbzr from unittest import TestCase from bzrlib.selftest import TestBase, InTempDir class TestVersion(TestBase): def runTest(self): # output is intentionally passed through to stdout so that we # can see the version being tested self.runcmd(['bzr', 'version']) class HelpCommands(TestBase): def runTest(self): self.runcmd('bzr --help') self.runcmd('bzr help') self.runcmd('bzr help commands') self.runcmd('bzr help help') self.runcmd('bzr commit -h') class InitBranch(InTempDir): def runTest(self): import os self.runcmd(['bzr', 'init']) class UserIdentity(InTempDir): def runTest(self): # this should always identify something, if only "john@localhost" self.runcmd("bzr whoami") self.runcmd("bzr whoami --email") self.assertEquals(self.backtick("bzr whoami --email").count('@'), 1) class InvalidCommands(InTempDir): def runTest(self): self.runcmd("bzr pants", retcode=1) self.runcmd("bzr --pants off", retcode=1) self.runcmd("bzr diff --message foo", retcode=1) class OldTests(InTempDir): # old tests moved from ./testbzr def runTest(self): from os import chdir, mkdir from os.path import exists import os runcmd = self.runcmd backtick = self.backtick progress = self.log progress("basic branch creation") runcmd(['mkdir', 'branch1']) chdir('branch1') runcmd('bzr init') self.assertEquals(backtick('bzr root').rstrip(), os.path.join(self.test_dir, 'branch1')) progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") self.assertEquals(out, 'test.txt\n') out = backtick("bzr status") assert out == 'unknown:\n test.txt\n' out = backtick("bzr status --all") assert out == "unknown:\n test.txt\n" out = backtick("bzr status test.txt --all") assert out == "unknown:\n test.txt\n" f = file('test2.txt', 'wt') f.write('goodbye cruel world...\n') f.close() out = backtick("bzr status test.txt") assert out == "unknown:\n test.txt\n" out = backtick("bzr status") assert out == ("unknown:\n" " test.txt\n" " test2.txt\n") os.unlink('test2.txt') progress("command aliases") out = backtick("bzr st --all") assert out == ("unknown:\n" " test.txt\n") out = backtick("bzr stat") assert out == ("unknown:\n" " test.txt\n") progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == ("added:\n" " test.txt\n") progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == os.path.join("sub2", "hello.txt\n") assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) chdir('sub1/sub2') self.assertEquals(backtick('bzr root')[:-1], os.path.join(self.test_dir, 'branch1')) runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') runcmd(['bzr', 'commit', '-m', 'move to parent directory']) chdir('..') assert backtick('bzr relpath sub2/hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') runcmd('bzr log') runcmd('bzr log -v') progress("file with spaces in name") mkdir('sub directory') file('sub directory/file with spaces ', 'wt').write('see how this works\n') runcmd('bzr add .') runcmd('bzr diff') runcmd('bzr commit -m add-spaces') runcmd('bzr check') runcmd('bzr log') runcmd('bzr log --forward') runcmd('bzr info') chdir('..') chdir('..') progress('branch') # Can't create a branch if it already exists runcmd('bzr branch branch1', retcode=1) # Can't create a branch if its parent doesn't exist runcmd('bzr branch /unlikely/to/exist', retcode=1) runcmd('bzr branch branch1 branch2') progress("pull") chdir('branch1') runcmd('bzr pull', retcode=1) runcmd('bzr pull ../branch2') chdir('.bzr') runcmd('bzr pull') runcmd('bzr commit -m empty') runcmd('bzr pull') chdir('../../branch2') runcmd('bzr pull') runcmd('bzr commit -m empty') chdir('../branch1') runcmd('bzr commit -m empty') runcmd('bzr pull', retcode=1) chdir ('..') progress('status after remove') mkdir('status-after-remove') # see mail from William Dodé, 2005-05-25 # $ bzr init; touch a; bzr add a; bzr commit -m "add a" # * looking for changes... # added a # * commited r1 # $ bzr remove a # $ bzr status # bzr: local variable 'kind' referenced before assignment # at /vrac/python/bazaar-ng/bzrlib/diff.py:286 in compare_trees() # see ~/.bzr.log for debug information chdir('status-after-remove') runcmd('bzr init') file('a', 'w').write('foo') runcmd('bzr add a') runcmd(['bzr', 'commit', '-m', 'add a']) runcmd('bzr remove a') runcmd('bzr status') chdir('..') progress('ignore patterns') mkdir('ignorebranch') chdir('ignorebranch') runcmd('bzr init') assert backtick('bzr unknowns') == '' file('foo.tmp', 'wt').write('tmp files are ignored') assert backtick('bzr unknowns') == '' file('foo.c', 'wt').write('int main() {}') assert backtick('bzr unknowns') == 'foo.c\n' runcmd('bzr add foo.c') assert backtick('bzr unknowns') == '' # 'ignore' works when creating the .bzignore file file('foo.blah', 'wt').write('blah') assert backtick('bzr unknowns') == 'foo.blah\n' runcmd('bzr ignore *.blah') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\n' # 'ignore' works when then .bzrignore file already exists file('garh', 'wt').write('garh') assert backtick('bzr unknowns') == 'garh\n' runcmd('bzr ignore garh') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\ngarh\n' chdir('..') progress("recursive and non-recursive add") mkdir('no-recurse') chdir('no-recurse') runcmd('bzr init') mkdir('foo') fp = os.path.join('foo', 'test.txt') f = file(fp, 'w') f.write('hello!\n') f.close() runcmd('bzr add --no-recurse foo') runcmd('bzr file-id foo') runcmd('bzr file-id ' + fp, 1) # not versioned yet runcmd('bzr commit -m add-dir-only') runcmd('bzr file-id ' + fp, 1) # still not versioned runcmd('bzr add foo') runcmd('bzr file-id ' + fp) runcmd('bzr commit -m add-sub-file') chdir('..') class RevertCommand(InTempDir): def runTest(self): self.runcmd('bzr init') file('hello', 'wt').write('foo') self.runcmd('bzr add hello') self.runcmd('bzr commit -m setup hello') file('hello', 'wt').write('bar') self.runcmd('bzr revert hello') self.check_file_contents('hello', 'foo') # lists all tests from this module in the best order to run them. we # do it this way rather than just discovering them all because it # allows us to test more basic functions first where failures will be # easiest to understand. def suite(): from unittest import TestSuite s = TestSuite() s.addTests([TestVersion(), InitBranch(), HelpCommands(), UserIdentity(), InvalidCommands(), RevertCommand(), OldTests(), ]) return s commit refs/heads/master mark :786 committer Martin Pool 1119835348 +1000 data 20 - fix missing import from :785 M 644 inline bzrlib/commands.py data 52276 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date plugin_cmds = {} def register_command(cmd): "Utility function to help register a command" global plugin_cmds k = cmd.__name__ if k.startswith("cmd_"): k_unsquished = _unsquish_command_name(k) else: k_unsquished = k if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = cmd else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r' % sys.modules[cmd.__module__]) def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _get_cmd_dict(plugins_override=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v # If we didn't load plugins, the plugin_cmds dict will be empty if plugins_override: d.update(plugin_cmds) else: d2 = plugin_cmds.copy() d2.update(d) d = d2 return d def get_all_cmds(plugins_override=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(plugins_override=plugins_override).iteritems(): yield k,v def get_cmd_class(cmd, plugins_override=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(plugins_override=plugins_override) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() for opt in self.takes_options: if not opt in OPTIONS: raise BzrError("Unknown option '%s' returned by external command %s" % (opt, path)) # TODO: Is there any way to check takes_args is valid here? self.takes_args = pipe.readline().split() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-usage'" % path) pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-help'" % path) def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: optname = name.replace('_','-') value = kargs[name] if OPTIONS.has_key(optname): # it's an option opts.append('--%s' % optname) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_mkdir(Command): """Create a new versioned directory. This is equivalent to creating the directory and then adding it. """ takes_args = ['dir+'] def run(self, dir_list): import os import bzrlib.branch b = None for d in dir_list: os.mkdir(d) if not b: b = bzrlib.branch.Branch(d) b.add([d], verbose=True) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import errno br_to = Branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if errno == errno.ENOENT: raise if location is None: if stored_loc is None: raise BzrCommandError("No pull location known or specified.") else: print "Using last location: %s" % stored_loc location = stored_loc from branch import find_branch, DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged. Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. To retrieve the branch as of a particular revision, supply the --revision parameter, as in "branch foo/bar -r 5". """ takes_args = ['from_location', 'to_location?'] takes_options = ['revision'] def run(self, from_location, to_location=None, revision=None): import errno from bzrlib.merge import merge from branch import find_branch, DivergedBranches, NoSuchRevision from shutil import rmtree try: br_from = find_branch(from_location) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location.rstrip("/\\")) try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) try: br_to.update_revisions(br_from, stop_revision=revision) except NoSuchRevision: rmtree(to_location) msg = "The branch %s has no revision %d." % (from_location, revision) raise BzrCommandError(msg) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False, ignore_zero=True) from_location = pull_loc(br_from) br_to.controlfile("x-pull", "wb").write(from_location + "\n") def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: raise BzrError("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: raise BzrError("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, exports to a directory (equivalent to --format=dir).""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format'] def run(self, dest, revision=None, format='dir'): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest, format) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit from bzrlib.osutils import get_text_message ## Warning: shadows builtin file() if not message and not file: import cStringIO stdout = sys.stdout catcher = cStringIO.StringIO() sys.stdout = catcher cmd_status({"file_list":selected_list}, {}) info = catcher.getvalue() sys.stdout = stdout message = get_text_message(info) if message is None: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_upgrade(Command): """Upgrade branch storage to current format. This should normally be used only after the check command tells you to run it. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.upgrade import upgrade upgrade(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest return int(not selftest()) class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Restore selected files from a previous revision. """ takes_args = ['file+'] def run(self, file_list): from bzrlib.branch import find_branch if not file_list: file_list = ['.'] b = find_branch(file_list[0]) b.revert([b.relpath(f) for f in file_list]) class cmd_merge_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) class cmd_plugins(Command): """List plugins""" hidden = True def run(self): import bzrlib.plugin from pprint import pprint pprint(bzrlib.plugin.all_plugins) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'update': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) >>> parse_args('log -r 500'.split()) (['log'], {'revision': 500}) >>> parse_args('log -r500:600'.split()) (['log'], {'revision': [500, 600]}) >>> parse_args('log -vr500:600'.split()) (['log'], {'verbose': True, 'revision': [500, 600]}) >>> parse_args('log -rv500:600'.split()) #the r takes an argument Traceback (most recent call last): ... ValueError: invalid literal for int(): v500 """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: raise BzrError('unknown long option %r' % a) else: shortopt = a[1:] if shortopt in SHORT_OPTIONS: # Multi-character options must have a space to delimit # their value optname = SHORT_OPTIONS[shortopt] else: # Single character short options, can be chained, # and have their value appended to their name shortopt = a[1:2] if shortopt not in SHORT_OPTIONS: # We didn't find the multi-character name, and we # didn't find the single char name raise BzrError('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if a[2:]: # There are extra things on this option # see if it is the value, or if it is another # short option optargfn = OPTIONS[optname] if optargfn is None: # This option does not take an argument, so the # next entry is another short option, pack it back # into the list argv.insert(0, '-' + a[2:]) else: # This option takes an argument, so pack it # into the array optarg = a[2:] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? raise BzrError('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: raise BzrError('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: raise BzrError('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def _parse_master_args(argv): """Parse the arguments that always go with the original command. These are things like bzr --no-plugins, etc. There are now 2 types of option flags. Ones that come *before* the command, and ones that come *after* the command. Ones coming *before* the command are applied against all possible commands. And are generally applied before plugins are loaded. The current list are: --builtin Allow plugins to load, but don't let them override builtin commands, they will still be allowed if they do not override a builtin. --no-plugins Don't load any plugins. This lets you get back to official source behavior. --profile Enable the hotspot profile before running the command. For backwards compatibility, this is also a non-master option. --version Spit out the version of bzr that is running and exit. This is also a non-master option. --help Run help and exit, also a non-master option (I think that should stay, though) >>> argv, opts = _parse_master_args(['bzr', '--test']) Traceback (most recent call last): ... BzrCommandError: Invalid master option: 'test' >>> argv, opts = _parse_master_args(['bzr', '--version', 'command']) >>> print argv ['command'] >>> print opts['version'] True >>> argv, opts = _parse_master_args(['bzr', '--profile', 'command', '--more-options']) >>> print argv ['command', '--more-options'] >>> print opts['profile'] True >>> argv, opts = _parse_master_args(['bzr', '--no-plugins', 'command']) >>> print argv ['command'] >>> print opts['no-plugins'] True >>> print opts['profile'] False >>> argv, opts = _parse_master_args(['bzr', 'command', '--profile']) >>> print argv ['command', '--profile'] >>> print opts['profile'] False """ master_opts = {'builtin':False, 'no-plugins':False, 'version':False, 'profile':False, 'help':False } # This is the point where we could hook into argv[0] to determine # what front-end is supposed to be run # For now, we are just ignoring it. cmd_name = argv.pop(0) for arg in argv[:]: if arg[:2] != '--': # at the first non-option, we return the rest break arg = arg[2:] # Remove '--' if arg not in master_opts: # We could say that this is not an error, that we should # just let it be handled by the main section instead raise BzrCommandError('Invalid master option: %r' % arg) argv.pop(0) # We are consuming this entry master_opts[arg] = True return argv, master_opts def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: # some options like --builtin and --no-plugins have special effects argv, master_opts = _parse_master_args(argv) if not master_opts['no-plugins']: bzrlib.load_plugins() args, opts = parse_args(argv) if master_opts['help']: from bzrlib.help import help if argv: help(argv[0]) else: help() return 0 if 'help' in opts: from bzrlib.help import help if args: help(args[0]) else: help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 plugins_override = not (master_opts['builtin']) canonical_cmd, cmd_class = get_cmd_class(cmd, plugins_override=plugins_override) profile = master_opts['profile'] # For backwards compatibility, I would rather stick with --profile being a # master/global option if 'profile' in opts: profile = True del opts['profile'] # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :787 committer Martin Pool 1119835524 +1000 data 27 - tweak rsync upload script from :786 M 644 inline contrib/upload-bzr.dev data 830 #! /bin/sh -ex # example of how to upload a bzr tree using rsync # --include-from is used to make sure that only versioned files and # control files are copied. We use includes/excludes rather than # --files-from so that we can delete any files from the destination # that are no longer present on the source. cd ~/work/bzr # note: don't use -a because that can mess up the permissions chmod a+rX `bzr inventory` bzr inventory | rsync -rltvv \ . \ escudero.ubuntu.com:/srv/www.bazaar-ng.org/rsync/bzr/bzr.dev/ \ --include .bzr \ --include '.bzr/**' \ --exclude-from .rsyncexclude \ --exclude-from .bzrignore \ --include-from - \ --exclude \* \ --exclude '.*' \ --delete-excluded --delete \ commit refs/heads/master mark :788 committer Martin Pool 1119835571 +1000 data 27 - tweak rsync upload script from :787 M 644 inline contrib/upload-bzr.dev data 829 #! /bin/sh -ex # example of how to upload a bzr tree using rsync # --include-from is used to make sure that only versioned files and # control files are copied. We use includes/excludes rather than # --files-from so that we can delete any files from the destination # that are no longer present on the source. cd ~/work/bzr # note: don't use -a because that can mess up the permissions chmod a+rX `bzr inventory` bzr inventory | rsync -rltv \ . \ escudero.ubuntu.com:/srv/www.bazaar-ng.org/rsync/bzr/bzr.dev/ \ --include .bzr \ --include '.bzr/**' \ --exclude-from .rsyncexclude \ --exclude-from .bzrignore \ --include-from - \ --exclude \* \ --exclude '.*' \ --delete-excluded --delete \ commit refs/heads/master mark :789 committer Martin Pool 1119835622 +1000 data 64 - patch from john to cope with branches with missing x-pull file from :788 M 644 inline bzrlib/commands.py data 52278 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date plugin_cmds = {} def register_command(cmd): "Utility function to help register a command" global plugin_cmds k = cmd.__name__ if k.startswith("cmd_"): k_unsquished = _unsquish_command_name(k) else: k_unsquished = k if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = cmd else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r' % sys.modules[cmd.__module__]) def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _get_cmd_dict(plugins_override=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v # If we didn't load plugins, the plugin_cmds dict will be empty if plugins_override: d.update(plugin_cmds) else: d2 = plugin_cmds.copy() d2.update(d) d = d2 return d def get_all_cmds(plugins_override=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(plugins_override=plugins_override).iteritems(): yield k,v def get_cmd_class(cmd, plugins_override=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(plugins_override=plugins_override) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() for opt in self.takes_options: if not opt in OPTIONS: raise BzrError("Unknown option '%s' returned by external command %s" % (opt, path)) # TODO: Is there any way to check takes_args is valid here? self.takes_args = pipe.readline().split() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-usage'" % path) pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-help'" % path) def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: optname = name.replace('_','-') value = kargs[name] if OPTIONS.has_key(optname): # it's an option opts.append('--%s' % optname) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_mkdir(Command): """Create a new versioned directory. This is equivalent to creating the directory and then adding it. """ takes_args = ['dir+'] def run(self, dir_list): import os import bzrlib.branch b = None for d in dir_list: os.mkdir(d) if not b: b = bzrlib.branch.Branch(d) b.add([d], verbose=True) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import errno br_to = Branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if e.errno != errno.ENOENT: raise if location is None: if stored_loc is None: raise BzrCommandError("No pull location known or specified.") else: print "Using last location: %s" % stored_loc location = stored_loc from branch import find_branch, DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged. Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. To retrieve the branch as of a particular revision, supply the --revision parameter, as in "branch foo/bar -r 5". """ takes_args = ['from_location', 'to_location?'] takes_options = ['revision'] def run(self, from_location, to_location=None, revision=None): import errno from bzrlib.merge import merge from branch import find_branch, DivergedBranches, NoSuchRevision from shutil import rmtree try: br_from = find_branch(from_location) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location.rstrip("/\\")) try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) try: br_to.update_revisions(br_from, stop_revision=revision) except NoSuchRevision: rmtree(to_location) msg = "The branch %s has no revision %d." % (from_location, revision) raise BzrCommandError(msg) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False, ignore_zero=True) from_location = pull_loc(br_from) br_to.controlfile("x-pull", "wb").write(from_location + "\n") def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: raise BzrError("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: raise BzrError("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, exports to a directory (equivalent to --format=dir).""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format'] def run(self, dest, revision=None, format='dir'): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest, format) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit from bzrlib.osutils import get_text_message ## Warning: shadows builtin file() if not message and not file: import cStringIO stdout = sys.stdout catcher = cStringIO.StringIO() sys.stdout = catcher cmd_status({"file_list":selected_list}, {}) info = catcher.getvalue() sys.stdout = stdout message = get_text_message(info) if message is None: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_upgrade(Command): """Upgrade branch storage to current format. This should normally be used only after the check command tells you to run it. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.upgrade import upgrade upgrade(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest return int(not selftest()) class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Restore selected files from a previous revision. """ takes_args = ['file+'] def run(self, file_list): from bzrlib.branch import find_branch if not file_list: file_list = ['.'] b = find_branch(file_list[0]) b.revert([b.relpath(f) for f in file_list]) class cmd_merge_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) class cmd_plugins(Command): """List plugins""" hidden = True def run(self): import bzrlib.plugin from pprint import pprint pprint(bzrlib.plugin.all_plugins) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'update': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) >>> parse_args('log -r 500'.split()) (['log'], {'revision': 500}) >>> parse_args('log -r500:600'.split()) (['log'], {'revision': [500, 600]}) >>> parse_args('log -vr500:600'.split()) (['log'], {'verbose': True, 'revision': [500, 600]}) >>> parse_args('log -rv500:600'.split()) #the r takes an argument Traceback (most recent call last): ... ValueError: invalid literal for int(): v500 """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: raise BzrError('unknown long option %r' % a) else: shortopt = a[1:] if shortopt in SHORT_OPTIONS: # Multi-character options must have a space to delimit # their value optname = SHORT_OPTIONS[shortopt] else: # Single character short options, can be chained, # and have their value appended to their name shortopt = a[1:2] if shortopt not in SHORT_OPTIONS: # We didn't find the multi-character name, and we # didn't find the single char name raise BzrError('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if a[2:]: # There are extra things on this option # see if it is the value, or if it is another # short option optargfn = OPTIONS[optname] if optargfn is None: # This option does not take an argument, so the # next entry is another short option, pack it back # into the list argv.insert(0, '-' + a[2:]) else: # This option takes an argument, so pack it # into the array optarg = a[2:] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? raise BzrError('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: raise BzrError('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: raise BzrError('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def _parse_master_args(argv): """Parse the arguments that always go with the original command. These are things like bzr --no-plugins, etc. There are now 2 types of option flags. Ones that come *before* the command, and ones that come *after* the command. Ones coming *before* the command are applied against all possible commands. And are generally applied before plugins are loaded. The current list are: --builtin Allow plugins to load, but don't let them override builtin commands, they will still be allowed if they do not override a builtin. --no-plugins Don't load any plugins. This lets you get back to official source behavior. --profile Enable the hotspot profile before running the command. For backwards compatibility, this is also a non-master option. --version Spit out the version of bzr that is running and exit. This is also a non-master option. --help Run help and exit, also a non-master option (I think that should stay, though) >>> argv, opts = _parse_master_args(['bzr', '--test']) Traceback (most recent call last): ... BzrCommandError: Invalid master option: 'test' >>> argv, opts = _parse_master_args(['bzr', '--version', 'command']) >>> print argv ['command'] >>> print opts['version'] True >>> argv, opts = _parse_master_args(['bzr', '--profile', 'command', '--more-options']) >>> print argv ['command', '--more-options'] >>> print opts['profile'] True >>> argv, opts = _parse_master_args(['bzr', '--no-plugins', 'command']) >>> print argv ['command'] >>> print opts['no-plugins'] True >>> print opts['profile'] False >>> argv, opts = _parse_master_args(['bzr', 'command', '--profile']) >>> print argv ['command', '--profile'] >>> print opts['profile'] False """ master_opts = {'builtin':False, 'no-plugins':False, 'version':False, 'profile':False, 'help':False } # This is the point where we could hook into argv[0] to determine # what front-end is supposed to be run # For now, we are just ignoring it. cmd_name = argv.pop(0) for arg in argv[:]: if arg[:2] != '--': # at the first non-option, we return the rest break arg = arg[2:] # Remove '--' if arg not in master_opts: # We could say that this is not an error, that we should # just let it be handled by the main section instead raise BzrCommandError('Invalid master option: %r' % arg) argv.pop(0) # We are consuming this entry master_opts[arg] = True return argv, master_opts def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: # some options like --builtin and --no-plugins have special effects argv, master_opts = _parse_master_args(argv) if not master_opts['no-plugins']: bzrlib.load_plugins() args, opts = parse_args(argv) if master_opts['help']: from bzrlib.help import help if argv: help(argv[0]) else: help() return 0 if 'help' in opts: from bzrlib.help import help if args: help(args[0]) else: help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 plugins_override = not (master_opts['builtin']) canonical_cmd, cmd_class = get_cmd_class(cmd, plugins_override=plugins_override) profile = master_opts['profile'] # For backwards compatibility, I would rather stick with --profile being a # master/global option if 'profile' in opts: profile = True del opts['profile'] # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :790 committer Martin Pool 1119836182 +1000 data 1377 Merge from aaron: ------------------------------------------------------------ revno: 763 committer: Aaron Bentley timestamp: Thu 2005-06-23 17:30:28 -0400 message: Copy files in immutable stores directly. ------------------------------------------------------------ revno: 762 committer: Aaron Bentley timestamp: Thu 2005-06-23 16:12:33 -0400 message: Fixed direct call of get_url in RemoteBranch.get_revision ------------------------------------------------------------ revno: 761 committer: Aaron Bentley timestamp: Thu 2005-06-23 12:00:31 -0400 message: Added prefetch support to update_revisions ------------------------------------------------------------ revno: 760 committer: Aaron Bentley timestamp: Thu 2005-06-23 11:57:54 -0400 message: Added cache support to branch and pull ------------------------------------------------------------ revno: 759 committer: Aaron Bentley timestamp: Thu 2005-06-23 11:21:37 -0400 message: Added find_cached_branch to branch ------------------------------------------------------------ revno: 758 committer: Aaron Bentley timestamp: Thu 2005-06-23 11:17:10 -0400 message: Added CachedStore type to reduce remote downloads from :789 M 644 inline bzrlib/meta_store.py data 1672 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from trace import mutter from bzrlib.store import ImmutableStore class CachedStore: """A store that caches data locally, to avoid repeated downloads. The precacache method should be used to avoid server round-trips for every piece of data. """ def __init__(self, store, cache_dir): self.source_store = store self.cache_store = ImmutableStore(cache_dir) def __getitem__(self, id): mutter("Cache add %s" % id) if id not in self.cache_store: self.cache_store.add(self.source_store[id], id) return self.cache_store[id] def prefetch(self, ids): """Copy a series of ids into the cache, before they are used. For remote stores that support pipelining or async downloads, this can increase speed considerably. """ mutter("Prefetch of ids %s" % ",".join(ids)) self.cache_store.copy_multi(self.source_store, ids) M 644 inline bzrlib/branch.py data 38207 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile import traceback, socket, fnmatch, difflib, time from binascii import hexlify import bzrlib from inventory import Inventory from trace import mutter, note from tree import Tree, EmptyTree, RevisionTree from inventory import InventoryEntry, Inventory from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \ format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \ joinpath, sha_file, sha_string, file_kind, local_time_offset, appendpath from store import ImmutableStore from revision import Revision from errors import BzrError from textui import show_status BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def find_cached_branch(f, cache_root, **args): from remotebranch import RemoteBranch br = find_branch(f, **args) def cacheify(br, store_name): from meta_store import CachedStore cache_path = os.path.join(cache_root, store_name) os.mkdir(cache_path) new_store = CachedStore(getattr(br, store_name), cache_path) setattr(br, store_name, new_store) if isinstance(br, RemoteBranch): cacheify(br, 'inventory_store') cacheify(br, 'text_store') cacheify(br, 'revision_store') return br def _relpath(base, path): """Return path relative to base, or raise exception. The path may be either an absolute path or a path relative to the current working directory. Lifted out of Branch.relpath for ease of testing. os.path.commonprefix (python2.4) has a bad bug that it works just on string prefixes, assuming that '/u' is a prefix of '/u2'. This avoids that problem.""" rp = os.path.abspath(path) s = [] head = rp while len(head) >= len(base): if head == base: break head, tail = os.path.split(head) if tail: s.insert(0, tail) else: from errors import NotBranchError raise NotBranchError("path %r is not within branch %r" % (rp, base)) return os.sep.join(s) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head class DivergedBranches(Exception): def __init__(self, branch1, branch2): self.branch1 = branch1 self.branch2 = branch2 Exception.__init__(self, "These branches have diverged.") class NoSuchRevision(BzrError): def __init__(self, branch, revision): self.branch = branch self.revision = revision msg = "Branch %s has no revision %d" % (branch, revision) BzrError.__init__(self, msg) ###################################################################### # branch objects class Branch(object): """Branch holding a history of revisions. base Base directory of the branch. _lock_mode None, or 'r' or 'w' _lock_count If _lock_mode is true, a positive count of the number of times the lock has been taken. _lock Lock object from bzrlib.lock. """ base = None _lock_mode = None _lock_count = None _lock = None def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): from errors import NotBranchError raise NotBranchError("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def __del__(self): if self._lock_mode or self._lock: from warnings import warn warn("branch %r was not explicitly unlocked" % self) self._lock.unlock() def lock_write(self): if self._lock_mode: if self._lock_mode != 'w': from errors import LockError raise LockError("can't upgrade to a write lock from %r" % self._lock_mode) self._lock_count += 1 else: from bzrlib.lock import WriteLock self._lock = WriteLock(self.controlfilename('branch-lock')) self._lock_mode = 'w' self._lock_count = 1 def lock_read(self): if self._lock_mode: assert self._lock_mode in ('r', 'w'), \ "invalid lock mode %r" % self._lock_mode self._lock_count += 1 else: from bzrlib.lock import ReadLock self._lock = ReadLock(self.controlfilename('branch-lock')) self._lock_mode = 'r' self._lock_count = 1 def unlock(self): if not self._lock_mode: from errors import LockError raise LockError('branch %r is not locked' % (self)) if self._lock_count > 1: self._lock_count -= 1 else: self._lock.unlock() self._lock = None self._lock_mode = self._lock_count = None def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" return _relpath(self.base, path) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, types.StringTypes): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.\n") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: raise BzrError('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" before = time.time() # ElementTree does its own conversion from UTF-8, so open in # binary. self.lock_read() try: inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time.time() - before)) return inv finally: self.unlock() def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self.lock_write() try: from bzrlib.atomicfile import AtomicFile f = AtomicFile(self.controlfilename('inventory'), 'wb') try: inv.write_xml(f) f.commit() finally: f.close() finally: self.unlock() mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. files List of paths to add, relative to the base of the tree. ids If set, use these instead of automatically generated ids. Must be the same length as the list of files, but may contain None for ids that are to be autogenerated. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, types.StringTypes): assert(ids is None or isinstance(ids, types.StringTypes)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) self.lock_write() try: inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): raise BzrError("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: raise BzrError("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: print 'added', quotefn(f) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) finally: self.unlock() def print_file(self, file, revno): """Print `file` to stdout.""" self.lock_read() try: tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: raise BzrError("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) finally: self.unlock() def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, types.StringTypes): files = [files] self.lock_write() try: tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: raise BzrError("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) finally: self.unlock() # FIXME: this doesn't need to be a branch method def set_inventory(self, new_inventory_list): inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): from bzrlib.atomicfile import AtomicFile mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() + [revision_id] f = AtomicFile(self.controlfilename('revision-history')) try: for rev_id in rev_history: print >>f, rev_id f.commit() finally: f.close() def get_revision(self, revision_id): """Return the Revision object for a named revision""" if not revision_id or not isinstance(revision_id, basestring): raise ValueError('invalid revision-id: %r' % revision_id) r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_revision_sha1(self, revision_id): """Hash the stored value of a revision, and return it.""" # In the future, revision entries will be signed. At that # point, it is probably best *not* to include the signature # in the revision hash. Because that lets you re-sign # the revision, (add signatures/remove signatures) and still # have all hash pointers stay consistent. # But for now, just hash the contents. return sha_file(self.revision_store[revision_id]) def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_inventory_sha1(self, inventory_id): """Return the sha1 hash of the inventory entry """ return sha_file(self.inventory_store[inventory_id]) def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self.lock_read() try: return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] finally: self.unlock() def common_ancestor(self, other, self_revno=None, other_revno=None): """ >>> import commit >>> sb = ScratchBranch(files=['foo', 'foo~']) >>> sb.common_ancestor(sb) == (None, None) True >>> commit.commit(sb, "Committing first revision", verbose=False) >>> sb.common_ancestor(sb)[0] 1 >>> clone = sb.clone() >>> commit.commit(sb, "Committing second revision", verbose=False) >>> sb.common_ancestor(sb)[0] 2 >>> sb.common_ancestor(clone)[0] 1 >>> commit.commit(clone, "Committing divergent second revision", ... verbose=False) >>> sb.common_ancestor(clone)[0] 1 >>> sb.common_ancestor(clone) == clone.common_ancestor(sb) True >>> sb.common_ancestor(sb) != clone.common_ancestor(clone) True >>> clone2 = sb.clone() >>> sb.common_ancestor(clone2)[0] 2 >>> sb.common_ancestor(clone2, self_revno=1)[0] 1 >>> sb.common_ancestor(clone2, other_revno=1)[0] 1 """ my_history = self.revision_history() other_history = other.revision_history() if self_revno is None: self_revno = len(my_history) if other_revno is None: other_revno = len(other_history) indices = range(min((self_revno, other_revno))) indices.reverse() for r in indices: if my_history[r] == other_history[r]: return r+1, my_history[r] return None, None def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def missing_revisions(self, other, stop_revision=None): """ If self and other have not diverged, return a list of the revisions present in other, but missing from self. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch() >>> br2 = ScratchBranch() >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [u'REVISION-ID-1'] >>> br2.missing_revisions(br1) [] >>> commit(br1, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-2A") >>> br1.missing_revisions(br2) [u'REVISION-ID-2A'] >>> commit(br1, "lala!", rev_id="REVISION-ID-2B") >>> br1.missing_revisions(br2) Traceback (most recent call last): DivergedBranches: These branches have diverged. """ self_history = self.revision_history() self_len = len(self_history) other_history = other.revision_history() other_len = len(other_history) common_index = min(self_len, other_len) -1 if common_index >= 0 and \ self_history[common_index] != other_history[common_index]: raise DivergedBranches(self, other) if stop_revision is None: stop_revision = other_len elif stop_revision > other_len: raise NoSuchRevision(self, stop_revision) return other_history[self_len:stop_revision] def update_revisions(self, other, stop_revision=None): """Pull in all new revisions from other branch. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch(files=['foo', 'bar']) >>> br1.add('foo') >>> br1.add('bar') >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False) >>> br2 = ScratchBranch() >>> br2.update_revisions(br1) Added 2 texts. Added 1 inventories. Added 1 revisions. >>> br2.revision_history() [u'REVISION-ID-1'] >>> br2.update_revisions(br1) Added 0 texts. Added 0 inventories. Added 0 revisions. >>> br1.text_store.total_size() == br2.text_store.total_size() True """ from bzrlib.progress import ProgressBar pb = ProgressBar() pb.update('comparing histories') revision_ids = self.missing_revisions(other, stop_revision) if hasattr(other.revision_store, "prefetch"): other.revision_store.prefetch(revision_ids) if hasattr(other.inventory_store, "prefetch"): inventory_ids = [other.get_revision(r).inventory_id for r in revision_ids] other.inventory_store.prefetch(inventory_ids) revisions = [] needed_texts = sets.Set() i = 0 for rev_id in revision_ids: i += 1 pb.update('fetching revision', i, len(revision_ids)) rev = other.get_revision(rev_id) revisions.append(rev) inv = other.get_inventory(str(rev.inventory_id)) for key, entry in inv.iter_entries(): if entry.text_id is None: continue if entry.text_id not in self.text_store: needed_texts.add(entry.text_id) pb.clear() count = self.text_store.copy_multi(other.text_store, needed_texts) print "Added %d texts." % count inventory_ids = [ f.inventory_id for f in revisions ] count = self.inventory_store.copy_multi(other.inventory_store, inventory_ids) print "Added %d inventories." % count revision_ids = [ f.revision_id for f in revisions] count = self.revision_store.copy_multi(other.revision_store, revision_ids) for revision_id in revision_ids: self.append_revision(revision_id) print "Added %d revisions." % count def commit(self, *args, **kw): from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self.lock_write() try: tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): raise BzrError("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): raise BzrError("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: raise BzrError("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): raise BzrError("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': raise BzrError("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self.lock_write() try: ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): raise BzrError("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): raise BzrError("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': raise BzrError("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): raise BzrError("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): raise BzrError("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: raise BzrError("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): raise BzrError("destination %r already exists" % dest_path) if f_id in to_idpath: raise BzrError("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() def revert(self, filenames, old_tree=None, backups=True): """Restore selected files to the versions from a previous tree. backups If true (default) backups are made of files before they're renamed. """ from bzrlib.errors import NotVersionedError, BzrError from bzrlib.atomicfile import AtomicFile from bzrlib.osutils import backup_file inv = self.read_working_inventory() if old_tree is None: old_tree = self.basis_tree() old_inv = old_tree.inventory nids = [] for fn in filenames: file_id = inv.path2id(fn) if not file_id: raise NotVersionedError("not a versioned file", fn) if not old_inv.has_id(file_id): raise BzrError("file not present in old tree", fn, file_id) nids.append((fn, file_id)) # TODO: Rename back if it was previously at a different location # TODO: If given a directory, restore the entire contents from # the previous version. # TODO: Make a backup to a temporary file. # TODO: If the file previously didn't exist, delete it? for fn, file_id in nids: backup_file(fn) f = AtomicFile(fn, 'wb') try: f.write(old_tree.get_file(file_id).read()) f.commit() finally: f.close() class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[], base=None): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ init = False if base is None: base = tempfile.mkdtemp() init = True Branch.__init__(self, base, init=init) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def clone(self): """ >>> orig = ScratchBranch(files=["file1", "file2"]) >>> clone = orig.clone() >>> os.path.samefile(orig.base, clone.base) False >>> os.path.isfile(os.path.join(clone.base, "file1")) True """ base = tempfile.mkdtemp() os.rmdir(base) shutil.copytree(self.base, base, symlinks=True) return ScratchBranch(base=base) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" try: if self.base: mutter("delete ScratchBranch %s" % self.base) shutil.rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) shutil.rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time.time()), s)) M 644 inline bzrlib/commands.py data 52866 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date plugin_cmds = {} def register_command(cmd): "Utility function to help register a command" global plugin_cmds k = cmd.__name__ if k.startswith("cmd_"): k_unsquished = _unsquish_command_name(k) else: k_unsquished = k if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = cmd else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r' % sys.modules[cmd.__module__]) def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _get_cmd_dict(plugins_override=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v # If we didn't load plugins, the plugin_cmds dict will be empty if plugins_override: d.update(plugin_cmds) else: d2 = plugin_cmds.copy() d2.update(d) d = d2 return d def get_all_cmds(plugins_override=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(plugins_override=plugins_override).iteritems(): yield k,v def get_cmd_class(cmd, plugins_override=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(plugins_override=plugins_override) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() for opt in self.takes_options: if not opt in OPTIONS: raise BzrError("Unknown option '%s' returned by external command %s" % (opt, path)) # TODO: Is there any way to check takes_args is valid here? self.takes_args = pipe.readline().split() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-usage'" % path) pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-help'" % path) def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: optname = name.replace('_','-') value = kargs[name] if OPTIONS.has_key(optname): # it's an option opts.append('--%s' % optname) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_mkdir(Command): """Create a new versioned directory. This is equivalent to creating the directory and then adding it. """ takes_args = ['dir+'] def run(self, dir_list): import os import bzrlib.branch b = None for d in dir_list: os.mkdir(d) if not b: b = bzrlib.branch.Branch(d) b.add([d], verbose=True) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import tempfile from shutil import rmtree import errno br_to = Branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if e.errno != errno.ENOENT: raise if location is None: if stored_loc is None: raise BzrCommandError("No pull location known or specified.") else: print "Using last location: %s" % stored_loc location = stored_loc cache_root = tempfile.mkdtemp() try: from branch import find_cached_branch, DivergedBranches br_from = find_cached_branch(location, cache_root) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged." " Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") finally: rmtree(cache_root) class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. To retrieve the branch as of a particular revision, supply the --revision parameter, as in "branch foo/bar -r 5". """ takes_args = ['from_location', 'to_location?'] takes_options = ['revision'] def run(self, from_location, to_location=None, revision=None): import errno from bzrlib.merge import merge from branch import find_cached_branch, DivergedBranches, NoSuchRevision from shutil import rmtree from meta_store import CachedStore import tempfile cache_root = tempfile.mkdtemp() try: try: br_from = find_cached_branch(from_location, cache_root) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not' ' exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location.rstrip("/\\")) try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already' ' exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) try: br_to.update_revisions(br_from, stop_revision=revision) except NoSuchRevision: rmtree(to_location) msg = "The branch %s has no revision %d." % (from_location, revision) raise BzrCommandError(msg) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False, ignore_zero=True) from_location = pull_loc(br_from) br_to.controlfile("x-pull", "wb").write(from_location + "\n") finally: rmtree(cache_root) def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: raise BzrError("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: raise BzrError("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') show_log(b, file_id, show_timezone=timezone, verbose=verbose, show_ids=show_ids, to_file=outf, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, exports to a directory (equivalent to --format=dir).""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format'] def run(self, dest, revision=None, format='dir'): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest, format) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit from bzrlib.osutils import get_text_message ## Warning: shadows builtin file() if not message and not file: import cStringIO stdout = sys.stdout catcher = cStringIO.StringIO() sys.stdout = catcher cmd_status({"file_list":selected_list}, {}) info = catcher.getvalue() sys.stdout = stdout message = get_text_message(info) if message is None: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_upgrade(Command): """Upgrade branch storage to current format. This should normally be used only after the check command tells you to run it. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.upgrade import upgrade upgrade(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest return int(not selftest()) class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Restore selected files from a previous revision. """ takes_args = ['file+'] def run(self, file_list): from bzrlib.branch import find_branch if not file_list: file_list = ['.'] b = find_branch(file_list[0]) b.revert([b.relpath(f) for f in file_list]) class cmd_merge_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) class cmd_plugins(Command): """List plugins""" hidden = True def run(self): import bzrlib.plugin from pprint import pprint pprint(bzrlib.plugin.all_plugins) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'update': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) >>> parse_args('log -r 500'.split()) (['log'], {'revision': 500}) >>> parse_args('log -r500:600'.split()) (['log'], {'revision': [500, 600]}) >>> parse_args('log -vr500:600'.split()) (['log'], {'verbose': True, 'revision': [500, 600]}) >>> parse_args('log -rv500:600'.split()) #the r takes an argument Traceback (most recent call last): ... ValueError: invalid literal for int(): v500 """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: raise BzrError('unknown long option %r' % a) else: shortopt = a[1:] if shortopt in SHORT_OPTIONS: # Multi-character options must have a space to delimit # their value optname = SHORT_OPTIONS[shortopt] else: # Single character short options, can be chained, # and have their value appended to their name shortopt = a[1:2] if shortopt not in SHORT_OPTIONS: # We didn't find the multi-character name, and we # didn't find the single char name raise BzrError('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if a[2:]: # There are extra things on this option # see if it is the value, or if it is another # short option optargfn = OPTIONS[optname] if optargfn is None: # This option does not take an argument, so the # next entry is another short option, pack it back # into the list argv.insert(0, '-' + a[2:]) else: # This option takes an argument, so pack it # into the array optarg = a[2:] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? raise BzrError('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: raise BzrError('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: raise BzrError('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def _parse_master_args(argv): """Parse the arguments that always go with the original command. These are things like bzr --no-plugins, etc. There are now 2 types of option flags. Ones that come *before* the command, and ones that come *after* the command. Ones coming *before* the command are applied against all possible commands. And are generally applied before plugins are loaded. The current list are: --builtin Allow plugins to load, but don't let them override builtin commands, they will still be allowed if they do not override a builtin. --no-plugins Don't load any plugins. This lets you get back to official source behavior. --profile Enable the hotspot profile before running the command. For backwards compatibility, this is also a non-master option. --version Spit out the version of bzr that is running and exit. This is also a non-master option. --help Run help and exit, also a non-master option (I think that should stay, though) >>> argv, opts = _parse_master_args(['bzr', '--test']) Traceback (most recent call last): ... BzrCommandError: Invalid master option: 'test' >>> argv, opts = _parse_master_args(['bzr', '--version', 'command']) >>> print argv ['command'] >>> print opts['version'] True >>> argv, opts = _parse_master_args(['bzr', '--profile', 'command', '--more-options']) >>> print argv ['command', '--more-options'] >>> print opts['profile'] True >>> argv, opts = _parse_master_args(['bzr', '--no-plugins', 'command']) >>> print argv ['command'] >>> print opts['no-plugins'] True >>> print opts['profile'] False >>> argv, opts = _parse_master_args(['bzr', 'command', '--profile']) >>> print argv ['command', '--profile'] >>> print opts['profile'] False """ master_opts = {'builtin':False, 'no-plugins':False, 'version':False, 'profile':False, 'help':False } # This is the point where we could hook into argv[0] to determine # what front-end is supposed to be run # For now, we are just ignoring it. cmd_name = argv.pop(0) for arg in argv[:]: if arg[:2] != '--': # at the first non-option, we return the rest break arg = arg[2:] # Remove '--' if arg not in master_opts: # We could say that this is not an error, that we should # just let it be handled by the main section instead raise BzrCommandError('Invalid master option: %r' % arg) argv.pop(0) # We are consuming this entry master_opts[arg] = True return argv, master_opts def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: # some options like --builtin and --no-plugins have special effects argv, master_opts = _parse_master_args(argv) if not master_opts['no-plugins']: bzrlib.load_plugins() args, opts = parse_args(argv) if master_opts['help']: from bzrlib.help import help if argv: help(argv[0]) else: help() return 0 if 'help' in opts: from bzrlib.help import help if args: help(args[0]) else: help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 plugins_override = not (master_opts['builtin']) canonical_cmd, cmd_class = get_cmd_class(cmd, plugins_override=plugins_override) profile = master_opts['profile'] # For backwards compatibility, I would rather stick with --profile being a # master/global option if 'profile' in opts: profile = True del opts['profile'] # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/store.py data 6663 # Copyright (C) 2005 by Canonical Development Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ Stores are the main data-storage mechanism for Bazaar-NG. A store is a simple write-once container indexed by a universally unique ID. """ import os, tempfile, types, osutils, gzip, errno from stat import ST_SIZE from StringIO import StringIO from trace import mutter ###################################################################### # stores class StoreError(Exception): pass class ImmutableStore(object): """Store that holds files indexed by unique names. Files can be added, but not modified once they are in. Typically the hash is used as the name, or something else known to be unique, such as a UUID. >>> st = ImmutableScratchStore() >>> st.add(StringIO('hello'), 'aa') >>> 'aa' in st True >>> 'foo' in st False You are not allowed to add an id that is already present. Entries can be retrieved as files, which may then be read. >>> st.add(StringIO('goodbye'), '123123') >>> st['123123'].read() 'goodbye' TODO: Atomic add by writing to a temporary file and renaming. In bzr 0.0.5 and earlier, files within the store were marked readonly on disk. This is no longer done but existing stores need to be accomodated. """ def __init__(self, basedir): self._basedir = basedir def _path(self, id): if '\\' in id or '/' in id: raise ValueError("invalid store id %r" % id) return os.path.join(self._basedir, id) def __repr__(self): return "%s(%r)" % (self.__class__.__name__, self._basedir) def add(self, f, fileid, compressed=True): """Add contents of a file into the store. f -- An open file, or file-like object.""" # FIXME: Only works on files that will fit in memory from bzrlib.atomicfile import AtomicFile mutter("add store entry %r" % (fileid)) if isinstance(f, types.StringTypes): content = f else: content = f.read() p = self._path(fileid) if os.access(p, os.F_OK) or os.access(p + '.gz', os.F_OK): raise BzrError("store %r already contains id %r" % (self._basedir, fileid)) fn = p if compressed: fn = fn + '.gz' af = AtomicFile(fn, 'wb') try: if compressed: gf = gzip.GzipFile(mode='wb', fileobj=af) gf.write(content) gf.close() else: af.write(content) af.commit() finally: af.close() def copy_multi(self, other, ids): """Copy texts for ids from other into self. If an id is present in self, it is skipped. A count of copied ids is returned, which may be less than len(ids). """ from bzrlib.progress import ProgressBar pb = ProgressBar() pb.update('preparing to copy') to_copy = [id for id in ids if id not in self] if isinstance(other, ImmutableStore): return self.copy_multi_immutable(other, to_copy, pb) count = 0 for id in to_copy: count += 1 pb.update('copy', count, len(to_copy)) self.add(other[id], id) assert count == len(to_copy) pb.clear() return count def copy_multi_immutable(self, other, to_copy, pb): from shutil import copyfile count = 0 for id in to_copy: p = self._path(id) other_p = other._path(id) try: copyfile(other_p, p) except IOError, e: if e.errno == errno.ENOENT: copyfile(other_p+".gz", p+".gz") else: raise count += 1 pb.update('copy', count, len(to_copy)) assert count == len(to_copy) pb.clear() return count def __contains__(self, fileid): """""" p = self._path(fileid) return (os.access(p, os.R_OK) or os.access(p + '.gz', os.R_OK)) # TODO: Guard against the same thing being stored twice, compressed and uncompresse def __iter__(self): for f in os.listdir(self._basedir): if f[-3:] == '.gz': # TODO: case-insensitive? yield f[:-3] else: yield f def __len__(self): return len(os.listdir(self._basedir)) def __getitem__(self, fileid): """Returns a file reading from a particular entry.""" p = self._path(fileid) try: return gzip.GzipFile(p + '.gz', 'rb') except IOError, e: if e.errno == errno.ENOENT: return file(p, 'rb') else: raise e def total_size(self): """Return (count, bytes) This is the (compressed) size stored on disk, not the size of the content.""" total = 0 count = 0 for fid in self: count += 1 p = self._path(fid) try: total += os.stat(p)[ST_SIZE] except OSError: total += os.stat(p + '.gz')[ST_SIZE] return count, total class ImmutableScratchStore(ImmutableStore): """Self-destructing test subclass of ImmutableStore. The Store only exists for the lifetime of the Python object. Obviously you should not put anything precious in it. """ def __init__(self): ImmutableStore.__init__(self, tempfile.mkdtemp()) def __del__(self): for f in os.listdir(self._basedir): fpath = os.path.join(self._basedir, f) # needed on windows, and maybe some other filesystems os.chmod(fpath, 0600) os.remove(fpath) os.rmdir(self._basedir) mutter("%r destroyed" % self) commit refs/heads/master mark :791 committer Martin Pool 1119836366 +1000 data 31 - import effbot.org http client from :790 M 644 inline effbot/__init__.py data 55 # $Id: __init__.py 271 2004-10-09 10:50:59Z fredrik $ M 644 inline effbot/org/__init__.py data 55 # $Id: __init__.py 271 2004-10-09 10:50:59Z fredrik $ M 644 inline effbot/org/gzip_consumer.py data 1916 # $Id: gzip_consumer.py 271 2004-10-09 10:50:59Z fredrik $ # gzip consumer # # Copyright (c) 2001-2004 by Fredrik Lundh. All rights reserved. # ## # Consumer wrapper for GZIP streams. class GzipConsumer: def __init__(self, consumer): self.__consumer = consumer self.__decoder = None self.__data = "" def __getattr__(self, key): return getattr(self.__consumer, key) def feed(self, data): if self.__decoder is None: # check if we have a full gzip header data = self.__data + data try: i = 10 flag = ord(data[3]) if flag & 4: # extra x = ord(data[i]) + 256*ord(data[i+1]) i = i + 2 + x if flag & 8: # filename while ord(data[i]): i = i + 1 i = i + 1 if flag & 16: # comment while ord(data[i]): i = i + 1 i = i + 1 if flag & 2: # crc i = i + 2 if len(data) < i: raise IndexError("not enough data") if data[:3] != "\x1f\x8b\x08": raise IOError("invalid gzip data") data = data[i:] except IndexError: self.__data = data return # need more data import zlib self.__data = "" self.__decoder = zlib.decompressobj(-zlib.MAX_WBITS) data = self.__decoder.decompress(data) if data: self.__consumer.feed(data) def close(self): if self.__decoder: data = self.__decoder.flush() if data: self.__consumer.feed(data) self.__consumer.close() M 644 inline effbot/org/http_client.py data 8087 # $Id: http_client.py 271 2004-10-09 10:50:59Z fredrik $ # a simple asynchronous http client (based on SimpleAsyncHTTP.py from # "Python Standard Library" by Fredrik Lundh, O'Reilly 2001) # # HTTP/1.1 and GZIP support added in January 2003 by Fredrik Lundh. # # changes: # 2004-08-26 fl unified http callback # 2004-10-09 fl factored out gzip_consumer support # # Copyright (c) 2001-2004 by Fredrik Lundh. All rights reserved. # import asyncore import socket, string, time, sys import StringIO import mimetools, urlparse, urllib try: from gzip_consumer import GzipConsumer except ImportError: pass ## # Close connection. Request handlers can raise this exception to # indicate that the connection should be closed. class CloseConnection(Exception): pass ## # Redirect connection. Request handlers can raise this exception to # indicate that the a new request should be issued. class Redirect(CloseConnection): def __init__(self, location): self.location = location ## # Asynchronous HTTP/1.1 client. class async_http(asyncore.dispatcher_with_send): # asynchronous http client user_agent = "http_client.py 1.2 (http://effbot.org/zone)" http_version = "1.1" proxies = urllib.getproxies() def __init__(self, uri, consumer, extra_headers=None): asyncore.dispatcher_with_send.__init__(self) # turn the uri into a valid request scheme, host, path, params, query, fragment = urlparse.urlparse(uri) # use origin host self.host = host # get proxy settings, if any proxy = self.proxies.get(scheme) if proxy: scheme, host, x, x, x, x = urlparse.urlparse(proxy) assert scheme == "http", "only supports HTTP requests (%s)" % scheme if not path: path = "/" if params: path = path + ";" + params if query: path = path + "?" + query if proxy: path = scheme + "://" + self.host + path self.path = path # get port number try: host, port = host.split(":", 1) port = int(port) except (TypeError, ValueError): port = 80 # default port self.consumer = consumer self.status = None self.header = None self.bytes_in = 0 self.bytes_out = 0 self.content_type = None self.content_length = None self.content_encoding = None self.transfer_encoding = None self.data = "" self.chunk_size = None self.timestamp = time.time() self.extra_headers = extra_headers self.create_socket(socket.AF_INET, socket.SOCK_STREAM) try: self.connect((host, port)) except socket.error: self.consumer.http(0, self, sys.exc_info()) def handle_connect(self): # connection succeeded request = [ "GET %s HTTP/%s" % (self.path, self.http_version), "Host: %s" % self.host, ] if GzipConsumer: request.append("Accept-Encoding: gzip") if self.extra_headers: request.extend(self.extra_headers) # make sure to include a user agent for header in request: if string.lower(header).startswith("user-agent:"): break else: request.append("User-Agent: %s" % self.user_agent) request = string.join(request, "\r\n") + "\r\n\r\n" self.send(request) self.bytes_out = self.bytes_out + len(request) def handle_expt(self): # connection failed (windows); notify consumer if sys.platform == "win32": self.close() self.consumer.http(0, self) def handle_read(self): # handle incoming data data = self.recv(2048) self.data = self.data + data self.bytes_in = self.bytes_in + len(data) while self.data: if not self.header: # check if we've seen a full header header = self.data.split("\r\n\r\n", 1) if len(header) <= 1: return header, self.data = header # parse header fp = StringIO.StringIO(header) self.status = fp.readline().split(" ", 2) self.header = mimetools.Message(fp) # get http headers self.content_type = self.header.get("content-type") try: self.content_length = int( self.header.get("content-length") ) except (ValueError, TypeError): self.content_length = None self.transfer_encoding = self.header.get("transfer-encoding") self.content_encoding = self.header.get("content-encoding") if self.content_encoding == "gzip": # FIXME: report error if GzipConsumer is not available self.consumer = GzipConsumer(self.consumer) try: self.consumer.http(1, self) except Redirect, v: # redirect if v.location: do_request( v.location, self.consumer, self.extra_headers ) self.close() return except CloseConnection: self.close() return if self.transfer_encoding == "chunked" and self.chunk_size is None: # strip off leading whitespace if self.data.startswith("\r\n"): self.data = self.data[2:] chunk_size = self.data.split("\r\n", 1) if len(chunk_size) <= 1: return chunk_size, self.data = chunk_size try: self.chunk_size = int(chunk_size, 16) if self.chunk_size <= 0: raise ValueError except ValueError: return self.handle_close() if not self.data: return data = self.data self.data = "" chunk_size = self.chunk_size or len(data) if chunk_size < len(data): self.data = data[chunk_size:] data = data[:chunk_size] self.chunk_size = None else: self.chunk_size = chunk_size - len(data) if self.chunk_size <= 0: self.chunk_size = None if data: self.consumer.feed(data) if self.content_length: self.content_length -= chunk_size if self.content_length <= 0: return self.handle_close() def handle_close(self): self.consumer.close() self.close() def handle_error(self): self.consumer.http(0, self, sys.exc_info()) self.close() def do_request(uri, consumer, extra_headers=None): return async_http(uri, consumer, extra_headers) if __name__ == "__main__": class dummy_consumer: def feed(self, data): # print "feed", repr(data) print "feed", repr(data[:20]), repr(data[-20:]), len(data) def close(self): print "close" def http(self, ok, connection, **args): print ok, connection, args print "status", connection.status print "header", connection.header try: url = sys.argv[1] except IndexError: url = "http://www.cnn.com/" do_request(url, dummy_consumer()) asyncore.loop() M 644 inline effbot/org/http_manager.py data 2230 # $Id: http_manager.py 270 2004-10-09 10:38:54Z fredrik $ # effnews http # # manage a set of http clients # # Copyright (c) 2001-2004 by Fredrik Lundh. All rights reserved. # import asyncore, time import http_client class http_manager: max_connections = 8 max_size = 1000000 max_time = 60 def __init__(self): self.queue = [] def request(self, uri, consumer, extra_headers=None): self.queue.append((uri, consumer, extra_headers)) def priority_request(self, uri, consumer, extra_headers=None): self.queue.insert(0, (uri, consumer, extra_headers)) def purge(self): for channel in asyncore.socket_map.values(): channel.close() del self.queue[:] def prioritize(self, priority_uri): i = 0 for uri, consumer, extra_headers in self.queue: if uri == priority_uri: del self.queue[i] self.priority_request(uri, consumer, extra_headers) return i = i + 1 def poll(self, timeout=0.1): # sanity checks now = time.time() for channel in asyncore.socket_map.values(): if channel.bytes_in > self.max_size: channel.close() # too much data try: channel.consumer.http( 0, channel, ("HTTPManager", "too much data", None) ) except: pass if channel.timestamp and now - channel.timestamp > self.max_time: channel.close() # too slow try: channel.consumer.http( 0, channel, ("HTTPManager", "timeout", None) ) except: pass # activate up to max_connections channels while self.queue and len(asyncore.socket_map) < self.max_connections: http_client.do_request(*self.queue.pop(0)) # keep the network running asyncore.poll(timeout) # return non-zero if we should keep on polling return len(self.queue) or len(asyncore.socket_map) commit refs/heads/master mark :792 committer Martin Pool 1119848607 +1000 data 50 - rsync upload/download plugins from John A Meinel from :791 M 644 inline plugins/rsync/__init__.py data 3033 #!/usr/bin/env python """\ This is a plugin for the Bazaar-NG revision control system. """ import os import bzrlib, bzrlib.commands class cmd_rsync_pull(bzrlib.commands.Command): """Update the current working tree using rsync. With no arguments, look for a .bzr/x-rsync-location file to determine which remote system to rsync from. Otherwise, you can specify a new location to rsync from. Normally the first time you use it, you would write: bzr rsync-pull . path/to/otherdirectory """ takes_args = ['local?', 'remote?'] takes_options = ['verbose'] aliases = ['rpull'] def run(self, local=None, remote=None, verbose=True): from rsync_update import get_branch_remote_update, \ check_should_pull, set_default_remote_info, pull b, remote, last_revno, last_revision = \ get_branch_remote_update(local=local, remote=remote) if not check_should_pull(b, last_revno, last_revision): return 1 b = pull(b, remote, verbose=verbose) set_default_remote_info(b, remote) class cmd_rsync_pull_bzr(cmd_rsync_pull): takes_args = ['remote?'] def run(self, remote=None, verbose=True): from rsync_update import get_branch_remote_update, \ check_should_pull, set_default_remote_info, pull bzr_path = os.path.dirname(bzrlib.__path__[0]) b, remote, last_revno, last_revision = \ get_branch_remote_update(local=bzr_path, remote=remote , alt_remote='bazaar-ng.org::bazaar-ng/bzr/bzr.dev/') if not check_should_pull(b, last_revno, last_revision): return 1 b = pull(b, remote, verbose=verbose) set_default_remote_info(b, remote) class cmd_rsync_push(bzrlib.commands.Command): """Update the remote tree using rsync. With no arguments, look for a .bzr/x-rsync-location file to determine which remote system to rsync to. Otherwise, you can specify a new location to rsync to. """ takes_args = ['local?', 'remote?'] takes_options = ['verbose'] aliases = ['rpush'] def run(self, local=None, remote=None, verbose=True): from rsync_update import get_branch_remote_update, \ check_should_push, set_default_remote_info, push b, remote, last_revno, last_revision = \ get_branch_remote_update(local=local, remote=remote) if not check_should_push(b, last_revno, last_revision): return 1 push(b, remote, verbose=verbose) set_default_remote_info(b, remote) if hasattr(bzrlib.commands, 'register_plugin_command'): bzrlib.commands.register_plugin_command(cmd_rsync_pull) bzrlib.commands.register_plugin_command(cmd_rsync_pull_bzr) bzrlib.commands.register_plugin_command(cmd_rsync_push) elif hasattr(bzrlib.commands, 'register_command'): bzrlib.commands.register_command(cmd_rsync_pull) bzrlib.commands.register_command(cmd_rsync_pull_bzr) bzrlib.commands.register_command(cmd_rsync_push) M 644 inline plugins/rsync/rsync_update.py data 11700 #!/usr/bin/env python """\ This encapsulates the functionality for trying to rsync a local working tree to/from a remote rsync accessible location. """ import os import bzrlib _rsync_location = 'x-rsync-data' _parent_locations = ['parent', 'pull', 'x-pull'] def temp_branch(): import tempfile dirname = tempfile.mkdtemp("temp-branch") return bzrlib.Branch(dirname, init=True) def rm_branch(branch): import shutil shutil.rmtree(branch.base) def is_clean(branch): """ Return true if no files are modifed or unknown >>> br = temp_branch() >>> is_clean(br) True >>> fooname = os.path.join(br.base, "foo") >>> file(fooname, "wb").write("bar") >>> is_clean(br) False >>> bzrlib.add.smart_add([fooname]) >>> is_clean(br) False >>> br.commit("added file") >>> is_clean(br) True >>> rm_branch(br) """ old_tree = branch.basis_tree() new_tree = branch.working_tree() for path, file_class, kind, file_id in new_tree.list_files(): if file_class == '?': return False delta = bzrlib.compare_trees(old_tree, new_tree, want_unchanged=False) if len(delta.added) > 0 or len(delta.removed) > 0 or \ len(delta.modified) > 0: return False return True def get_default_remote_info(branch): """Return the value stored in .bzr/x-rsync-location if it exists. >>> br = temp_branch() >>> get_default_remote_info(br) (None, 0, None) >>> import bzrlib.commit >>> bzrlib.commit.commit(br, 'test commit', rev_id='test-id-12345') >>> set_default_remote_info(br, 'http://somewhere') >>> get_default_remote_info(br) ('http://somewhere', 1, 'test-id-12345') """ def_remote = None revno = 0 revision = None def_remote_filename = branch.controlfilename(_rsync_location) if os.path.isfile(def_remote_filename): [def_remote,revno, revision] = [x.strip() for x in open(def_remote_filename).readlines()] return def_remote, int(revno), revision def set_default_remote_info(branch, location): """Store the location into the .bzr/x-rsync-location. """ from bzrlib.atomicfile import AtomicFile remote, revno, revision = get_default_remote_info(branch) if (remote == location and revno == branch.revno() and revision == branch.last_patch()): return #Nothing would change, so skip it # TODO: Consider adding to x-pull so that we can try a RemoteBranch # for checking the need to update f = AtomicFile(branch.controlfilename(_rsync_location)) f.write(location) f.write('\n') f.write(str(branch.revno())) f.write('\n') f.write(branch.last_patch()) f.write('\n') f.commit() def get_parent_branch(branch): """Try to get the pull location, in case this directory supports the normal bzr pull. The idea is that we can use RemoteBranch to see if we actually need to do anything, and then we can decide whether to run rsync or not. """ import errno stored_loc = None for fname in _parent_locations: try: stored_loc = branch.controlfile(fname, 'rb').read().rstrip('\n') except IOError, e: if e.errno != errno.ENOENT: raise if stored_loc: break if stored_loc: from bzrlib.branch import find_branch return find_branch(stored_loc) return None def get_branch_remote_update(local=None, remote=None, alt_remote=None): from bzrlib.errors import BzrCommandError from bzrlib.branch import find_branch if local is None: local = '.' if remote is not None and remote[-1:] != '/': remote += '/' if alt_remote is not None and alt_remote[-1:] != '/': alt_remote += '/' if not os.path.exists(local): if remote is None: remote = alt_remote if remote is None: raise BzrCommandError('No remote location specified while creating a new local location') return local, remote, 0, None b = find_branch(local) def_remote, last_revno, last_revision = get_default_remote_info(b) if remote is None: if def_remote is None: if alt_remote is None: raise BzrCommandError('No remote location specified, and no default exists.') else: remote = alt_remote else: remote = def_remote if remote[-1:] != '/': remote += '/' return b, remote, last_revno, last_revision def check_should_pull(branch, last_revno, last_revision): if isinstance(branch, basestring): # We don't even have a local branch yet return True if not is_clean(branch): print '** Local tree is not clean. Either has unknown or modified files.' return False b_parent = get_parent_branch(branch) if b_parent is not None: from bzrlib.branch import DivergedBranches # This may throw a Diverged branches. try: missing_revisions = branch.missing_revisions(b_parent) except DivergedBranches: print '** Local tree history has diverged from remote.' print '** Not allowing you to overwrite local changes.' return False if len(missing_revisions) == 0: # There is nothing to do, the remote branch has no changes missing_revisions = b_parent.missing_revisions(branch) if len(missing_revisions) > 0: print '** Local tree is up-to-date with remote.' print '** But remote tree is missing local revisions.' print '** Consider using bzr rsync-push' else: print '** Both trees fully up-to-date.' return False # We are sure that we are missing remote revisions return True if last_revno == branch.revno() and last_revision == branch.last_patch(): # We can go ahead and try return True print 'Local working directory has a different revision than last rsync.' val = raw_input('Are you sure you want to download [y/N]? ') if val.lower() in ('y', 'yes'): return True return False def check_should_push(branch, last_revno, last_revision): if not is_clean(branch): print '** Local tree is not clean (either modified or unknown files)' return False b_parent = get_parent_branch(branch) if b_parent is not None: from bzrlib.branch import DivergedBranches # This may throw a Diverged branches. try: missing_revisions = b_parent.missing_revisions(branch) except DivergedBranches: print '** Local tree history has diverged from remote.' print '** Not allowing you to overwrite remote changes.' return False if len(missing_revisions) == 0: # There is nothing to do, the remote branch is up to date missing_revisions = branch.missing_revisions(b_parent) if len(missing_revisions) > 0: print '** Remote tree is up-to-date with local.' print '** But local tree is missing remote revisions.' print '** Consider using bzr rsync-pull' else: print '** Both trees fully up-to-date.' return False # We are sure that we are missing remote revisions return True if last_revno is None and last_revision is None: print 'Local tree does not have a valid last rsync revision.' val = raw_input('push anyway [y/N]? ') if val.lower() in ('y', 'yes'): return True return False if last_revno == branch.revno() and last_revision == branch.last_patch(): print 'No new revisions.' return False return True def pull(branch, remote, verbose=False, dry_run=False): """Update the local repository from the location specified by 'remote' :param branch: Either a string specifying a local path, or a Branch object. If a local path, the download will be performed, and then a Branch object will be created. :return: Return the branch object that was created """ if isinstance(branch, basestring): local = branch cur_revno = 0 else: local = branch.base cur_revno = branch.revno() if remote[-1:] != '/': remote += '/' rsyncopts = ['-rltp', '--delete' # Don't pull in a new parent location , "--exclude '**/.bzr/x-rsync*'", "--exclude '**/.bzr/x-pull*'" , "--exclude '**/.bzr/parent'", "--exclude '**/.bzr/pull'" ] # Note that when pulling, we do not delete excluded files rsync_exclude = os.path.join(local, '.rsyncexclude') if os.path.exists(rsync_exclude): rsyncopts.append('--exclude-from "%s"' % rsync_exclude) bzr_ignore = os.path.join(local, '.bzrignore') if os.path.exists(bzr_ignore): rsyncopts.append('--exclude-from "%s"' % bzr_ignore) if verbose: rsyncopts.append('-v') if dry_run: rsyncopts.append('--dry-run') cmd = 'rsync %s "%s" "%s"' % (' '.join(rsyncopts), remote, local) if verbose: print cmd status = os.system(cmd) if status != 0: from bzrlib.errors import BzrError raise BzrError('Rsync failed with error code: %s' % status) if isinstance(branch, basestring): from bzrlib.branch import Branch branch = Branch(branch) new_revno = branch.revno() if cur_revno == new_revno: print '** tree is up-to-date' if verbose: if cur_revno != new_revno: from bzrlib.log import show_log show_log(branch, direction='forward', start_revision=cur_revno+1, end_revision=new_revno) return branch def push(branch, remote, verbose=False, dry_run=False): """Update the local repository from the location specified by 'remote' :param branch: Should always be a Branch object """ if isinstance(branch, basestring): from bzrlib.errors import BzrError raise BzrError('rsync push requires a Branch object, not a string') local = branch.base if remote[-1:] != '/': remote += '/' rsyncopts = ['-rltp', '--include-from -' , '--include .bzr' # We don't want to push our local meta information to the remote , "--exclude '.bzr/x-rsync*'", "--exclude '.bzr/x-pull*'" , "--exclude '.bzr/parent'", "--exclude '.bzr/pull'" , "--include '.bzr/**'" , "--exclude '*'", "--exclude '.*'" , '--delete', '--delete-excluded' ] rsync_exclude = os.path.join(local, '.rsyncexclude') if os.path.exists(rsync_exclude): rsyncopts.append('--exclude-from "%s"' % rsync_exclude) bzr_ignore = os.path.join(local, '.bzrignore') if os.path.exists(bzr_ignore): rsyncopts.append('--exclude-from "%s"' % bzr_ignore) if verbose: rsyncopts.append('-v') if dry_run: rsyncopts.append('--dry-run') cmd = 'rsync %s "." "%s"' % (' '.join(rsyncopts), remote) if verbose: print cmd pwd = os.getcwd() try: os.chdir(local) child = os.popen(cmd, 'w') inv = branch.read_working_inventory() for path, entry in inv.entries(): child.write(path) child.write('\n') child.flush() retval = child.close() if retval is not None: from bzrlib.errors import BzrError raise BzrError('Rsync failed with error code: %s' % retval) finally: os.chdir(pwd) commit refs/heads/master mark :793 committer Martin Pool 1119849255 +1000 data 40 - stub shell plugin to check permissions from :792 M 644 inline plugins/checkperms data 59 #! /bin/sh -ex root=`bzr root` find $root \! -perm -0444 commit refs/heads/master mark :794 committer Martin Pool 1119852574 +1000 data 210 - Merge John's nice short-log format. - Change log code to produce output through a LogFormatter object, constructed from a factory method, to allow for more easily adding multiple formats in the future. from :793 M 644 inline bzrlib/commands.py data 53016 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import BzrError, BzrCheckError, BzrCommandError from bzrlib.osutils import quotefn from bzrlib import Branch, Inventory, InventoryEntry, BZRDIR, \ format_date plugin_cmds = {} def register_command(cmd): "Utility function to help register a command" global plugin_cmds k = cmd.__name__ if k.startswith("cmd_"): k_unsquished = _unsquish_command_name(k) else: k_unsquished = k if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = cmd else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r' % sys.modules[cmd.__module__]) def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _get_cmd_dict(plugins_override=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v # If we didn't load plugins, the plugin_cmds dict will be empty if plugins_override: d.update(plugin_cmds) else: d2 = plugin_cmds.copy() d2.update(d) d = d2 return d def get_all_cmds(plugins_override=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(plugins_override=plugins_override).iteritems(): yield k,v def get_cmd_class(cmd, plugins_override=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(plugins_override=plugins_override) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() for opt in self.takes_options: if not opt in OPTIONS: raise BzrError("Unknown option '%s' returned by external command %s" % (opt, path)) # TODO: Is there any way to check takes_args is valid here? self.takes_args = pipe.readline().split() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-usage'" % path) pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-help'" % path) def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: optname = name.replace('_','-') value = kargs[name] if OPTIONS.has_key(optname): # it's an option opts.append('--%s' % optname) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = Branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = Branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): Branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print Branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): bzrlib.add.smart_add(file_list, verbose, not no_recurse) class cmd_mkdir(Command): """Create a new versioned directory. This is equivalent to creating the directory and then adding it. """ takes_args = ['dir+'] def run(self, dir_list): import os import bzrlib.branch b = None for d in dir_list: os.mkdir(d) if not b: b = bzrlib.branch.Branch(d) b.add([d], verbose=True) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print Branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = Branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = Branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = Branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import tempfile from shutil import rmtree import errno br_to = Branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if e.errno != errno.ENOENT: raise if location is None: if stored_loc is None: raise BzrCommandError("No pull location known or specified.") else: print "Using last location: %s" % stored_loc location = stored_loc cache_root = tempfile.mkdtemp() try: from branch import find_cached_branch, DivergedBranches br_from = find_cached_branch(location, cache_root) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged." " Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") finally: rmtree(cache_root) class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. To retrieve the branch as of a particular revision, supply the --revision parameter, as in "branch foo/bar -r 5". """ takes_args = ['from_location', 'to_location?'] takes_options = ['revision'] def run(self, from_location, to_location=None, revision=None): import errno from bzrlib.merge import merge from branch import find_cached_branch, DivergedBranches, NoSuchRevision from shutil import rmtree from meta_store import CachedStore import tempfile cache_root = tempfile.mkdtemp() try: try: br_from = find_cached_branch(from_location, cache_root) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not' ' exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location.rstrip("/\\")) try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already' ' exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) try: br_to.update_revisions(br_from, stop_revision=revision) except NoSuchRevision: rmtree(to_location) msg = "The branch %s has no revision %d." % (from_location, revision) raise BzrCommandError(msg) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False, ignore_zero=True) from_location = pull_loc(br_from) br_to.controlfile("x-pull", "wb").write(from_location + "\n") finally: rmtree(cache_root) def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = Branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info from branch import find_branch b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = Branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: raise BzrError("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = Branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: raise BzrError("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in Branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in Branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff from bzrlib import find_branch if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = Branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = Branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = Branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = Branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" from branch import find_branch b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib import show_log, find_branch from bzrlib.log import log_formatter import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') lf = log_formatter('short', show_ids=show_ids, to_file=outf, show_timezone=timezone) show_log(b, lf, file_id, verbose=verbose, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = Branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = Branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): for f in Branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = Branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = Branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print Branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, exports to a directory (equivalent to --format=dir).""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format'] def run(self, dest, revision=None, format='dir'): b = Branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest, format) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = Branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit from bzrlib.osutils import get_text_message ## Warning: shadows builtin file() if not message and not file: import cStringIO stdout = sys.stdout catcher = cStringIO.StringIO() sys.stdout = catcher cmd_status({"file_list":selected_list}, {}) info = catcher.getvalue() sys.stdout = stdout message = get_text_message(info) if message is None: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = Branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] def run(self, dir='.'): import bzrlib.check bzrlib.check.check(Branch(dir)) class cmd_upgrade(Command): """Upgrade branch storage to current format. This should normally be used only after the check command tells you to run it. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.upgrade import upgrade upgrade(Branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest return int(not selftest()) class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Restore selected files from a previous revision. """ takes_args = ['file+'] def run(self, file_list): from bzrlib.branch import find_branch if not file_list: file_list = ['.'] b = find_branch(file_list[0]) b.revert([b.relpath(f) for f in file_list]) class cmd_merge_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = Branch('.') statcache.update_cache(b.base, b.read_working_inventory()) class cmd_plugins(Command): """List plugins""" hidden = True def run(self): import bzrlib.plugin from pprint import pprint pprint(bzrlib.plugin.all_plugins) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'update': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) >>> parse_args('log -r 500'.split()) (['log'], {'revision': 500}) >>> parse_args('log -r500:600'.split()) (['log'], {'revision': [500, 600]}) >>> parse_args('log -vr500:600'.split()) (['log'], {'verbose': True, 'revision': [500, 600]}) >>> parse_args('log -rv500:600'.split()) #the r takes an argument Traceback (most recent call last): ... ValueError: invalid literal for int(): v500 """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: raise BzrError('unknown long option %r' % a) else: shortopt = a[1:] if shortopt in SHORT_OPTIONS: # Multi-character options must have a space to delimit # their value optname = SHORT_OPTIONS[shortopt] else: # Single character short options, can be chained, # and have their value appended to their name shortopt = a[1:2] if shortopt not in SHORT_OPTIONS: # We didn't find the multi-character name, and we # didn't find the single char name raise BzrError('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if a[2:]: # There are extra things on this option # see if it is the value, or if it is another # short option optargfn = OPTIONS[optname] if optargfn is None: # This option does not take an argument, so the # next entry is another short option, pack it back # into the list argv.insert(0, '-' + a[2:]) else: # This option takes an argument, so pack it # into the array optarg = a[2:] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? raise BzrError('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: raise BzrError('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: raise BzrError('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def _parse_master_args(argv): """Parse the arguments that always go with the original command. These are things like bzr --no-plugins, etc. There are now 2 types of option flags. Ones that come *before* the command, and ones that come *after* the command. Ones coming *before* the command are applied against all possible commands. And are generally applied before plugins are loaded. The current list are: --builtin Allow plugins to load, but don't let them override builtin commands, they will still be allowed if they do not override a builtin. --no-plugins Don't load any plugins. This lets you get back to official source behavior. --profile Enable the hotspot profile before running the command. For backwards compatibility, this is also a non-master option. --version Spit out the version of bzr that is running and exit. This is also a non-master option. --help Run help and exit, also a non-master option (I think that should stay, though) >>> argv, opts = _parse_master_args(['bzr', '--test']) Traceback (most recent call last): ... BzrCommandError: Invalid master option: 'test' >>> argv, opts = _parse_master_args(['bzr', '--version', 'command']) >>> print argv ['command'] >>> print opts['version'] True >>> argv, opts = _parse_master_args(['bzr', '--profile', 'command', '--more-options']) >>> print argv ['command', '--more-options'] >>> print opts['profile'] True >>> argv, opts = _parse_master_args(['bzr', '--no-plugins', 'command']) >>> print argv ['command'] >>> print opts['no-plugins'] True >>> print opts['profile'] False >>> argv, opts = _parse_master_args(['bzr', 'command', '--profile']) >>> print argv ['command', '--profile'] >>> print opts['profile'] False """ master_opts = {'builtin':False, 'no-plugins':False, 'version':False, 'profile':False, 'help':False } # This is the point where we could hook into argv[0] to determine # what front-end is supposed to be run # For now, we are just ignoring it. cmd_name = argv.pop(0) for arg in argv[:]: if arg[:2] != '--': # at the first non-option, we return the rest break arg = arg[2:] # Remove '--' if arg not in master_opts: # We could say that this is not an error, that we should # just let it be handled by the main section instead raise BzrCommandError('Invalid master option: %r' % arg) argv.pop(0) # We are consuming this entry master_opts[arg] = True return argv, master_opts def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: # some options like --builtin and --no-plugins have special effects argv, master_opts = _parse_master_args(argv) if not master_opts['no-plugins']: bzrlib.load_plugins() args, opts = parse_args(argv) if master_opts['help']: from bzrlib.help import help if argv: help(argv[0]) else: help() return 0 if 'help' in opts: from bzrlib.help import help if args: help(args[0]) else: help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 plugins_override = not (master_opts['builtin']) canonical_cmd, cmd_class = get_cmd_class(cmd, plugins_override=plugins_override) profile = master_opts['profile'] # For backwards compatibility, I would rather stick with --profile being a # master/global option if 'profile' in opts: profile = True del opts['profile'] # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): import errno bzrlib.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/log.py data 8113 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Code to show logs of changes. Various flavors of log can be produced: * for one file, or the whole tree, and (not done yet) for files in a given directory * in "verbose" mode with a description of what changed from one version to the next * with file-ids and revision-ids shown * from last to first or (not anymore) from first to last; the default is "reversed" because it shows the likely most relevant and interesting information first * (not yet) in XML format """ from trace import mutter def find_touching_revisions(branch, file_id): """Yield a description of revisions which affect the file_id. Each returned element is (revno, revision_id, description) This is the list of revisions where the file is either added, modified, renamed or deleted. TODO: Perhaps some way to limit this to only particular revisions, or to traverse a non-mainline set of revisions? """ last_ie = None last_path = None revno = 1 for revision_id in branch.revision_history(): this_inv = branch.get_revision_inventory(revision_id) if file_id in this_inv: this_ie = this_inv[file_id] this_path = this_inv.id2path(file_id) else: this_ie = this_path = None # now we know how it was last time, and how it is in this revision. # are those two states effectively the same or not? if not this_ie and not last_ie: # not present in either pass elif this_ie and not last_ie: yield revno, revision_id, "added " + this_path elif not this_ie and last_ie: # deleted here yield revno, revision_id, "deleted " + last_path elif this_path != last_path: yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path)) elif (this_ie.text_size != last_ie.text_size or this_ie.text_sha1 != last_ie.text_sha1): yield revno, revision_id, "modified " + this_path last_ie = this_ie last_path = this_path revno += 1 def show_log(branch, lf, specific_fileid=None, verbose=False, direction='reverse', start_revision=None, end_revision=None): """Write out human-readable log of commits to this branch. lf LogFormatter object to show the output. specific_fileid If true, list only the commits affecting the specified file, rather than all commits. verbose If true show added/changed/deleted/renamed files. direction 'reverse' (default) is latest to earliest; 'forward' is earliest to latest. start_revision If not None, only show revisions >= start_revision end_revision If not None, only show revisions <= end_revision """ from bzrlib.osutils import format_date from bzrlib.errors import BzrCheckError from bzrlib.textui import show_status from warnings import warn if not isinstance(lf, LogFormatter): warn("not a LogFormatter instance: %r" % lf) if specific_fileid: mutter('get log for file_id %r' % specific_fileid) which_revs = branch.enum_history(direction) if not (verbose or specific_fileid): # no need to know what changed between revisions with_deltas = deltas_for_log_dummy(branch, which_revs) elif direction == 'reverse': with_deltas = deltas_for_log_reverse(branch, which_revs) else: raise NotImplementedError("sorry, verbose forward logs not done yet") for revno, rev, delta in with_deltas: if specific_fileid: if not delta.touches_file_id(specific_fileid): continue if start_revision is not None and revno < start_revision: continue if end_revision is not None and revno > end_revision: continue if not verbose: # although we calculated it, throw it away without display delta = None lf.show(revno, rev, delta) def deltas_for_log_dummy(branch, which_revs): for revno, revision_id in which_revs: yield revno, branch.get_revision(revision_id), None def deltas_for_log_reverse(branch, which_revs): """Compute deltas for display in reverse log. Given a sequence of (revno, revision_id) pairs, return (revno, rev, delta). The delta is from the given revision to the next one in the sequence, which makes sense if the log is being displayed from newest to oldest. """ from tree import EmptyTree from diff import compare_trees last_revno = last_revision_id = last_tree = None for revno, revision_id in which_revs: this_tree = branch.revision_tree(revision_id) this_revision = branch.get_revision(revision_id) if last_revno: yield last_revno, last_revision, compare_trees(this_tree, last_tree, False) last_revno = revno last_revision = this_revision last_tree = this_tree if last_revno: this_tree = EmptyTree() yield last_revno, last_revision, compare_trees(this_tree, last_tree, False) class LogFormatter(object): """Abstract class to display log messages.""" def __init__(self, to_file, show_ids=False, show_timezone=False): self.to_file = to_file self.show_ids = show_ids self.show_timezone = show_timezone class LongLogFormatter(LogFormatter): def show(self, revno, rev, delta): from osutils import format_date to_file = self.to_file print >>to_file, '-' * 60 print >>to_file, 'revno:', revno if self.show_ids: print >>to_file, 'revision-id:', rev.revision_id print >>to_file, 'committer:', rev.committer print >>to_file, 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, self.show_timezone)) print >>to_file, 'message:' if not rev.message: print >>to_file, ' (no message)' else: for l in rev.message.split('\n'): print >>to_file, ' ' + l if delta != None: delta.show(to_file, self.show_ids) class ShortLogFormatter(LogFormatter): def show(self, revno, rev, delta): from bzrlib.osutils import format_date to_file = self.to_file print >>to_file, "%5d %s\t%s" % (revno, rev.committer, format_date(rev.timestamp, rev.timezone or 0, self.show_timezone)) if self.show_ids: print >>to_file, ' revision-id:', rev.revision_id if not rev.message: print >>to_file, ' (no message)' else: for l in rev.message.split('\n'): print >>to_file, ' ' + l if delta != None: delta.show(to_file, self.show_ids) print FORMATTERS = {'long': LongLogFormatter, 'short': ShortLogFormatter, } def log_formatter(name, *args, **kwargs): from bzrlib.errors import BzrCommandError try: return FORMATTERS[name](*args, **kwargs) except IndexError: raise BzrCommandError("unknown log formatter: %r" % name) commit refs/heads/master mark :795 committer Martin Pool 1119852615 +1000 data 91 Patch from John: - Use config_dir for default plugin dir - Fix up disabling BZR_PLUGIN_PATH from :794 M 644 inline bzrlib/plugin.py data 4708 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # This module implements plug-in support. # Any python module in $BZR_PLUGIN_PATH will be imported upon initialization # of bzrlib (and then forgotten about). In the plugin's main body, it should # update any bzrlib registries it wants to extend; for example, to add new # commands, import bzrlib.commands and add your new command to the # plugin_cmds variable. import os from bzrlib.osutils import config_dir DEFAULT_PLUGIN_PATH = os.path.join(config_dir(), 'plugins') all_plugins = [] def load_plugins(): """ Find all python plugins and load them. Loading a plugin means importing it into the python interpreter. The plugin is expected to make calls to register commands when it's loaded (or perhaps access other hooks in future.) A list of plugs is stored in bzrlib.plugin.all_plugins for future reference. The environment variable BZR_PLUGIN_PATH is considered a delimited set of paths to look through. Each entry is searched for *.py files (and whatever other extensions are used in the platform, such as *.pyd). """ import sys, os, imp try: set except NameError: from sets import Set as set # python2.3 from bzrlib.trace import log_error, mutter, log_exception from bzrlib.errors import BzrError bzrpath = os.environ.get('BZR_PLUGIN_PATH') if bzrpath is None: bzrpath = DEFAULT_PLUGIN_PATH global all_plugins if all_plugins: raise BzrError("plugins already initialized") # The problem with imp.get_suffixes() is that it doesn't include # .pyo which is technically valid # It also means that "testmodule.so" will show up as both test and testmodule # though it is only valid as 'test' # but you should be careful, because "testmodule.py" loads as testmodule. suffixes = imp.get_suffixes() suffixes.append(('.pyo', 'rb', imp.PY_COMPILED)) package_entries = ['__init__.py', '__init__.pyc', '__init__.pyo'] for d in bzrpath.split(os.pathsep): # going through them one by one allows different plugins with the same # filename in different directories in the path mutter('looking for plugins in %s' % d) if not d: continue plugin_names = set() if not os.path.isdir(d): continue for f in os.listdir(d): path = os.path.join(d, f) if os.path.isdir(path): for entry in package_entries: # This directory should be a package, and thus added to # the list if os.path.isfile(os.path.join(path, entry)): break else: # This directory is not a package continue else: for suffix_info in suffixes: if f.endswith(suffix_info[0]): f = f[:-len(suffix_info[0])] if suffix_info[2] == imp.C_EXTENSION and f.endswith('module'): f = f[:-len('module')] break else: continue mutter('add plugin name %s' % f) plugin_names.add(f) plugin_names = list(plugin_names) plugin_names.sort() for name in plugin_names: try: plugin_info = imp.find_module(name, [d]) mutter('load plugin %r' % (plugin_info,)) try: plugin = imp.load_module('bzrlib.plugin.' + name, *plugin_info) all_plugins.append(plugin_info) finally: if plugin_info[0] is not None: plugin_info[0].close() except Exception, e: log_error('Unable to load plugin %r from %r' % (name, d)) log_error(str(e)) log_exception() commit refs/heads/master mark :796 committer Martin Pool 1119853530 +1000 data 22 - note bzr log changes from :795 M 644 inline NEWS data 8911 DEVELOPMENT HEAD NEW FEATURES: * Python plugins, automatically loaded from the directories on BZR_PLUGIN_PATH or ~/.bzr.conf/plugins by default. * New 'bzr mkdir' command. * Commit mesage is fetched from an editor if not given on the command line; patch from Torsten Marek. CHANGES: * New ``bzr upgrade`` command to upgrade the format of a branch, replacing ``bzr check --update``. * Files within store directories are no longer marked readonly on disk. * Changed ``bzr log`` output to a more compact form suggested by John A Meinel. bzr-0.0.5 2005-06-15 CHANGES: * ``bzr`` with no command now shows help rather than giving an error. Suggested by Michael Ellerman. * ``bzr status`` output format changed, because svn-style output doesn't really match the model of bzr. Now files are grouped by status and can be shown with their IDs. ``bzr status --all`` shows all versioned files and unknown files but not ignored files. * ``bzr log`` runs from most-recent to least-recent, the reverse of the previous order. The previous behaviour can be obtained with the ``--forward`` option. * ``bzr inventory`` by default shows only filenames, and also ids if ``--show-ids`` is given, in which case the id is the second field. ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New ``bzr ignore PATTERN`` command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.sw[nop] .git .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. File may be in a branch other than the working directory. * ``bzr log`` and ``bzr root`` can be given an http URL instead of a filename. * Commands can now be defined by external programs or scripts in a directory on $BZRPATH. * New "stat cache" avoids reading the contents of files if they haven't changed since the previous time. * If the Python interpreter is too old, try to find a better one or give an error. Based on a patch from Fredrik Lundh. * New optional parameter ``bzr info [BRANCH]``. * New form ``bzr commit SELECTED`` to commit only selected files. * New form ``bzr log -r FROM:TO`` shows changes in selected range; contributed by John A Meinel. * New option ``bzr diff --diff-options 'OPTS'`` allows passing options through to an external GNU diff. * New option ``bzr add --no-recurse`` to add a directory but not their contents. * ``bzr --version`` now shows more information if bzr is being run from a branch. BUG FIXES: * Fixed diff format so that added and removed files will be handled properly by patch. Fix from Lalo Martins. * Various fixes for files whose names contain spaces or other metacharacters. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. * testbzr requires python2.4, but can be used to test bzr running under a different version. * Tests added for many other changes in this release. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. * New developer commands ``added``, ``modified``. PORTABILITY: * Cope on Windows on python2.3 by using the weaker random seed. 2.4 is now only recommended. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. commit refs/heads/master mark :797 committer Martin Pool 1119853810 +1000 data 31 - run the right bzr for testing from :796 M 644 inline bzrlib/selftest/__init__.py data 8929 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") raise class CommandFailed(Exception): pass class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr, so that we can run it # through a specified Python intepreter OVERRIDE_PYTHON = None # to run with alternative python 'python' BZRPATH = 'bzr' _log_buf = "" def formcmd(self, cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = self.BZRPATH if self.OVERRIDE_PYTHON: cmd.insert(0, self.OVERRIDE_PYTHON) self.log('$ %r' % cmd) return cmd def runcmd(self, cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = self.formcmd(cmd) self.log('$ ' + ' '.join(cmd)) actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(self, cmd, retcode=0): """Run a command and return its output""" cmd = self.formcmd(cmd) child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG) outd, errd = child.communicate() self.log(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def build_tree(self, shape): """Build a test tree according to a pattern. shape is a sequence of file specifications. If the final character is '/', a directory is created. This doesn't add anything to a branch. """ # XXX: It's OK to just create them using forward slashes on windows? import os for name in shape: assert isinstance(name, basestring) if name[-1] == '/': os.mkdir(name[:-1]) else: f = file(name, 'wt') print >>f, "contents of", name f.close() def log(self, msg): """Log a message to a progress file""" self._log_buf = self._log_buf + str(msg) + '\n' print >>self.TEST_LOG, msg def check_inventory_shape(self, inv, shape): """ Compare an inventory to a list of expected names. Fail if they are not precisely equal. """ extras = [] shape = list(shape) # copy for path, ie in inv.entries(): name = path.replace('\\', '/') if ie.kind == 'dir': name = name + '/' if name in shape: shape.remove(name) else: extras.append(name) if shape: self.fail("expected paths not found in inventory: %r" % shape) if extras: self.fail("unexpected paths found in inventory: %r" % extras) def check_file_contents(self, filename, expect): self.log("check contents of file %s" % filename) contents = file(filename, 'r').read() if contents != expect: self.log("expected: %r" % expected) self.log("actually: %r" % contents) self.fail("contents of %s not as expected") class InTempDir(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.test_dir = os.path.join(self.TEST_ROOT, self.__class__.__name__) os.mkdir(self.test_dir) os.chdir(self.test_dir) def tearDown(self): import os os.chdir(self.TEST_ROOT) class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ def __init__(self, out): self.out = out TestResult.__init__(self) def startTest(self, test): # TODO: Maybe show test.shortDescription somewhere? print >>self.out, '%-60.60s' % test.id(), self.out.flush() TestResult.startTest(self, test) def stopTest(self, test): # print TestResult.stopTest(self, test) def addError(self, test, err): print >>self.out, 'ERROR' TestResult.addError(self, test, err) _show_test_failure('error', test, err, self.out) def addFailure(self, test, err): print >>self.out, 'FAILURE' TestResult.addFailure(self, test, err) _show_test_failure('failure', test, err, self.out) def addSuccess(self, test): print >>self.out, 'OK' TestResult.addSuccess(self, test) def selftest(): from unittest import TestLoader, TestSuite import bzrlib import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning from doctest import DocTestSuite import os import shutil import time import sys TestBase.BZRPATH = os.path.join(os.path.realpath(os.path.dirname(bzrlib.__path__[0])), 'bzr') print '%-30s %s' % ('bzr binary', TestBase.BZRPATH) _setup_test_log() _setup_test_dir() print suite = TestSuite() tl = TestLoader() for m in bzrlib.selftest.whitebox, \ bzrlib.selftest.versioning: suite.addTest(tl.loadTestsFromModule(m)) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands: suite.addTest(DocTestSuite(m)) suite.addTest(bzrlib.selftest.blackbox.suite()) # save stdout & stderr so there's no leakage from code-under-test real_stdout = sys.stdout real_stderr = sys.stderr sys.stdout = sys.stderr = TestBase.TEST_LOG try: result = _MyResult(real_stdout) suite.run(result) finally: sys.stdout = real_stdout sys.stderr = real_stderr _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os log_filename = os.path.abspath('testbzr.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "bzr tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_ROOT = os.path.abspath("testbzr.tmp") print '%-30s %s' % ('running tests in', TestBase.TEST_ROOT) if os.path.exists(TestBase.TEST_ROOT): shutil.rmtree(TestBase.TEST_ROOT) os.mkdir(TestBase.TEST_ROOT) os.chdir(TestBase.TEST_ROOT) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_ROOT, '.bzr')) def _show_results(result): print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, exc_info, out): from traceback import print_exception print >>out, '-' * 60 print >>out, case desc = case.shortDescription() if desc: print >>out, ' (%s)' % desc print_exception(exc_info[0], exc_info[1], exc_info[2], None, out) if isinstance(case, TestBase): print >>out print >>out, 'log from this test:' print >>out, case._log_buf print >>out, '-' * 60 commit refs/heads/master mark :798 committer Martin Pool 1119855484 +1000 data 95 - allow run_bzr to be invoked repeatedly without complaining about repeatedly loading plugins from :797 M 644 inline bzrlib/plugin.py data 4699 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # This module implements plug-in support. # Any python module in $BZR_PLUGIN_PATH will be imported upon initialization # of bzrlib (and then forgotten about). In the plugin's main body, it should # update any bzrlib registries it wants to extend; for example, to add new # commands, import bzrlib.commands and add your new command to the # plugin_cmds variable. import os from bzrlib.osutils import config_dir DEFAULT_PLUGIN_PATH = os.path.join(config_dir(), 'plugins') all_plugins = [] def load_plugins(): """ Find all python plugins and load them. Loading a plugin means importing it into the python interpreter. The plugin is expected to make calls to register commands when it's loaded (or perhaps access other hooks in future.) A list of plugs is stored in bzrlib.plugin.all_plugins for future reference. The environment variable BZR_PLUGIN_PATH is considered a delimited set of paths to look through. Each entry is searched for *.py files (and whatever other extensions are used in the platform, such as *.pyd). """ global all_plugins if all_plugins: return # plugins already initialized import sys, os, imp try: set except NameError: from sets import Set as set # python2.3 from bzrlib.trace import log_error, mutter, log_exception from bzrlib.errors import BzrError bzrpath = os.environ.get('BZR_PLUGIN_PATH') if bzrpath is None: bzrpath = DEFAULT_PLUGIN_PATH # The problem with imp.get_suffixes() is that it doesn't include # .pyo which is technically valid # It also means that "testmodule.so" will show up as both test and testmodule # though it is only valid as 'test' # but you should be careful, because "testmodule.py" loads as testmodule. suffixes = imp.get_suffixes() suffixes.append(('.pyo', 'rb', imp.PY_COMPILED)) package_entries = ['__init__.py', '__init__.pyc', '__init__.pyo'] for d in bzrpath.split(os.pathsep): # going through them one by one allows different plugins with the same # filename in different directories in the path mutter('looking for plugins in %s' % d) if not d: continue plugin_names = set() if not os.path.isdir(d): continue for f in os.listdir(d): path = os.path.join(d, f) if os.path.isdir(path): for entry in package_entries: # This directory should be a package, and thus added to # the list if os.path.isfile(os.path.join(path, entry)): break else: # This directory is not a package continue else: for suffix_info in suffixes: if f.endswith(suffix_info[0]): f = f[:-len(suffix_info[0])] if suffix_info[2] == imp.C_EXTENSION and f.endswith('module'): f = f[:-len('module')] break else: continue mutter('add plugin name %s' % f) plugin_names.add(f) plugin_names = list(plugin_names) plugin_names.sort() for name in plugin_names: try: plugin_info = imp.find_module(name, [d]) mutter('load plugin %r' % (plugin_info,)) try: plugin = imp.load_module('bzrlib.plugin.' + name, *plugin_info) all_plugins.append(plugin_info) finally: if plugin_info[0] is not None: plugin_info[0].close() except Exception, e: log_error('Unable to load plugin %r from %r' % (name, d)) log_error(str(e)) log_exception() commit refs/heads/master mark :799 committer Martin Pool 1119926241 +1000 data 31 - tidy up check for plugin path from :798 M 644 inline bzrlib/plugin.py data 4658 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # This module implements plug-in support. # Any python module in $BZR_PLUGIN_PATH will be imported upon initialization # of bzrlib (and then forgotten about). In the plugin's main body, it should # update any bzrlib registries it wants to extend; for example, to add new # commands, import bzrlib.commands and add your new command to the # plugin_cmds variable. import os from bzrlib.osutils import config_dir DEFAULT_PLUGIN_PATH = os.path.join(config_dir(), 'plugins') all_plugins = [] def load_plugins(): """ Find all python plugins and load them. Loading a plugin means importing it into the python interpreter. The plugin is expected to make calls to register commands when it's loaded (or perhaps access other hooks in future.) A list of plugs is stored in bzrlib.plugin.all_plugins for future reference. The environment variable BZR_PLUGIN_PATH is considered a delimited set of paths to look through. Each entry is searched for *.py files (and whatever other extensions are used in the platform, such as *.pyd). """ global all_plugins if all_plugins: return # plugins already initialized import sys, os, imp try: set except NameError: from sets import Set as set # python2.3 from bzrlib.trace import log_error, mutter, log_exception from bzrlib.errors import BzrError bzrpath = os.environ.get('BZR_PLUGIN_PATH', DEFAULT_PLUGIN_PATH) # The problem with imp.get_suffixes() is that it doesn't include # .pyo which is technically valid # It also means that "testmodule.so" will show up as both test and testmodule # though it is only valid as 'test' # but you should be careful, because "testmodule.py" loads as testmodule. suffixes = imp.get_suffixes() suffixes.append(('.pyo', 'rb', imp.PY_COMPILED)) package_entries = ['__init__.py', '__init__.pyc', '__init__.pyo'] for d in bzrpath.split(os.pathsep): # going through them one by one allows different plugins with the same # filename in different directories in the path mutter('looking for plugins in %s' % d) if not d: continue plugin_names = set() if not os.path.isdir(d): continue for f in os.listdir(d): path = os.path.join(d, f) if os.path.isdir(path): for entry in package_entries: # This directory should be a package, and thus added to # the list if os.path.isfile(os.path.join(path, entry)): break else: # This directory is not a package continue else: for suffix_info in suffixes: if f.endswith(suffix_info[0]): f = f[:-len(suffix_info[0])] if suffix_info[2] == imp.C_EXTENSION and f.endswith('module'): f = f[:-len('module')] break else: continue mutter('add plugin name %s' % f) plugin_names.add(f) plugin_names = list(plugin_names) plugin_names.sort() for name in plugin_names: try: plugin_info = imp.find_module(name, [d]) mutter('load plugin %r' % (plugin_info,)) try: plugin = imp.load_module('bzrlib.plugin.' + name, *plugin_info) all_plugins.append(plugin_info) finally: if plugin_info[0] is not None: plugin_info[0].close() except Exception, e: log_error('Unable to load plugin %r from %r' % (name, d)) log_error(str(e)) log_exception() commit refs/heads/master mark :800 committer Martin Pool 1119927751 +1000 data 3166 Merge John's import-speedup branch: 777 John Arbash Meinel Sun 2005-06-26 22:20:32 -0500 revision-id: john@arbash-meinel.com-20050627032031-e82a50db3863b18e bzr selftest was not using the correct bzr 776 John Arbash Meinel Sun 2005-06-26 22:20:22 -0500 revision-id: john@arbash-meinel.com-20050627032021-c9f21fde989ddaee Add was using an old mutter 775 John Arbash Meinel Sun 2005-06-26 22:02:33 -0500 revision-id: john@arbash-meinel.com-20050627030233-9165cfe98fc63298 Cleaned up to be less different 774 John Arbash Meinel Sun 2005-06-26 21:54:53 -0500 revision-id: john@arbash-meinel.com-20050627025452-4260d0e744edef43 Allow BZR_PLUGIN_PATH='' to negate plugin loading. 773 John Arbash Meinel Sun 2005-06-26 21:49:34 -0500 revision-id: john@arbash-meinel.com-20050627024933-b7158f67b7b9eae5 Finished the previous cleanup (allowing load_plugins to be called twice) 772 John Arbash Meinel Sun 2005-06-26 21:45:08 -0500 revision-id: john@arbash-meinel.com-20050627024508-723b1df510d196fc Work on making the tests pass. versioning.py is calling run_cmd directly, but plugins have been loaded. 771 John Arbash Meinel Sun 2005-06-26 21:32:29 -0500 revision-id: john@arbash-meinel.com-20050627023228-79972744d7c53e15 Got it down a little bit more by removing import of tree and inventory. 770 John Arbash Meinel Sun 2005-06-26 21:26:05 -0500 revision-id: john@arbash-meinel.com-20050627022604-350b9773ef622f95 Reducing the number of import from bzrlib/__init__.py and bzrlib/branch.py 769 John Arbash Meinel Sun 2005-06-26 20:32:25 -0500 revision-id: john@arbash-meinel.com-20050627013225-32dd044f10d23948 Updated revision.py and xml.py to include SubElement. 768 John Arbash Meinel Sun 2005-06-26 20:03:56 -0500 revision-id: john@arbash-meinel.com-20050627010356-ee66919e1c377faf Minor typo 767 John Arbash Meinel Sun 2005-06-26 20:03:13 -0500 revision-id: john@arbash-meinel.com-20050627010312-40d024007eb85051 Caching the import 766 John Arbash Meinel Sun 2005-06-26 19:51:47 -0500 revision-id: john@arbash-meinel.com-20050627005147-5281c99e48ed1834 Created wrapper functions for lazy import of ElementTree 765 John Arbash Meinel Sun 2005-06-26 19:46:37 -0500 revision-id: john@arbash-meinel.com-20050627004636-bf432902004a94c5 Removed all of the test imports of cElementTree 764 John Arbash Meinel Sun 2005-06-26 19:43:59 -0500 revision-id: john@arbash-meinel.com-20050627004358-d137fbe9570dd71b Trying to make bzr startup faster. from :799 M 644 inline bzrlib/__init__.py data 1906 # (C) 2005 Canonical Development Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr library""" from branch import Branch, ScratchBranch, find_branch from errors import BzrError BZRDIR = ".bzr" DEFAULT_IGNORE = ['.bzr.log', '*~', '#*#', '*$', '.#*', '.*.sw[nop]', '.*.tmp', '*.tmp', '*.bak', '*.BAK', '*.orig', '*.o', '*.obj', '*.a', '*.py[oc]', '*.so', '*.exe', '*.elc', '{arch}', 'CVS', 'CVS.adm', '.svn', '_darcs', 'SCCS', 'RCS', '*,v', 'BitKeeper', '.git', 'TAGS', '.make.state', '.sconsign', '.tmp*', '.del-*'] IGNORE_FILENAME = ".bzrignore" import locale user_encoding = locale.getpreferredencoding() del locale __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __version__ = '0.0.5' def get_bzr_revision(): """If bzr is run from a branch, return (revno,revid) or None""" try: branch = Branch(__path__[0]) rh = branch.revision_history() if rh: return len(rh), rh[-1] else: return None except BzrError: return None M 644 inline bzrlib/add.py data 3607 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, sys import bzrlib from trace import mutter, note def glob_expand_for_win32(file_list): import glob expanded_file_list = [] for possible_glob in file_list: glob_files = glob.glob(possible_glob) if glob_files == []: # special case to let the normal code path handle # files that do not exists expanded_file_list.append(possible_glob) else: expanded_file_list += glob_files return expanded_file_list def smart_add(file_list, verbose=True, recurse=True): """Add files to version, optionally recursing into directories. This is designed more towards DWIM for humans than API simplicity. For the specific behaviour see the help for cmd_add(). """ from bzrlib.osutils import quotefn, kind_marker from bzrlib.errors import BadFileKindError, ForbiddenFileError assert file_list if sys.platform == 'win32': file_list = glob_expand_for_win32(file_list) user_list = file_list[:] assert not isinstance(file_list, basestring) b = bzrlib.branch.Branch(file_list[0], find_root=True) inv = b.read_working_inventory() tree = b.working_tree() count = 0 for f in file_list: rf = b.relpath(f) af = b.abspath(rf) kind = bzrlib.osutils.file_kind(af) if kind != 'file' and kind != 'directory': if f in user_list: raise BadFileKindError("cannot add %s of type %s" % (f, kind)) else: print "skipping %s (can't add file of kind '%s')" % (f, kind) continue mutter("smart add of %r, abs=%r" % (f, af)) if bzrlib.branch.is_control_file(af): raise ForbiddenFileError('cannot add control file %s' % f) versioned = (inv.path2id(rf) != None) if rf == '': mutter("branch root doesn't need to be added") elif versioned: mutter("%r is already versioned" % f) else: file_id = bzrlib.branch.gen_file_id(rf) inv.add_path(rf, kind=kind, file_id=file_id) mutter("added %r kind %r file_id={%s}" % (rf, kind, file_id)) count += 1 print 'added', quotefn(f) if kind == 'directory' and recurse: for subf in os.listdir(af): subp = os.path.join(rf, subf) if subf == bzrlib.BZRDIR: mutter("skip control directory %r" % subp) elif tree.is_ignored(subp): mutter("skip ignored sub-file %r" % subp) else: mutter("queue to add sub-file %r" % subp) file_list.append(b.abspath(subp)) if count > 0: if verbose: note('added %d' % count) b._write_inventory(inv) M 644 inline bzrlib/branch.py data 38542 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, splitpath, \ sha_file, appendpath, file_kind from bzrlib.errors import BzrError BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def find_cached_branch(f, cache_root, **args): from remotebranch import RemoteBranch br = find_branch(f, **args) def cacheify(br, store_name): from meta_store import CachedStore cache_path = os.path.join(cache_root, store_name) os.mkdir(cache_path) new_store = CachedStore(getattr(br, store_name), cache_path) setattr(br, store_name, new_store) if isinstance(br, RemoteBranch): cacheify(br, 'inventory_store') cacheify(br, 'text_store') cacheify(br, 'revision_store') return br def _relpath(base, path): """Return path relative to base, or raise exception. The path may be either an absolute path or a path relative to the current working directory. Lifted out of Branch.relpath for ease of testing. os.path.commonprefix (python2.4) has a bad bug that it works just on string prefixes, assuming that '/u' is a prefix of '/u2'. This avoids that problem.""" rp = os.path.abspath(path) s = [] head = rp while len(head) >= len(base): if head == base: break head, tail = os.path.split(head) if tail: s.insert(0, tail) else: from errors import NotBranchError raise NotBranchError("path %r is not within branch %r" % (rp, base)) return os.sep.join(s) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head class DivergedBranches(Exception): def __init__(self, branch1, branch2): self.branch1 = branch1 self.branch2 = branch2 Exception.__init__(self, "These branches have diverged.") class NoSuchRevision(BzrError): def __init__(self, branch, revision): self.branch = branch self.revision = revision msg = "Branch %s has no revision %d" % (branch, revision) BzrError.__init__(self, msg) ###################################################################### # branch objects class Branch(object): """Branch holding a history of revisions. base Base directory of the branch. _lock_mode None, or 'r' or 'w' _lock_count If _lock_mode is true, a positive count of the number of times the lock has been taken. _lock Lock object from bzrlib.lock. """ base = None _lock_mode = None _lock_count = None _lock = None def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ from bzrlib.store import ImmutableStore if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): from errors import NotBranchError raise NotBranchError("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def __del__(self): if self._lock_mode or self._lock: from warnings import warn warn("branch %r was not explicitly unlocked" % self) self._lock.unlock() def lock_write(self): if self._lock_mode: if self._lock_mode != 'w': from errors import LockError raise LockError("can't upgrade to a write lock from %r" % self._lock_mode) self._lock_count += 1 else: from bzrlib.lock import WriteLock self._lock = WriteLock(self.controlfilename('branch-lock')) self._lock_mode = 'w' self._lock_count = 1 def lock_read(self): if self._lock_mode: assert self._lock_mode in ('r', 'w'), \ "invalid lock mode %r" % self._lock_mode self._lock_count += 1 else: from bzrlib.lock import ReadLock self._lock = ReadLock(self.controlfilename('branch-lock')) self._lock_mode = 'r' self._lock_count = 1 def unlock(self): if not self._lock_mode: from errors import LockError raise LockError('branch %r is not locked' % (self)) if self._lock_count > 1: self._lock_count -= 1 else: self._lock.unlock() self._lock = None self._lock_mode = self._lock_count = None def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" return _relpath(self.base, path) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, basestring): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): from bzrlib.inventory import Inventory os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.\n") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) Inventory().write_xml(self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: raise BzrError('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" from bzrlib.inventory import Inventory from time import time before = time() # ElementTree does its own conversion from UTF-8, so open in # binary. self.lock_read() try: inv = Inventory.read_xml(self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time() - before)) return inv finally: self.unlock() def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ self.lock_write() try: from bzrlib.atomicfile import AtomicFile f = AtomicFile(self.controlfilename('inventory'), 'wb') try: inv.write_xml(f) f.commit() finally: f.close() finally: self.unlock() mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. files List of paths to add, relative to the base of the tree. ids If set, use these instead of automatically generated ids. Must be the same length as the list of files, but may contain None for ids that are to be autogenerated. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ from bzrlib.textui import show_status # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, basestring): assert(ids is None or isinstance(ids, basestring)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) self.lock_write() try: inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): raise BzrError("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: raise BzrError("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: print 'added', quotefn(f) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) finally: self.unlock() def print_file(self, file, revno): """Print `file` to stdout.""" self.lock_read() try: tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: raise BzrError("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) finally: self.unlock() def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ from bzrlib.textui import show_status ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, basestring): files = [files] self.lock_write() try: tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: raise BzrError("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) finally: self.unlock() # FIXME: this doesn't need to be a branch method def set_inventory(self, new_inventory_list): from bzrlib.inventory import Inventory, InventoryEntry inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): from bzrlib.atomicfile import AtomicFile mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() + [revision_id] f = AtomicFile(self.controlfilename('revision-history')) try: for rev_id in rev_history: print >>f, rev_id f.commit() finally: f.close() def get_revision(self, revision_id): """Return the Revision object for a named revision""" from bzrlib.revision import Revision if not revision_id or not isinstance(revision_id, basestring): raise ValueError('invalid revision-id: %r' % revision_id) r = Revision.read_xml(self.revision_store[revision_id]) assert r.revision_id == revision_id return r def get_revision_sha1(self, revision_id): """Hash the stored value of a revision, and return it.""" # In the future, revision entries will be signed. At that # point, it is probably best *not* to include the signature # in the revision hash. Because that lets you re-sign # the revision, (add signatures/remove signatures) and still # have all hash pointers stay consistent. # But for now, just hash the contents. return sha_file(self.revision_store[revision_id]) def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" from bzrlib.inventory import Inventory i = Inventory.read_xml(self.inventory_store[inventory_id]) return i def get_inventory_sha1(self, inventory_id): """Return the sha1 hash of the inventory entry """ return sha_file(self.inventory_store[inventory_id]) def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: from bzrlib.inventory import Inventory return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self.lock_read() try: return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] finally: self.unlock() def common_ancestor(self, other, self_revno=None, other_revno=None): """ >>> import commit >>> sb = ScratchBranch(files=['foo', 'foo~']) >>> sb.common_ancestor(sb) == (None, None) True >>> commit.commit(sb, "Committing first revision", verbose=False) >>> sb.common_ancestor(sb)[0] 1 >>> clone = sb.clone() >>> commit.commit(sb, "Committing second revision", verbose=False) >>> sb.common_ancestor(sb)[0] 2 >>> sb.common_ancestor(clone)[0] 1 >>> commit.commit(clone, "Committing divergent second revision", ... verbose=False) >>> sb.common_ancestor(clone)[0] 1 >>> sb.common_ancestor(clone) == clone.common_ancestor(sb) True >>> sb.common_ancestor(sb) != clone.common_ancestor(clone) True >>> clone2 = sb.clone() >>> sb.common_ancestor(clone2)[0] 2 >>> sb.common_ancestor(clone2, self_revno=1)[0] 1 >>> sb.common_ancestor(clone2, other_revno=1)[0] 1 """ my_history = self.revision_history() other_history = other.revision_history() if self_revno is None: self_revno = len(my_history) if other_revno is None: other_revno = len(other_history) indices = range(min((self_revno, other_revno))) indices.reverse() for r in indices: if my_history[r] == other_history[r]: return r+1, my_history[r] return None, None def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def missing_revisions(self, other, stop_revision=None): """ If self and other have not diverged, return a list of the revisions present in other, but missing from self. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch() >>> br2 = ScratchBranch() >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [u'REVISION-ID-1'] >>> br2.missing_revisions(br1) [] >>> commit(br1, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-2A") >>> br1.missing_revisions(br2) [u'REVISION-ID-2A'] >>> commit(br1, "lala!", rev_id="REVISION-ID-2B") >>> br1.missing_revisions(br2) Traceback (most recent call last): DivergedBranches: These branches have diverged. """ self_history = self.revision_history() self_len = len(self_history) other_history = other.revision_history() other_len = len(other_history) common_index = min(self_len, other_len) -1 if common_index >= 0 and \ self_history[common_index] != other_history[common_index]: raise DivergedBranches(self, other) if stop_revision is None: stop_revision = other_len elif stop_revision > other_len: raise NoSuchRevision(self, stop_revision) return other_history[self_len:stop_revision] def update_revisions(self, other, stop_revision=None): """Pull in all new revisions from other branch. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch(files=['foo', 'bar']) >>> br1.add('foo') >>> br1.add('bar') >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False) >>> br2 = ScratchBranch() >>> br2.update_revisions(br1) Added 2 texts. Added 1 inventories. Added 1 revisions. >>> br2.revision_history() [u'REVISION-ID-1'] >>> br2.update_revisions(br1) Added 0 texts. Added 0 inventories. Added 0 revisions. >>> br1.text_store.total_size() == br2.text_store.total_size() True """ from bzrlib.progress import ProgressBar try: set except NameError: from sets import Set as set pb = ProgressBar() pb.update('comparing histories') revision_ids = self.missing_revisions(other, stop_revision) if hasattr(other.revision_store, "prefetch"): other.revision_store.prefetch(revision_ids) if hasattr(other.inventory_store, "prefetch"): inventory_ids = [other.get_revision(r).inventory_id for r in revision_ids] other.inventory_store.prefetch(inventory_ids) revisions = [] needed_texts = set() i = 0 for rev_id in revision_ids: i += 1 pb.update('fetching revision', i, len(revision_ids)) rev = other.get_revision(rev_id) revisions.append(rev) inv = other.get_inventory(str(rev.inventory_id)) for key, entry in inv.iter_entries(): if entry.text_id is None: continue if entry.text_id not in self.text_store: needed_texts.add(entry.text_id) pb.clear() count = self.text_store.copy_multi(other.text_store, needed_texts) print "Added %d texts." % count inventory_ids = [ f.inventory_id for f in revisions ] count = self.inventory_store.copy_multi(other.inventory_store, inventory_ids) print "Added %d inventories." % count revision_ids = [ f.revision_id for f in revisions] count = self.revision_store.copy_multi(other.revision_store, revision_ids) for revision_id in revision_ids: self.append_revision(revision_id) print "Added %d revisions." % count def commit(self, *args, **kw): from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" from bzrlib.tree import EmptyTree, RevisionTree # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ from bzrlib.tree import EmptyTree, RevisionTree r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self.lock_write() try: tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): raise BzrError("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): raise BzrError("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: raise BzrError("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): raise BzrError("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': raise BzrError("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self.lock_write() try: ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): raise BzrError("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): raise BzrError("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': raise BzrError("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): raise BzrError("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): raise BzrError("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: raise BzrError("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): raise BzrError("destination %r already exists" % dest_path) if f_id in to_idpath: raise BzrError("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() def revert(self, filenames, old_tree=None, backups=True): """Restore selected files to the versions from a previous tree. backups If true (default) backups are made of files before they're renamed. """ from bzrlib.errors import NotVersionedError, BzrError from bzrlib.atomicfile import AtomicFile from bzrlib.osutils import backup_file inv = self.read_working_inventory() if old_tree is None: old_tree = self.basis_tree() old_inv = old_tree.inventory nids = [] for fn in filenames: file_id = inv.path2id(fn) if not file_id: raise NotVersionedError("not a versioned file", fn) if not old_inv.has_id(file_id): raise BzrError("file not present in old tree", fn, file_id) nids.append((fn, file_id)) # TODO: Rename back if it was previously at a different location # TODO: If given a directory, restore the entire contents from # the previous version. # TODO: Make a backup to a temporary file. # TODO: If the file previously didn't exist, delete it? for fn, file_id in nids: backup_file(fn) f = AtomicFile(fn, 'wb') try: f.write(old_tree.get_file(file_id).read()) f.commit() finally: f.close() class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[], base=None): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ from tempfile import mkdtemp init = False if base is None: base = mkdtemp() init = True Branch.__init__(self, base, init=init) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def clone(self): """ >>> orig = ScratchBranch(files=["file1", "file2"]) >>> clone = orig.clone() >>> os.path.samefile(orig.base, clone.base) False >>> os.path.isfile(os.path.join(clone.base, "file1")) True """ from shutil import copytree from tempfile import mkdtemp base = mkdtemp() os.rmdir(base) copytree(self.base, base, symlinks=True) return ScratchBranch(base=base) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" from shutil import rmtree try: if self.base: mutter("delete ScratchBranch %s" % self.base) rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re from binascii import hexlify from time import time # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time()), s)) M 644 inline bzrlib/commands.py data 53306 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import BzrError, BzrCheckError, BzrCommandError from bzrlib.branch import find_branch from bzrlib import BZRDIR plugin_cmds = {} def register_command(cmd): "Utility function to help register a command" global plugin_cmds k = cmd.__name__ if k.startswith("cmd_"): k_unsquished = _unsquish_command_name(k) else: k_unsquished = k if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = cmd else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r' % sys.modules[cmd.__module__]) def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _get_cmd_dict(plugins_override=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v # If we didn't load plugins, the plugin_cmds dict will be empty if plugins_override: d.update(plugin_cmds) else: d2 = plugin_cmds.copy() d2.update(d) d = d2 return d def get_all_cmds(plugins_override=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(plugins_override=plugins_override).iteritems(): yield k,v def get_cmd_class(cmd, plugins_override=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(plugins_override=plugins_override) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() for opt in self.takes_options: if not opt in OPTIONS: raise BzrError("Unknown option '%s' returned by external command %s" % (opt, path)) # TODO: Is there any way to check takes_args is valid here? self.takes_args = pipe.readline().split() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-usage'" % path) pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-help'" % path) def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: optname = name.replace('_','-') value = kargs[name] if OPTIONS.has_key(optname): # it's an option opts.append('--%s' % optname) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = find_branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): find_branch('.').get_revision(revision_id).write_xml(sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print find_branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): from bzrlib.add import smart_add smart_add(file_list, verbose, not no_recurse) class cmd_mkdir(Command): """Create a new versioned directory. This is equivalent to creating the directory and then adding it. """ takes_args = ['dir+'] def run(self, dir_list): b = None for d in dir_list: os.mkdir(d) if not b: b = find_branch(d) b.add([d], verbose=True) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print find_branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = find_branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = find_branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = find_branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import tempfile from shutil import rmtree import errno br_to = find_branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if e.errno != errno.ENOENT: raise if location is None: if stored_loc is None: raise BzrCommandError("No pull location known or specified.") else: print "Using last location: %s" % stored_loc location = stored_loc cache_root = tempfile.mkdtemp() from bzrlib.branch import DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: from branch import find_cached_branch, DivergedBranches br_from = find_cached_branch(location, cache_root) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged." " Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") finally: rmtree(cache_root) class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. To retrieve the branch as of a particular revision, supply the --revision parameter, as in "branch foo/bar -r 5". """ takes_args = ['from_location', 'to_location?'] takes_options = ['revision'] def run(self, from_location, to_location=None, revision=None): import errno from bzrlib.merge import merge from bzrlib.branch import DivergedBranches, NoSuchRevision, \ find_cached_branch, Branch from shutil import rmtree from meta_store import CachedStore import tempfile cache_root = tempfile.mkdtemp() try: try: br_from = find_cached_branch(from_location, cache_root) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not' ' exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location.rstrip("/\\")) try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already' ' exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) try: br_to.update_revisions(br_from, stop_revision=revision) except NoSuchRevision: rmtree(to_location) msg = "The branch %s has no revision %d." % (from_location, revision) raise BzrCommandError(msg) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False, ignore_zero=True) from_location = pull_loc(br_from) br_to.controlfile("x-pull", "wb").write(from_location + "\n") finally: rmtree(cache_root) def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = find_branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = find_branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = find_branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: raise BzrError("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = find_branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: raise BzrError("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in find_branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in find_branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): from bzrlib.branch import Branch Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = find_branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = find_branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = find_branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = find_branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib.branch import find_branch from bzrlib.log import log_formatter, show_log import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') lf = log_formatter('short', show_ids=show_ids, to_file=outf, show_timezone=timezone) show_log(b, lf, file_id, verbose=verbose, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = find_branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = find_branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): from bzrlib.osutils import quotefn for f in find_branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = find_branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = find_branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print find_branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, exports to a directory (equivalent to --format=dir).""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format'] def run(self, dest, revision=None, format='dir'): b = find_branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest, format) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = find_branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit from bzrlib.osutils import get_text_message ## Warning: shadows builtin file() if not message and not file: import cStringIO stdout = sys.stdout catcher = cStringIO.StringIO() sys.stdout = catcher cmd_status({"file_list":selected_list}, {}) info = catcher.getvalue() sys.stdout = stdout message = get_text_message(info) if message is None: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = find_branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.check import check check(find_branch(dir)) class cmd_upgrade(Command): """Upgrade branch storage to current format. This should normally be used only after the check command tells you to run it. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.upgrade import upgrade upgrade(find_branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest return int(not selftest()) class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Restore selected files from a previous revision. """ takes_args = ['file+'] def run(self, file_list): from bzrlib.branch import find_branch if not file_list: file_list = ['.'] b = find_branch(file_list[0]) b.revert([b.relpath(f) for f in file_list]) class cmd_merge_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = find_branch('.') statcache.update_cache(b.base, b.read_working_inventory()) class cmd_plugins(Command): """List plugins""" hidden = True def run(self): import bzrlib.plugin from pprint import pprint pprint(bzrlib.plugin.all_plugins) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'update': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) >>> parse_args('log -r 500'.split()) (['log'], {'revision': 500}) >>> parse_args('log -r500:600'.split()) (['log'], {'revision': [500, 600]}) >>> parse_args('log -vr500:600'.split()) (['log'], {'verbose': True, 'revision': [500, 600]}) >>> parse_args('log -rv500:600'.split()) #the r takes an argument Traceback (most recent call last): ... ValueError: invalid literal for int(): v500 """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: raise BzrError('unknown long option %r' % a) else: shortopt = a[1:] if shortopt in SHORT_OPTIONS: # Multi-character options must have a space to delimit # their value optname = SHORT_OPTIONS[shortopt] else: # Single character short options, can be chained, # and have their value appended to their name shortopt = a[1:2] if shortopt not in SHORT_OPTIONS: # We didn't find the multi-character name, and we # didn't find the single char name raise BzrError('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if a[2:]: # There are extra things on this option # see if it is the value, or if it is another # short option optargfn = OPTIONS[optname] if optargfn is None: # This option does not take an argument, so the # next entry is another short option, pack it back # into the list argv.insert(0, '-' + a[2:]) else: # This option takes an argument, so pack it # into the array optarg = a[2:] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? raise BzrError('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: raise BzrError('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: raise BzrError('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def _parse_master_args(argv): """Parse the arguments that always go with the original command. These are things like bzr --no-plugins, etc. There are now 2 types of option flags. Ones that come *before* the command, and ones that come *after* the command. Ones coming *before* the command are applied against all possible commands. And are generally applied before plugins are loaded. The current list are: --builtin Allow plugins to load, but don't let them override builtin commands, they will still be allowed if they do not override a builtin. --no-plugins Don't load any plugins. This lets you get back to official source behavior. --profile Enable the hotspot profile before running the command. For backwards compatibility, this is also a non-master option. --version Spit out the version of bzr that is running and exit. This is also a non-master option. --help Run help and exit, also a non-master option (I think that should stay, though) >>> argv, opts = _parse_master_args(['bzr', '--test']) Traceback (most recent call last): ... BzrCommandError: Invalid master option: 'test' >>> argv, opts = _parse_master_args(['bzr', '--version', 'command']) >>> print argv ['command'] >>> print opts['version'] True >>> argv, opts = _parse_master_args(['bzr', '--profile', 'command', '--more-options']) >>> print argv ['command', '--more-options'] >>> print opts['profile'] True >>> argv, opts = _parse_master_args(['bzr', '--no-plugins', 'command']) >>> print argv ['command'] >>> print opts['no-plugins'] True >>> print opts['profile'] False >>> argv, opts = _parse_master_args(['bzr', 'command', '--profile']) >>> print argv ['command', '--profile'] >>> print opts['profile'] False """ master_opts = {'builtin':False, 'no-plugins':False, 'version':False, 'profile':False, 'help':False } # This is the point where we could hook into argv[0] to determine # what front-end is supposed to be run # For now, we are just ignoring it. cmd_name = argv.pop(0) for arg in argv[:]: if arg[:2] != '--': # at the first non-option, we return the rest break arg = arg[2:] # Remove '--' if arg not in master_opts: # We could say that this is not an error, that we should # just let it be handled by the main section instead raise BzrCommandError('Invalid master option: %r' % arg) argv.pop(0) # We are consuming this entry master_opts[arg] = True return argv, master_opts def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: # some options like --builtin and --no-plugins have special effects argv, master_opts = _parse_master_args(argv) if not master_opts['no-plugins']: from bzrlib.plugin import load_plugins load_plugins() args, opts = parse_args(argv) if master_opts['help']: from bzrlib.help import help if argv: help(argv[0]) else: help() return 0 if 'help' in opts: from bzrlib.help import help if args: help(args[0]) else: help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 plugins_override = not (master_opts['builtin']) canonical_cmd, cmd_class = get_cmd_class(cmd, plugins_override=plugins_override) profile = master_opts['profile'] # For backwards compatibility, I would rather stick with --profile being a # master/global option if 'profile' in opts: profile = True del opts['profile'] # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): bzrlib.trace.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: import errno quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/inventory.py data 19295 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # This should really be an id randomly assigned when the tree is # created, but it's not for now. ROOT_ID = "TREE_ROOT" import sys, os.path, types, re import bzrlib from bzrlib.xml import XMLMixin, Element from bzrlib.errors import BzrError, BzrCheckError from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(XMLMixin): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') 'TREE_ROOT' >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID)) >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123')) Traceback (most recent call last): ... BzrError: inventory already contains entry with id {2323} >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123')) >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' TODO: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ # TODO: split InventoryEntry into subclasses for files, # directories, etc etc. text_sha1 = None text_size = None def __init__(self, file_id, name, kind, parent_id, text_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID) >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID) Traceback (most recent call last): BzrCheckError: InventoryEntry name 'src/hello.c' is invalid """ if '/' in name or '\\' in name: raise BzrCheckError('InventoryEntry name %r is invalid' % name) self.file_id = file_id self.name = name self.kind = kind self.text_id = text_id self.parent_id = parent_id if kind == 'directory': self.children = {} elif kind == 'file': pass else: raise BzrError("unhandled entry kind %r" % kind) def sorted_children(self): l = self.children.items() l.sort() return l def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.parent_id, text_id=self.text_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size # note that children are *not* copied; they're pulled across when # others are added return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size != None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1']: v = getattr(self, f) if v != None: e.set(f, v) # to be conservative, we don't externalize the root pointers # for now, leaving them as null in the xml form. in a future # version it will be implied by nested elements. if self.parent_id != ROOT_ID: assert isinstance(self.parent_id, basestring) e.set('parent_id', self.parent_id) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' ## original format inventories don't have a parent_id for ## nodes in the root directory, but it's cleaner to use one ## internally. parent_id = elt.get('parent_id') if parent_id == None: parent_id = ROOT_ID self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __eq__(self, other): if not isinstance(other, InventoryEntry): return NotImplemented return (self.file_id == other.file_id) \ and (self.name == other.name) \ and (self.text_sha1 == other.text_sha1) \ and (self.text_size == other.text_size) \ and (self.text_id == other.text_id) \ and (self.parent_id == other.parent_id) \ and (self.kind == other.kind) def __ne__(self, other): return not (self == other) def __hash__(self): raise ValueError('not hashable') class RootEntry(InventoryEntry): def __init__(self, file_id): self.file_id = file_id self.children = {} self.kind = 'root_directory' self.parent_id = None self.name = '' def __eq__(self, other): if not isinstance(other, RootEntry): return NotImplemented return (self.file_id == other.file_id) \ and (self.children == other.children) class Inventory(XMLMixin): """Inventory of versioned files in a tree. This describes which file_id is present at each point in the tree, and possibly the SHA-1 or other information about the file. Entries can be looked up either by path or by file_id. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted, other than through the Inventory API. >>> inv = Inventory() >>> inv.write_xml(sys.stdout) >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID)) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] >>> inv.write_xml(sys.stdout) """ def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. The inventory is created with a default root directory, with an id of None. """ self.root = RootEntry(ROOT_ID) self._byid = {self.root.file_id: self.root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, from_dir=None): """Return (path, entry) pairs, in order by name.""" if from_dir == None: assert self.root from_dir = self.root elif isinstance(from_dir, basestring): from_dir = self._byid[from_dir] kids = from_dir.children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(from_dir=ie.file_id): yield os.path.join(name, cn), cie def entries(self): """Return list of (path, ie) for all entries except the root. This may be faster than iter_entries. """ accum = [] def descend(dir_ie, dir_path): kids = dir_ie.children.items() kids.sort() for name, ie in kids: child_path = os.path.join(dir_path, name) accum.append((child_path, ie)) if ie.kind == 'directory': descend(ie, child_path) descend(self.root, '') return accum def directories(self): """Return (path, entry) pairs for all directories, including the root. """ accum = [] def descend(parent_ie, parent_path): accum.append((parent_path, parent_ie)) kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory'] kids.sort() for name, child_ie in kids: child_path = os.path.join(parent_path, name) descend(child_ie, child_path) descend(self.root, '') return accum def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID)) >>> inv['123123'].name 'hello.c' """ try: return self._byid[file_id] except KeyError: if file_id == None: raise BzrError("can't look up file_id None") else: raise BzrError("file_id {%s} not in inventory" % file_id) def get_file_kind(self, file_id): return self._byid[file_id].kind def get_child(self, parent_id, filename): return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self._byid: raise BzrError("inventory already contains entry with id {%s}" % entry.file_id) try: parent = self._byid[entry.parent_id] except KeyError: raise BzrError("parent_id {%s} not in inventory" % entry.parent_id) if parent.children.has_key(entry.name): raise BzrError("%s is already versioned" % appendpath(self.id2path(parent.file_id), entry.name)) self._byid[entry.file_id] = entry parent.children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" from bzrlib.errors import NotVersionedError parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: raise BzrError("cannot re-add root of inventory") if file_id == None: from bzrlib.branch import gen_file_id file_id = gen_file_id(relpath) parent_path = parts[:-1] parent_id = self.path2id(parent_path) if parent_id == None: raise NotVersionedError(parent_path) ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def to_element(self): """Convert to XML Element""" e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID)) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __eq__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 True """ if not isinstance(other, Inventory): return NotImplemented if len(self._byid) != len(other._byid): # shortcut: obviously not the same return False return self._byid == other._byid def __ne__(self, other): return not (self == other) def __hash__(self): raise ValueError('not hashable') def get_idpath(self, file_id): """Return a list of file_ids for the path to an entry. The list contains one element for each directory followed by the id of the file itself. So the length of the returned list is equal to the depth of the file in the tree, counting the root directory as depth 1. """ p = [] while file_id != None: try: ie = self._byid[file_id] except KeyError: raise BzrError("file_id {%s} not found in inventory" % file_id) p.insert(0, ie.file_id) file_id = ie.parent_id return p def id2path(self, file_id): """Return as a list the path to file_id.""" # get all names, skipping root p = [self[fid].name for fid in self.get_idpath(file_id)[1:]] return os.sep.join(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. Returns None iff the path is not found. """ if isinstance(name, types.StringTypes): name = splitpath(name) mutter("lookup path %r" % name) parent = self.root for f in name: try: cie = parent.children[f] assert cie.name == f assert cie.parent_id == parent.file_id parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): return self._byid.has_key(file_id) def rename(self, file_id, new_parent_id, new_name): """Move a file within the inventory. This can change either the name, or the parent, or both. This does not move the working file.""" if not is_valid_name(new_name): raise BzrError("not an acceptable filename: %r" % new_name) new_parent = self._byid[new_parent_id] if new_name in new_parent.children: raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id))) new_parent_idpath = self.get_idpath(new_parent_id) if file_id in new_parent_idpath: raise BzrError("cannot move directory %r into a subdirectory of itself, %r" % (self.id2path(file_id), self.id2path(new_parent_id))) file_ie = self._byid[file_id] old_parent = self._byid[file_ie.parent_id] # TODO: Don't leave things messed up if this fails del old_parent.children[file_ie.name] new_parent.children[new_name] = file_ie file_ie.name = new_name file_ie.parent_id = new_parent_id _NAME_RE = re.compile(r'^[^/\\]+$') def is_valid_name(name): return bool(_NAME_RE.match(name)) M 644 inline bzrlib/newinventory.py data 4463 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from xml import ElementTree, Element def write_inventory(inv, f): el = Element('inventory', {'version': '2'}) el.text = '\n' root = Element('root_directory', {'id': inv.root.file_id}) root.tail = root.text = '\n' el.append(root) def descend(parent_el, ie): kind = ie.kind el = Element(kind, {'name': ie.name, 'id': ie.file_id,}) if kind == 'file': if ie.text_id: el.set('text_id', ie.text_id) if ie.text_sha1: el.set('text_sha1', ie.text_sha1) if ie.text_size != None: el.set('text_size', ('%d' % ie.text_size)) elif kind != 'directory': raise BzrError('unknown InventoryEntry kind %r' % kind) el.tail = '\n' parent_el.append(el) if kind == 'directory': el.text = '\n' # break before having children l = ie.children.items() l.sort() for child_name, child_ie in l: descend(el, child_ie) # walk down through inventory, adding all directories l = inv.root.children.items() l.sort() for entry_name, ie in l: descend(root, ie) ElementTree(el).write(f, 'utf-8') f.write('\n') def escape_attr(text): return text.replace("&", "&") \ .replace("'", "'") \ .replace('"', """) \ .replace("<", "<") \ .replace(">", ">") # This writes out an inventory without building an XML tree first, # just to see if it's faster. Not currently used. def write_slacker_inventory(inv, f): def descend(ie): kind = ie.kind f.write('<%s name="%s" id="%s" ' % (kind, escape_attr(ie.name), escape_attr(ie.file_id))) if kind == 'file': if ie.text_id: f.write('text_id="%s" ' % ie.text_id) if ie.text_sha1: f.write('text_sha1="%s" ' % ie.text_sha1) if ie.text_size != None: f.write('text_size="%d" ' % ie.text_size) f.write('/>\n') elif kind == 'directory': f.write('>\n') l = ie.children.items() l.sort() for child_name, child_ie in l: descend(child_ie) f.write('\n') else: raise BzrError('unknown InventoryEntry kind %r' % kind) f.write('\n') f.write('\n' % escape_attr(inv.root.file_id)) l = inv.root.children.items() l.sort() for entry_name, ie in l: descend(ie) f.write('\n') f.write('\n') def read_new_inventory(f): from inventory import Inventory, InventoryEntry def descend(parent_ie, el): kind = el.tag name = el.get('name') file_id = el.get('id') ie = InventoryEntry(file_id, name, el.tag) parent_ie.children[name] = ie inv._byid[file_id] = ie if kind == 'directory': for child_el in el: descend(ie, child_el) elif kind == 'file': assert len(el) == 0 ie.text_id = el.get('text_id') v = el.get('text_size') ie.text_size = v and int(v) ie.text_sha1 = el.get('text_sha1') else: raise BzrError("unknown inventory entry %r" % kind) inv_el = ElementTree().parse(f) assert inv_el.tag == 'inventory' root_el = inv_el[0] assert root_el.tag == 'root_directory' inv = Inventory() for el in root_el: descend(inv.root, el) M 644 inline bzrlib/plugin.py data 4799 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # This module implements plug-in support. # Any python module in $BZR_PLUGIN_PATH will be imported upon initialization # of bzrlib (and then forgotten about). In the plugin's main body, it should # update any bzrlib registries it wants to extend; for example, to add new # commands, import bzrlib.commands and add your new command to the # plugin_cmds variable. import os from bzrlib.osutils import config_dir DEFAULT_PLUGIN_PATH = os.path.join(config_dir(), 'plugins') all_plugins = [] _loaded = False def load_plugins(): """ Find all python plugins and load them. Loading a plugin means importing it into the python interpreter. The plugin is expected to make calls to register commands when it's loaded (or perhaps access other hooks in future.) A list of plugs is stored in bzrlib.plugin.all_plugins for future reference. The environment variable BZR_PLUGIN_PATH is considered a delimited set of paths to look through. Each entry is searched for *.py files (and whatever other extensions are used in the platform, such as *.pyd). """ global all_plugins, _loaded if _loaded: # People can make sure plugins are loaded, they just won't be twice return #raise BzrError("plugins already initialized") _loaded = True import sys, os, imp try: set except NameError: from sets import Set as set # python2.3 from bzrlib.trace import log_error, mutter, log_exception from bzrlib.errors import BzrError bzrpath = os.environ.get('BZR_PLUGIN_PATH', DEFAULT_PLUGIN_PATH) # The problem with imp.get_suffixes() is that it doesn't include # .pyo which is technically valid # It also means that "testmodule.so" will show up as both test and testmodule # though it is only valid as 'test' # but you should be careful, because "testmodule.py" loads as testmodule. suffixes = imp.get_suffixes() suffixes.append(('.pyo', 'rb', imp.PY_COMPILED)) package_entries = ['__init__.py', '__init__.pyc', '__init__.pyo'] for d in bzrpath.split(os.pathsep): # going through them one by one allows different plugins with the same # filename in different directories in the path mutter('looking for plugins in %s' % d) if not d: continue plugin_names = set() if not os.path.isdir(d): continue for f in os.listdir(d): path = os.path.join(d, f) if os.path.isdir(path): for entry in package_entries: # This directory should be a package, and thus added to # the list if os.path.isfile(os.path.join(path, entry)): break else: # This directory is not a package continue else: for suffix_info in suffixes: if f.endswith(suffix_info[0]): f = f[:-len(suffix_info[0])] if suffix_info[2] == imp.C_EXTENSION and f.endswith('module'): f = f[:-len('module')] break else: continue mutter('add plugin name %s' % f) plugin_names.add(f) plugin_names = list(plugin_names) plugin_names.sort() for name in plugin_names: try: plugin_info = imp.find_module(name, [d]) mutter('load plugin %r' % (plugin_info,)) try: plugin = imp.load_module('bzrlib.plugin.' + name, *plugin_info) all_plugins.append(plugin_info) finally: if plugin_info[0] is not None: plugin_info[0].close() except Exception, e: log_error('Unable to load plugin %r from %r' % (name, d)) log_error(str(e)) log_exception() M 644 inline bzrlib/revision.py data 5995 # (C) 2005 Canonical # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from xml import XMLMixin, Element, SubElement from errors import BzrError class RevisionReference: """ Reference to a stored revision. Includes the revision_id and revision_sha1. """ revision_id = None revision_sha1 = None def __init__(self, revision_id, revision_sha1): if revision_id == None \ or isinstance(revision_id, basestring): self.revision_id = revision_id else: raise ValueError('bad revision_id %r' % revision_id) if revision_sha1 != None: if isinstance(revision_sha1, basestring) \ and len(revision_sha1) == 40: self.revision_sha1 = revision_sha1 else: raise ValueError('bad revision_sha1 %r' % revision_sha1) class Revision(XMLMixin): """Single revision on a branch. Revisions may know their revision_hash, but only once they've been written out. This is not stored because you cannot write the hash into the file it describes. After bzr 0.0.5 revisions are allowed to have multiple parents. To support old clients this is written out in a slightly redundant form: the first parent as the predecessor. This will eventually be dropped. parents List of parent revisions, each is a RevisionReference. """ inventory_id = None inventory_sha1 = None revision_id = None timestamp = None message = None timezone = None committer = None def __init__(self, **args): self.__dict__.update(args) self.parents = [] def _get_precursor(self): from warnings import warn warn("Revision.precursor is deprecated", stacklevel=2) if self.parents: return self.parents[0].revision_id else: return None def _get_precursor_sha1(self): from warnings import warn warn("Revision.precursor_sha1 is deprecated", stacklevel=2) if self.parents: return self.parents[0].revision_sha1 else: return None def _fail(self): raise Exception("can't assign to precursor anymore") precursor = property(_get_precursor, _fail, _fail) precursor_sha1 = property(_get_precursor_sha1, _fail, _fail) def __repr__(self): return "" % self.revision_id def to_element(self): root = Element('revision', committer = self.committer, timestamp = '%.9f' % self.timestamp, revision_id = self.revision_id, inventory_id = self.inventory_id, inventory_sha1 = self.inventory_sha1, ) if self.timezone: root.set('timezone', str(self.timezone)) root.text = '\n' msg = SubElement(root, 'message') msg.text = self.message msg.tail = '\n' if self.parents: # first parent stored as precursor for compatability with 0.0.5 and # earlier pr = self.parents[0] assert pr.revision_id root.set('precursor', pr.revision_id) if pr.revision_sha1: root.set('precursor_sha1', pr.revision_sha1) if self.parents: pelts = SubElement(root, 'parents') pelts.tail = pelts.text = '\n' for rr in self.parents: assert isinstance(rr, RevisionReference) p = SubElement(pelts, 'revision_ref') p.tail = '\n' assert rr.revision_id p.set('revision_id', rr.revision_id) if rr.revision_sha1: p.set('revision_sha1', rr.revision_sha1) return root def from_element(cls, elt): return unpack_revision(elt) from_element = classmethod(from_element) def unpack_revision(elt): """Convert XML element into Revision object.""" # is deprecated... if elt.tag not in ('revision', 'changeset'): raise BzrError("unexpected tag in revision file: %r" % elt) rev = Revision(committer = elt.get('committer'), timestamp = float(elt.get('timestamp')), revision_id = elt.get('revision_id'), inventory_id = elt.get('inventory_id'), inventory_sha1 = elt.get('inventory_sha1') ) precursor = elt.get('precursor') precursor_sha1 = elt.get('precursor_sha1') pelts = elt.find('parents') if precursor: # revisions written prior to 0.0.5 have a single precursor # give as an attribute rev_ref = RevisionReference(precursor, precursor_sha1) rev.parents.append(rev_ref) elif pelts: for p in pelts: assert p.tag == 'revision_ref', \ "bad parent node tag %r" % p.tag rev_ref = RevisionReference(p.get('revision_id'), p.get('revision_sha1')) rev.parents.append(rev_ref) v = elt.get('timezone') rev.timezone = v and int(v) rev.message = elt.findtext('message') # text of return rev M 644 inline bzrlib/selftest/__init__.py data 9010 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") raise class CommandFailed(Exception): pass class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr, so that we can run it # through a specified Python intepreter OVERRIDE_PYTHON = None # to run with alternative python 'python' BZRPATH = 'bzr' _log_buf = "" def formcmd(self, cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = self.BZRPATH if self.OVERRIDE_PYTHON: cmd.insert(0, self.OVERRIDE_PYTHON) self.log('$ %r' % cmd) return cmd def runcmd(self, cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = self.formcmd(cmd) self.log('$ ' + ' '.join(cmd)) actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(self, cmd, retcode=0): """Run a command and return its output""" cmd = self.formcmd(cmd) child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG) outd, errd = child.communicate() self.log(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def build_tree(self, shape): """Build a test tree according to a pattern. shape is a sequence of file specifications. If the final character is '/', a directory is created. This doesn't add anything to a branch. """ # XXX: It's OK to just create them using forward slashes on windows? import os for name in shape: assert isinstance(name, basestring) if name[-1] == '/': os.mkdir(name[:-1]) else: f = file(name, 'wt') print >>f, "contents of", name f.close() def log(self, msg): """Log a message to a progress file""" self._log_buf = self._log_buf + str(msg) + '\n' print >>self.TEST_LOG, msg def check_inventory_shape(self, inv, shape): """ Compare an inventory to a list of expected names. Fail if they are not precisely equal. """ extras = [] shape = list(shape) # copy for path, ie in inv.entries(): name = path.replace('\\', '/') if ie.kind == 'dir': name = name + '/' if name in shape: shape.remove(name) else: extras.append(name) if shape: self.fail("expected paths not found in inventory: %r" % shape) if extras: self.fail("unexpected paths found in inventory: %r" % extras) def check_file_contents(self, filename, expect): self.log("check contents of file %s" % filename) contents = file(filename, 'r').read() if contents != expect: self.log("expected: %r" % expected) self.log("actually: %r" % contents) self.fail("contents of %s not as expected") class InTempDir(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.test_dir = os.path.join(self.TEST_ROOT, self.__class__.__name__) os.mkdir(self.test_dir) os.chdir(self.test_dir) def tearDown(self): import os os.chdir(self.TEST_ROOT) class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ def __init__(self, out): self.out = out TestResult.__init__(self) def startTest(self, test): # TODO: Maybe show test.shortDescription somewhere? print >>self.out, '%-60.60s' % test.id(), self.out.flush() TestResult.startTest(self, test) def stopTest(self, test): # print TestResult.stopTest(self, test) def addError(self, test, err): print >>self.out, 'ERROR' TestResult.addError(self, test, err) _show_test_failure('error', test, err, self.out) def addFailure(self, test, err): print >>self.out, 'FAILURE' TestResult.addFailure(self, test, err) _show_test_failure('failure', test, err, self.out) def addSuccess(self, test): print >>self.out, 'OK' TestResult.addSuccess(self, test) def selftest(): from unittest import TestLoader, TestSuite import bzrlib, bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, bzrlib.commands import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning from doctest import DocTestSuite import os import shutil import time import sys TestBase.BZRPATH = os.path.join(os.path.realpath(os.path.dirname(bzrlib.__path__[0])), 'bzr') print '%-30s %s' % ('bzr binary', TestBase.BZRPATH) _setup_test_log() _setup_test_dir() print suite = TestSuite() tl = TestLoader() for m in bzrlib.selftest.whitebox, \ bzrlib.selftest.versioning: suite.addTest(tl.loadTestsFromModule(m)) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands: suite.addTest(DocTestSuite(m)) suite.addTest(bzrlib.selftest.blackbox.suite()) # save stdout & stderr so there's no leakage from code-under-test real_stdout = sys.stdout real_stderr = sys.stderr sys.stdout = sys.stderr = TestBase.TEST_LOG try: result = _MyResult(real_stdout) suite.run(result) finally: sys.stdout = real_stdout sys.stderr = real_stderr _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os log_filename = os.path.abspath('testbzr.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "bzr tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_ROOT = os.path.abspath("testbzr.tmp") print '%-30s %s' % ('running tests in', TestBase.TEST_ROOT) if os.path.exists(TestBase.TEST_ROOT): shutil.rmtree(TestBase.TEST_ROOT) os.mkdir(TestBase.TEST_ROOT) os.chdir(TestBase.TEST_ROOT) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_ROOT, '.bzr')) def _show_results(result): print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, exc_info, out): from traceback import print_exception print >>out, '-' * 60 print >>out, case desc = case.shortDescription() if desc: print >>out, ' (%s)' % desc print_exception(exc_info[0], exc_info[1], exc_info[2], None, out) if isinstance(case, TestBase): print >>out print >>out, 'log from this test:' print >>out, case._log_buf print >>out, '-' * 60 M 644 inline bzrlib/tree.py data 10178 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tree classes, representing directory at point in time. """ from osutils import pumpfile, appendpath, fingerprint_file from bzrlib.trace import mutter, note from bzrlib.errors import BzrError import bzrlib exporters = {} class Tree(object): """Abstract file tree. There are several subclasses: * `WorkingTree` exists as files on disk editable by the user. * `RevisionTree` is a tree as recorded at some point in the past. * `EmptyTree` Trees contain an `Inventory` object, and also know how to retrieve file texts mentioned in the inventory, either from a working directory or from a store. It is possible for trees to contain files that are not described in their inventory or vice versa; for this use `filenames()`. Trees can be compared, etc, regardless of whether they are working trees or versioned trees. """ def has_filename(self, filename): """True if the tree has given filename.""" raise NotImplementedError() def has_id(self, file_id): return self.inventory.has_id(file_id) __contains__ = has_id def __iter__(self): return iter(self.inventory) def id2path(self, file_id): return self.inventory.id2path(file_id) def _get_inventory(self): return self._inventory inventory = property(_get_inventory, doc="Inventory of this Tree") def _check_retrieved(self, ie, f): fp = fingerprint_file(f) f.seek(0) if ie.text_size != None: if ie.text_size != fp['size']: raise BzrError("mismatched size for file %r in %r" % (ie.file_id, self._store), ["inventory expects %d bytes" % ie.text_size, "file is actually %d bytes" % fp['size'], "store is probably damaged/corrupt"]) if ie.text_sha1 != fp['sha1']: raise BzrError("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store), ["inventory expects %s" % ie.text_sha1, "file is actually %s" % fp['sha1'], "store is probably damaged/corrupt"]) def print_file(self, fileid): """Print file with id `fileid` to stdout.""" import sys pumpfile(self.get_file(fileid), sys.stdout) def export(self, dest, format='dir'): """Export this tree.""" try: exporter = exporters[format] except KeyError: raise BzrCommandError("export format %r not supported" % format) exporter(self, dest) class RevisionTree(Tree): """Tree viewing a previous revision. File text can be retrieved from the text store. TODO: Some kind of `__repr__` method, but a good one probably means knowing the branch and revision number, or at least passing a description to the constructor. """ def __init__(self, store, inv): self._store = store self._inventory = inv def get_file(self, file_id): ie = self._inventory[file_id] f = self._store[ie.text_id] mutter(" get fileid{%s} from %r" % (file_id, self)) self._check_retrieved(ie, f) return f def get_file_size(self, file_id): return self._inventory[file_id].text_size def get_file_sha1(self, file_id): ie = self._inventory[file_id] return ie.text_sha1 def has_filename(self, filename): return bool(self.inventory.path2id(filename)) def list_files(self): # The only files returned by this are those from the version for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id class EmptyTree(Tree): def __init__(self): from bzrlib.inventory import Inventory self._inventory = Inventory() def has_filename(self, filename): return False def list_files(self): if False: # just to make it a generator yield None ###################################################################### # diff # TODO: Merge these two functions into a single one that can operate # on either a whole tree or a set of files. # TODO: Return the diff in order by filename, not by category or in # random order. Can probably be done by lock-stepping through the # filenames from both trees. def file_status(filename, old_tree, new_tree): """Return single-letter status, old and new names for a file. The complexity here is in deciding how to represent renames; many complex cases are possible. """ old_inv = old_tree.inventory new_inv = new_tree.inventory new_id = new_inv.path2id(filename) old_id = old_inv.path2id(filename) if not new_id and not old_id: # easy: doesn't exist in either; not versioned at all if new_tree.is_ignored(filename): return 'I', None, None else: return '?', None, None elif new_id: # There is now a file of this name, great. pass else: # There is no longer a file of this name, but we can describe # what happened to the file that used to have # this name. There are two possibilities: either it was # deleted entirely, or renamed. assert old_id if new_inv.has_id(old_id): return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id) else: return 'D', old_inv.id2path(old_id), None # if the file_id is new in this revision, it is added if new_id and not old_inv.has_id(new_id): return 'A' # if there used to be a file of this name, but that ID has now # disappeared, it is deleted if old_id and not new_inv.has_id(old_id): return 'D' return 'wtf?' def find_renames(old_inv, new_inv): for file_id in old_inv: if file_id not in new_inv: continue old_name = old_inv.id2path(file_id) new_name = new_inv.id2path(file_id) if old_name != new_name: yield (old_name, new_name) ###################################################################### # export def dir_exporter(tree, dest): """Export this tree to a new directory. `dest` should not exist, and will be created holding the contents of this tree. TODO: To handle subdirectories we need to create the directories first. :note: If the export fails, the destination directory will be left in a half-assed state. """ import os os.mkdir(dest) mutter('export version %r' % tree) inv = tree.inventory for dp, ie in inv.iter_entries(): kind = ie.kind fullpath = appendpath(dest, dp) if kind == 'directory': os.mkdir(fullpath) elif kind == 'file': pumpfile(tree.get_file(ie.file_id), file(fullpath, 'wb')) else: raise BzrError("don't know how to export {%s} of kind %r" % (ie.file_id, kind)) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) exporters['dir'] = dir_exporter try: import tarfile except ImportError: pass else: def tar_exporter(tree, dest, compression=None): """Export this tree to a new tar file. `dest` will be created holding the contents of this tree; if it already exists, it will be clobbered, like with "tar -c". """ from time import time now = time() compression = str(compression or '') try: ball = tarfile.open(dest, 'w:' + compression) except tarfile.CompressionError, e: raise BzrError(str(e)) mutter('export version %r' % tree) inv = tree.inventory for dp, ie in inv.iter_entries(): mutter(" export {%s} kind %s to %s" % (ie.file_id, ie.kind, dest)) item = tarfile.TarInfo(dp) # TODO: would be cool to actually set it to the timestamp of the # revision it was last changed item.mtime = now if ie.kind == 'directory': item.type = tarfile.DIRTYPE fileobj = None item.name += '/' item.size = 0 item.mode = 0755 elif ie.kind == 'file': item.type = tarfile.REGTYPE fileobj = tree.get_file(ie.file_id) item.size = _find_file_size(fileobj) item.mode = 0644 else: raise BzrError("don't know how to export {%s} of kind %r" % (ie.file_id, ie.kind)) ball.addfile(item, fileobj) ball.close() exporters['tar'] = tar_exporter def tgz_exporter(tree, dest): tar_exporter(tree, dest, compression='gz') exporters['tgz'] = tgz_exporter def tbz_exporter(tree, dest): tar_exporter(tree, dest, compression='bz2') exporters['tbz2'] = tbz_exporter def _find_file_size(fileobj): offset = fileobj.tell() try: fileobj.seek(0, 2) size = fileobj.tell() except TypeError: # gzip doesn't accept second argument to seek() fileobj.seek(0) size = 0 while True: nread = len(fileobj.read()) if nread == 0: break size += nread fileobj.seek(offset) return size M 644 inline bzrlib/xml.py data 2352 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """XML externalization support.""" # "XML is like violence: if it doesn't solve your problem, you aren't # using enough of it." -- various __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " _ElementTree = None def ElementTree(*args, **kwargs): global _ElementTree if _ElementTree is None: try: from cElementTree import ElementTree except ImportError: from elementtree.ElementTree import ElementTree _ElementTree = ElementTree return _ElementTree(*args, **kwargs) _Element = None def Element(*args, **kwargs): global _Element if _Element is None: try: from cElementTree import Element except ImportError: from elementtree.ElementTree import Element _Element = Element return _Element(*args, **kwargs) _SubElement = None def SubElement(*args, **kwargs): global _SubElement if _SubElement is None: try: from cElementTree import SubElement except ImportError: from elementtree.ElementTree import SubElement _SubElement = SubElement return _SubElement(*args, **kwargs) import os, time from trace import mutter class XMLMixin: def to_element(self): raise Exception("XMLMixin.to_element must be overridden in concrete classes") def write_xml(self, f): ElementTree(self.to_element()).write(f, 'utf-8') f.write('\n') def read_xml(cls, f): return cls.from_element(ElementTree().parse(f)) read_xml = classmethod(read_xml) commit refs/heads/master mark :801 committer Martin Pool 1119927999 +1000 data 41 - remove unneeded imports from bzrlib.xml from :800 M 644 inline bzrlib/xml.py data 2310 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """XML externalization support.""" # "XML is like violence: if it doesn't solve your problem, you aren't # using enough of it." -- various __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " _ElementTree = None def ElementTree(*args, **kwargs): global _ElementTree if _ElementTree is None: try: from cElementTree import ElementTree except ImportError: from elementtree.ElementTree import ElementTree _ElementTree = ElementTree return _ElementTree(*args, **kwargs) _Element = None def Element(*args, **kwargs): global _Element if _Element is None: try: from cElementTree import Element except ImportError: from elementtree.ElementTree import Element _Element = Element return _Element(*args, **kwargs) _SubElement = None def SubElement(*args, **kwargs): global _SubElement if _SubElement is None: try: from cElementTree import SubElement except ImportError: from elementtree.ElementTree import SubElement _SubElement = SubElement return _SubElement(*args, **kwargs) class XMLMixin: def to_element(self): raise Exception("XMLMixin.to_element must be overridden in concrete classes") def write_xml(self, f): ElementTree(self.to_element()).write(f, 'utf-8') f.write('\n') def read_xml(cls, f): return cls.from_element(ElementTree().parse(f)) read_xml = classmethod(read_xml) commit refs/heads/master mark :802 committer Martin Pool 1119936820 +1000 data 159 - Remove XMLMixin class in favour of simple pack_xml, unpack_xml functions called as needed. - Avoid importing xml and ElementTree library unless needed. from :801 M 644 inline bzrlib/branch.py data 38929 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, splitpath, \ sha_file, appendpath, file_kind from bzrlib.errors import BzrError BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def find_cached_branch(f, cache_root, **args): from remotebranch import RemoteBranch br = find_branch(f, **args) def cacheify(br, store_name): from meta_store import CachedStore cache_path = os.path.join(cache_root, store_name) os.mkdir(cache_path) new_store = CachedStore(getattr(br, store_name), cache_path) setattr(br, store_name, new_store) if isinstance(br, RemoteBranch): cacheify(br, 'inventory_store') cacheify(br, 'text_store') cacheify(br, 'revision_store') return br def _relpath(base, path): """Return path relative to base, or raise exception. The path may be either an absolute path or a path relative to the current working directory. Lifted out of Branch.relpath for ease of testing. os.path.commonprefix (python2.4) has a bad bug that it works just on string prefixes, assuming that '/u' is a prefix of '/u2'. This avoids that problem.""" rp = os.path.abspath(path) s = [] head = rp while len(head) >= len(base): if head == base: break head, tail = os.path.split(head) if tail: s.insert(0, tail) else: from errors import NotBranchError raise NotBranchError("path %r is not within branch %r" % (rp, base)) return os.sep.join(s) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head class DivergedBranches(Exception): def __init__(self, branch1, branch2): self.branch1 = branch1 self.branch2 = branch2 Exception.__init__(self, "These branches have diverged.") class NoSuchRevision(BzrError): def __init__(self, branch, revision): self.branch = branch self.revision = revision msg = "Branch %s has no revision %d" % (branch, revision) BzrError.__init__(self, msg) ###################################################################### # branch objects class Branch(object): """Branch holding a history of revisions. base Base directory of the branch. _lock_mode None, or 'r' or 'w' _lock_count If _lock_mode is true, a positive count of the number of times the lock has been taken. _lock Lock object from bzrlib.lock. """ base = None _lock_mode = None _lock_count = None _lock = None def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ from bzrlib.store import ImmutableStore if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): from errors import NotBranchError raise NotBranchError("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def __del__(self): if self._lock_mode or self._lock: from warnings import warn warn("branch %r was not explicitly unlocked" % self) self._lock.unlock() def lock_write(self): if self._lock_mode: if self._lock_mode != 'w': from errors import LockError raise LockError("can't upgrade to a write lock from %r" % self._lock_mode) self._lock_count += 1 else: from bzrlib.lock import WriteLock self._lock = WriteLock(self.controlfilename('branch-lock')) self._lock_mode = 'w' self._lock_count = 1 def lock_read(self): if self._lock_mode: assert self._lock_mode in ('r', 'w'), \ "invalid lock mode %r" % self._lock_mode self._lock_count += 1 else: from bzrlib.lock import ReadLock self._lock = ReadLock(self.controlfilename('branch-lock')) self._lock_mode = 'r' self._lock_count = 1 def unlock(self): if not self._lock_mode: from errors import LockError raise LockError('branch %r is not locked' % (self)) if self._lock_count > 1: self._lock_count -= 1 else: self._lock.unlock() self._lock = None self._lock_mode = self._lock_count = None def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" return _relpath(self.base, path) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, basestring): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): from bzrlib.inventory import Inventory from bzrlib.xml import pack_xml os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.\n") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) pack_xml(Inventory(), self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: raise BzrError('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" from bzrlib.inventory import Inventory from bzrlib.xml import unpack_xml from time import time before = time() self.lock_read() try: # ElementTree does its own conversion from UTF-8, so open in # binary. inv = unpack_xml(Inventory, self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time() - before)) return inv finally: self.unlock() def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ from bzrlib.atomicfile import AtomicFile from bzrlib.xml import pack_xml self.lock_write() try: f = AtomicFile(self.controlfilename('inventory'), 'wb') try: pack_xml(inv, f) f.commit() finally: f.close() finally: self.unlock() mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. files List of paths to add, relative to the base of the tree. ids If set, use these instead of automatically generated ids. Must be the same length as the list of files, but may contain None for ids that are to be autogenerated. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ from bzrlib.textui import show_status # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, basestring): assert(ids is None or isinstance(ids, basestring)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) self.lock_write() try: inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): raise BzrError("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: raise BzrError("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: print 'added', quotefn(f) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) finally: self.unlock() def print_file(self, file, revno): """Print `file` to stdout.""" self.lock_read() try: tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: raise BzrError("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) finally: self.unlock() def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ from bzrlib.textui import show_status ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, basestring): files = [files] self.lock_write() try: tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: raise BzrError("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) finally: self.unlock() # FIXME: this doesn't need to be a branch method def set_inventory(self, new_inventory_list): from bzrlib.inventory import Inventory, InventoryEntry inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): from bzrlib.atomicfile import AtomicFile mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() + [revision_id] f = AtomicFile(self.controlfilename('revision-history')) try: for rev_id in rev_history: print >>f, rev_id f.commit() finally: f.close() def get_revision(self, revision_id): """Return the Revision object for a named revision""" from bzrlib.revision import Revision from bzrlib.xml import unpack_xml self.lock_read() try: if not revision_id or not isinstance(revision_id, basestring): raise ValueError('invalid revision-id: %r' % revision_id) r = unpack_xml(Revision, self.revision_store[revision_id]) finally: self.unlock() assert r.revision_id == revision_id return r def get_revision_sha1(self, revision_id): """Hash the stored value of a revision, and return it.""" # In the future, revision entries will be signed. At that # point, it is probably best *not* to include the signature # in the revision hash. Because that lets you re-sign # the revision, (add signatures/remove signatures) and still # have all hash pointers stay consistent. # But for now, just hash the contents. return sha_file(self.revision_store[revision_id]) def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" from bzrlib.inventory import Inventory from bzrlib.xml import unpack_xml return unpack_xml(Inventory, self.inventory_store[inventory_id]) def get_inventory_sha1(self, inventory_id): """Return the sha1 hash of the inventory entry """ return sha_file(self.inventory_store[inventory_id]) def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: from bzrlib.inventory import Inventory return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self.lock_read() try: return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] finally: self.unlock() def common_ancestor(self, other, self_revno=None, other_revno=None): """ >>> import commit >>> sb = ScratchBranch(files=['foo', 'foo~']) >>> sb.common_ancestor(sb) == (None, None) True >>> commit.commit(sb, "Committing first revision", verbose=False) >>> sb.common_ancestor(sb)[0] 1 >>> clone = sb.clone() >>> commit.commit(sb, "Committing second revision", verbose=False) >>> sb.common_ancestor(sb)[0] 2 >>> sb.common_ancestor(clone)[0] 1 >>> commit.commit(clone, "Committing divergent second revision", ... verbose=False) >>> sb.common_ancestor(clone)[0] 1 >>> sb.common_ancestor(clone) == clone.common_ancestor(sb) True >>> sb.common_ancestor(sb) != clone.common_ancestor(clone) True >>> clone2 = sb.clone() >>> sb.common_ancestor(clone2)[0] 2 >>> sb.common_ancestor(clone2, self_revno=1)[0] 1 >>> sb.common_ancestor(clone2, other_revno=1)[0] 1 """ my_history = self.revision_history() other_history = other.revision_history() if self_revno is None: self_revno = len(my_history) if other_revno is None: other_revno = len(other_history) indices = range(min((self_revno, other_revno))) indices.reverse() for r in indices: if my_history[r] == other_history[r]: return r+1, my_history[r] return None, None def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def missing_revisions(self, other, stop_revision=None): """ If self and other have not diverged, return a list of the revisions present in other, but missing from self. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch() >>> br2 = ScratchBranch() >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [u'REVISION-ID-1'] >>> br2.missing_revisions(br1) [] >>> commit(br1, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-2A") >>> br1.missing_revisions(br2) [u'REVISION-ID-2A'] >>> commit(br1, "lala!", rev_id="REVISION-ID-2B") >>> br1.missing_revisions(br2) Traceback (most recent call last): DivergedBranches: These branches have diverged. """ self_history = self.revision_history() self_len = len(self_history) other_history = other.revision_history() other_len = len(other_history) common_index = min(self_len, other_len) -1 if common_index >= 0 and \ self_history[common_index] != other_history[common_index]: raise DivergedBranches(self, other) if stop_revision is None: stop_revision = other_len elif stop_revision > other_len: raise NoSuchRevision(self, stop_revision) return other_history[self_len:stop_revision] def update_revisions(self, other, stop_revision=None): """Pull in all new revisions from other branch. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch(files=['foo', 'bar']) >>> br1.add('foo') >>> br1.add('bar') >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False) >>> br2 = ScratchBranch() >>> br2.update_revisions(br1) Added 2 texts. Added 1 inventories. Added 1 revisions. >>> br2.revision_history() [u'REVISION-ID-1'] >>> br2.update_revisions(br1) Added 0 texts. Added 0 inventories. Added 0 revisions. >>> br1.text_store.total_size() == br2.text_store.total_size() True """ from bzrlib.progress import ProgressBar try: set except NameError: from sets import Set as set pb = ProgressBar() pb.update('comparing histories') revision_ids = self.missing_revisions(other, stop_revision) if hasattr(other.revision_store, "prefetch"): other.revision_store.prefetch(revision_ids) if hasattr(other.inventory_store, "prefetch"): inventory_ids = [other.get_revision(r).inventory_id for r in revision_ids] other.inventory_store.prefetch(inventory_ids) revisions = [] needed_texts = set() i = 0 for rev_id in revision_ids: i += 1 pb.update('fetching revision', i, len(revision_ids)) rev = other.get_revision(rev_id) revisions.append(rev) inv = other.get_inventory(str(rev.inventory_id)) for key, entry in inv.iter_entries(): if entry.text_id is None: continue if entry.text_id not in self.text_store: needed_texts.add(entry.text_id) pb.clear() count = self.text_store.copy_multi(other.text_store, needed_texts) print "Added %d texts." % count inventory_ids = [ f.inventory_id for f in revisions ] count = self.inventory_store.copy_multi(other.inventory_store, inventory_ids) print "Added %d inventories." % count revision_ids = [ f.revision_id for f in revisions] count = self.revision_store.copy_multi(other.revision_store, revision_ids) for revision_id in revision_ids: self.append_revision(revision_id) print "Added %d revisions." % count def commit(self, *args, **kw): from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" from bzrlib.tree import EmptyTree, RevisionTree # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ from bzrlib.tree import EmptyTree, RevisionTree r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self.lock_write() try: tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): raise BzrError("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): raise BzrError("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: raise BzrError("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): raise BzrError("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': raise BzrError("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self.lock_write() try: ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): raise BzrError("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): raise BzrError("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': raise BzrError("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): raise BzrError("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): raise BzrError("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: raise BzrError("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): raise BzrError("destination %r already exists" % dest_path) if f_id in to_idpath: raise BzrError("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() def revert(self, filenames, old_tree=None, backups=True): """Restore selected files to the versions from a previous tree. backups If true (default) backups are made of files before they're renamed. """ from bzrlib.errors import NotVersionedError, BzrError from bzrlib.atomicfile import AtomicFile from bzrlib.osutils import backup_file inv = self.read_working_inventory() if old_tree is None: old_tree = self.basis_tree() old_inv = old_tree.inventory nids = [] for fn in filenames: file_id = inv.path2id(fn) if not file_id: raise NotVersionedError("not a versioned file", fn) if not old_inv.has_id(file_id): raise BzrError("file not present in old tree", fn, file_id) nids.append((fn, file_id)) # TODO: Rename back if it was previously at a different location # TODO: If given a directory, restore the entire contents from # the previous version. # TODO: Make a backup to a temporary file. # TODO: If the file previously didn't exist, delete it? for fn, file_id in nids: backup_file(fn) f = AtomicFile(fn, 'wb') try: f.write(old_tree.get_file(file_id).read()) f.commit() finally: f.close() class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[], base=None): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ from tempfile import mkdtemp init = False if base is None: base = mkdtemp() init = True Branch.__init__(self, base, init=init) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def clone(self): """ >>> orig = ScratchBranch(files=["file1", "file2"]) >>> clone = orig.clone() >>> os.path.samefile(orig.base, clone.base) False >>> os.path.isfile(os.path.join(clone.base, "file1")) True """ from shutil import copytree from tempfile import mkdtemp base = mkdtemp() os.rmdir(base) copytree(self.base, base, symlinks=True) return ScratchBranch(base=base) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" from shutil import rmtree try: if self.base: mutter("delete ScratchBranch %s" % self.base) rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re from binascii import hexlify from time import time # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time()), s)) M 644 inline bzrlib/commands.py data 53346 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import BzrError, BzrCheckError, BzrCommandError from bzrlib.branch import find_branch from bzrlib import BZRDIR plugin_cmds = {} def register_command(cmd): "Utility function to help register a command" global plugin_cmds k = cmd.__name__ if k.startswith("cmd_"): k_unsquished = _unsquish_command_name(k) else: k_unsquished = k if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = cmd else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r' % sys.modules[cmd.__module__]) def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _get_cmd_dict(plugins_override=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v # If we didn't load plugins, the plugin_cmds dict will be empty if plugins_override: d.update(plugin_cmds) else: d2 = plugin_cmds.copy() d2.update(d) d = d2 return d def get_all_cmds(plugins_override=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(plugins_override=plugins_override).iteritems(): yield k,v def get_cmd_class(cmd, plugins_override=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(plugins_override=plugins_override) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() for opt in self.takes_options: if not opt in OPTIONS: raise BzrError("Unknown option '%s' returned by external command %s" % (opt, path)) # TODO: Is there any way to check takes_args is valid here? self.takes_args = pipe.readline().split() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-usage'" % path) pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-help'" % path) def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: optname = name.replace('_','-') value = kargs[name] if OPTIONS.has_key(optname): # it's an option opts.append('--%s' % optname) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = find_branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): from bzrlib.xml import pack_xml pack_xml(find_branch('.').get_revision(revision_id), sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print find_branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): from bzrlib.add import smart_add smart_add(file_list, verbose, not no_recurse) class cmd_mkdir(Command): """Create a new versioned directory. This is equivalent to creating the directory and then adding it. """ takes_args = ['dir+'] def run(self, dir_list): b = None for d in dir_list: os.mkdir(d) if not b: b = find_branch(d) b.add([d], verbose=True) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print find_branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = find_branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = find_branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = find_branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import tempfile from shutil import rmtree import errno br_to = find_branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if e.errno != errno.ENOENT: raise if location is None: if stored_loc is None: raise BzrCommandError("No pull location known or specified.") else: print "Using last location: %s" % stored_loc location = stored_loc cache_root = tempfile.mkdtemp() from bzrlib.branch import DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: from branch import find_cached_branch, DivergedBranches br_from = find_cached_branch(location, cache_root) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged." " Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") finally: rmtree(cache_root) class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. To retrieve the branch as of a particular revision, supply the --revision parameter, as in "branch foo/bar -r 5". """ takes_args = ['from_location', 'to_location?'] takes_options = ['revision'] def run(self, from_location, to_location=None, revision=None): import errno from bzrlib.merge import merge from bzrlib.branch import DivergedBranches, NoSuchRevision, \ find_cached_branch, Branch from shutil import rmtree from meta_store import CachedStore import tempfile cache_root = tempfile.mkdtemp() try: try: br_from = find_cached_branch(from_location, cache_root) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not' ' exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location.rstrip("/\\")) try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already' ' exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) try: br_to.update_revisions(br_from, stop_revision=revision) except NoSuchRevision: rmtree(to_location) msg = "The branch %s has no revision %d." % (from_location, revision) raise BzrCommandError(msg) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False, ignore_zero=True) from_location = pull_loc(br_from) br_to.controlfile("x-pull", "wb").write(from_location + "\n") finally: rmtree(cache_root) def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = find_branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = find_branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = find_branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: raise BzrError("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = find_branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: raise BzrError("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in find_branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in find_branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): from bzrlib.branch import Branch Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = find_branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = find_branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = find_branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = find_branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None): from bzrlib.branch import find_branch from bzrlib.log import log_formatter, show_log import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') lf = log_formatter('short', show_ids=show_ids, to_file=outf, show_timezone=timezone) show_log(b, lf, file_id, verbose=verbose, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = find_branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = find_branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): from bzrlib.osutils import quotefn for f in find_branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = find_branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = find_branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print find_branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, exports to a directory (equivalent to --format=dir).""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format'] def run(self, dest, revision=None, format='dir'): b = find_branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest, format) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = find_branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit from bzrlib.osutils import get_text_message ## Warning: shadows builtin file() if not message and not file: import cStringIO stdout = sys.stdout catcher = cStringIO.StringIO() sys.stdout = catcher cmd_status({"file_list":selected_list}, {}) info = catcher.getvalue() sys.stdout = stdout message = get_text_message(info) if message is None: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = find_branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.check import check check(find_branch(dir)) class cmd_upgrade(Command): """Upgrade branch storage to current format. This should normally be used only after the check command tells you to run it. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.upgrade import upgrade upgrade(find_branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest return int(not selftest()) class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Restore selected files from a previous revision. """ takes_args = ['file+'] def run(self, file_list): from bzrlib.branch import find_branch if not file_list: file_list = ['.'] b = find_branch(file_list[0]) b.revert([b.relpath(f) for f in file_list]) class cmd_merge_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = find_branch('.') statcache.update_cache(b.base, b.read_working_inventory()) class cmd_plugins(Command): """List plugins""" hidden = True def run(self): import bzrlib.plugin from pprint import pprint pprint(bzrlib.plugin.all_plugins) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'update': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) >>> parse_args('log -r 500'.split()) (['log'], {'revision': 500}) >>> parse_args('log -r500:600'.split()) (['log'], {'revision': [500, 600]}) >>> parse_args('log -vr500:600'.split()) (['log'], {'verbose': True, 'revision': [500, 600]}) >>> parse_args('log -rv500:600'.split()) #the r takes an argument Traceback (most recent call last): ... ValueError: invalid literal for int(): v500 """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: raise BzrError('unknown long option %r' % a) else: shortopt = a[1:] if shortopt in SHORT_OPTIONS: # Multi-character options must have a space to delimit # their value optname = SHORT_OPTIONS[shortopt] else: # Single character short options, can be chained, # and have their value appended to their name shortopt = a[1:2] if shortopt not in SHORT_OPTIONS: # We didn't find the multi-character name, and we # didn't find the single char name raise BzrError('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if a[2:]: # There are extra things on this option # see if it is the value, or if it is another # short option optargfn = OPTIONS[optname] if optargfn is None: # This option does not take an argument, so the # next entry is another short option, pack it back # into the list argv.insert(0, '-' + a[2:]) else: # This option takes an argument, so pack it # into the array optarg = a[2:] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? raise BzrError('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: raise BzrError('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: raise BzrError('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def _parse_master_args(argv): """Parse the arguments that always go with the original command. These are things like bzr --no-plugins, etc. There are now 2 types of option flags. Ones that come *before* the command, and ones that come *after* the command. Ones coming *before* the command are applied against all possible commands. And are generally applied before plugins are loaded. The current list are: --builtin Allow plugins to load, but don't let them override builtin commands, they will still be allowed if they do not override a builtin. --no-plugins Don't load any plugins. This lets you get back to official source behavior. --profile Enable the hotspot profile before running the command. For backwards compatibility, this is also a non-master option. --version Spit out the version of bzr that is running and exit. This is also a non-master option. --help Run help and exit, also a non-master option (I think that should stay, though) >>> argv, opts = _parse_master_args(['bzr', '--test']) Traceback (most recent call last): ... BzrCommandError: Invalid master option: 'test' >>> argv, opts = _parse_master_args(['bzr', '--version', 'command']) >>> print argv ['command'] >>> print opts['version'] True >>> argv, opts = _parse_master_args(['bzr', '--profile', 'command', '--more-options']) >>> print argv ['command', '--more-options'] >>> print opts['profile'] True >>> argv, opts = _parse_master_args(['bzr', '--no-plugins', 'command']) >>> print argv ['command'] >>> print opts['no-plugins'] True >>> print opts['profile'] False >>> argv, opts = _parse_master_args(['bzr', 'command', '--profile']) >>> print argv ['command', '--profile'] >>> print opts['profile'] False """ master_opts = {'builtin':False, 'no-plugins':False, 'version':False, 'profile':False, 'help':False } # This is the point where we could hook into argv[0] to determine # what front-end is supposed to be run # For now, we are just ignoring it. cmd_name = argv.pop(0) for arg in argv[:]: if arg[:2] != '--': # at the first non-option, we return the rest break arg = arg[2:] # Remove '--' if arg not in master_opts: # We could say that this is not an error, that we should # just let it be handled by the main section instead raise BzrCommandError('Invalid master option: %r' % arg) argv.pop(0) # We are consuming this entry master_opts[arg] = True return argv, master_opts def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: # some options like --builtin and --no-plugins have special effects argv, master_opts = _parse_master_args(argv) if not master_opts['no-plugins']: from bzrlib.plugin import load_plugins load_plugins() args, opts = parse_args(argv) if master_opts['help']: from bzrlib.help import help if argv: help(argv[0]) else: help() return 0 if 'help' in opts: from bzrlib.help import help if args: help(args[0]) else: help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 plugins_override = not (master_opts['builtin']) canonical_cmd, cmd_class = get_cmd_class(cmd, plugins_override=plugins_override) profile = master_opts['profile'] # For backwards compatibility, I would rather stick with --profile being a # master/global option if 'profile' in opts: profile = True del opts['profile'] # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): bzrlib.trace.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: import errno quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/commit.py data 10486 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def commit(branch, message, timestamp=None, timezone=None, committer=None, verbose=True, specific_files=None, rev_id=None): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. specific_files If true, commit only those files. rev_id If set, use this as the new revision id. Useful for test or import commands that need to tightly control what revisions are assigned. If you duplicate a revision id that exists elsewhere it is your own fault. If null (default), a time/random revision id is generated. """ import time, tempfile from bzrlib.osutils import local_time_offset, username from bzrlib.branch import gen_file_id from bzrlib.errors import BzrError from bzrlib.revision import Revision, RevisionReference from bzrlib.trace import mutter, note from bzrlib.xml import pack_xml branch.lock_write() try: # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_tree = branch.working_tree() work_inv = work_tree.inventory basis = branch.basis_tree() basis_inv = basis.inventory if verbose: note('looking for changes...') missing_ids, new_inv = _gather_commit(branch, work_tree, work_inv, basis_inv, specific_files, verbose) for file_id in missing_ids: # Any files that have been deleted are now removed from the # working inventory. Files that were not selected for commit # are left as they were in the working inventory and ommitted # from the revision inventory. # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itbranch. if work_inv.has_id(file_id): del work_inv[file_id] if rev_id is None: rev_id = _gen_revision_id(time.time()) inv_id = rev_id inv_tmp = tempfile.TemporaryFile() pack_xml(new_inv, inv_tmp) inv_tmp.seek(0) branch.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) # We could also just sha hash the inv_tmp file # however, in the case that branch.inventory_store.add() # ever actually does anything special inv_sha1 = branch.get_inventory_sha1(inv_id) branch._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, message = message, inventory_id=inv_id, inventory_sha1=inv_sha1, revision_id=rev_id) precursor_id = branch.last_patch() if precursor_id: precursor_sha1 = branch.get_revision_sha1(precursor_id) rev.parents = [RevisionReference(precursor_id, precursor_sha1)] rev_tmp = tempfile.TemporaryFile() pack_xml(rev, rev_tmp) rev_tmp.seek(0) branch.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (branch.revno() + 1)) branch.append_revision(rev_id) if verbose: note("commited r%d" % branch.revno()) finally: branch.unlock() def _gen_revision_id(when): """Return new revision-id.""" from binascii import hexlify from osutils import rand_bytes, compact_date, user_email s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def _gather_commit(branch, work_tree, work_inv, basis_inv, specific_files, verbose): """Build inventory preparatory to commit. This adds any changed files into the text store, and sets their test-id, sha and size in the returned inventory appropriately. missing_ids Modified to hold a list of files that have been deleted from the working directory; these should be removed from the working inventory. """ from bzrlib.inventory import Inventory from osutils import isdir, isfile, sha_string, quotefn, \ local_time_offset, username, kind_marker, is_inside_any from branch import gen_file_id from errors import BzrError from revision import Revision from bzrlib.trace import mutter, note inv = Inventory() missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). p = branch.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if specific_files and not is_inside_any(specific_files, path): if basis_inv.has_id(file_id): # carry over with previous state inv.add(basis_inv[file_id].copy()) else: # omit this from committed inventory pass continue if not work_tree.has_id(file_id): if verbose: print('deleted %s%s' % (path, kind_marker(entry.kind))) mutter(" file is missing, removing from inventory") missing_ids.append(file_id) continue # this is present in the new inventory; may be new, modified or # unchanged. old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] entry = entry.copy() inv.add(entry) if old_ie: old_kind = old_ie.kind if old_kind != entry.kind: raise BzrError("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): raise BzrError("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): raise BzrError("%s is entered as file but is not a file" % quotefn(p)) new_sha1 = work_tree.get_file_sha1(file_id) if (old_ie and old_ie.text_sha1 == new_sha1): ## assert content == basis.get_file(file_id).read() entry.text_id = old_ie.text_id entry.text_sha1 = new_sha1 entry.text_size = old_ie.text_size mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: content = file(p, 'rb').read() # calculate the sha again, just in case the file contents # changed since we updated the cache entry.text_sha1 = sha_string(content) entry.text_size = len(content) entry.text_id = gen_file_id(entry.name) branch.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: marked = path + kind_marker(entry.kind) if not old_ie: print 'added', marked elif old_ie == entry: pass # unchanged elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): print 'modified', marked else: print 'renamed', marked return missing_ids, inv M 644 inline bzrlib/inventory.py data 19147 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # This should really be an id randomly assigned when the tree is # created, but it's not for now. ROOT_ID = "TREE_ROOT" import sys, os.path, types, re import bzrlib from bzrlib.errors import BzrError, BzrCheckError from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(object): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') 'TREE_ROOT' >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID)) >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123')) Traceback (most recent call last): ... BzrError: inventory already contains entry with id {2323} >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123')) >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' TODO: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ # TODO: split InventoryEntry into subclasses for files, # directories, etc etc. text_sha1 = None text_size = None def __init__(self, file_id, name, kind, parent_id, text_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID) >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID) Traceback (most recent call last): BzrCheckError: InventoryEntry name 'src/hello.c' is invalid """ if '/' in name or '\\' in name: raise BzrCheckError('InventoryEntry name %r is invalid' % name) self.file_id = file_id self.name = name self.kind = kind self.text_id = text_id self.parent_id = parent_id if kind == 'directory': self.children = {} elif kind == 'file': pass else: raise BzrError("unhandled entry kind %r" % kind) def sorted_children(self): l = self.children.items() l.sort() return l def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.parent_id, text_id=self.text_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size # note that children are *not* copied; they're pulled across when # others are added return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" from bzrlib.xml import Element e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size != None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1']: v = getattr(self, f) if v != None: e.set(f, v) # to be conservative, we don't externalize the root pointers # for now, leaving them as null in the xml form. in a future # version it will be implied by nested elements. if self.parent_id != ROOT_ID: assert isinstance(self.parent_id, basestring) e.set('parent_id', self.parent_id) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' ## original format inventories don't have a parent_id for ## nodes in the root directory, but it's cleaner to use one ## internally. parent_id = elt.get('parent_id') if parent_id == None: parent_id = ROOT_ID self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __eq__(self, other): if not isinstance(other, InventoryEntry): return NotImplemented return (self.file_id == other.file_id) \ and (self.name == other.name) \ and (self.text_sha1 == other.text_sha1) \ and (self.text_size == other.text_size) \ and (self.text_id == other.text_id) \ and (self.parent_id == other.parent_id) \ and (self.kind == other.kind) def __ne__(self, other): return not (self == other) def __hash__(self): raise ValueError('not hashable') class RootEntry(InventoryEntry): def __init__(self, file_id): self.file_id = file_id self.children = {} self.kind = 'root_directory' self.parent_id = None self.name = '' def __eq__(self, other): if not isinstance(other, RootEntry): return NotImplemented return (self.file_id == other.file_id) \ and (self.children == other.children) class Inventory(object): """Inventory of versioned files in a tree. This describes which file_id is present at each point in the tree, and possibly the SHA-1 or other information about the file. Entries can be looked up either by path or by file_id. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted, other than through the Inventory API. >>> inv = Inventory() >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID)) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] """ def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. The inventory is created with a default root directory, with an id of None. """ self.root = RootEntry(ROOT_ID) self._byid = {self.root.file_id: self.root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, from_dir=None): """Return (path, entry) pairs, in order by name.""" if from_dir == None: assert self.root from_dir = self.root elif isinstance(from_dir, basestring): from_dir = self._byid[from_dir] kids = from_dir.children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(from_dir=ie.file_id): yield os.path.join(name, cn), cie def entries(self): """Return list of (path, ie) for all entries except the root. This may be faster than iter_entries. """ accum = [] def descend(dir_ie, dir_path): kids = dir_ie.children.items() kids.sort() for name, ie in kids: child_path = os.path.join(dir_path, name) accum.append((child_path, ie)) if ie.kind == 'directory': descend(ie, child_path) descend(self.root, '') return accum def directories(self): """Return (path, entry) pairs for all directories, including the root. """ accum = [] def descend(parent_ie, parent_path): accum.append((parent_path, parent_ie)) kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory'] kids.sort() for name, child_ie in kids: child_path = os.path.join(parent_path, name) descend(child_ie, child_path) descend(self.root, '') return accum def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID)) >>> inv['123123'].name 'hello.c' """ try: return self._byid[file_id] except KeyError: if file_id == None: raise BzrError("can't look up file_id None") else: raise BzrError("file_id {%s} not in inventory" % file_id) def get_file_kind(self, file_id): return self._byid[file_id].kind def get_child(self, parent_id, filename): return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self._byid: raise BzrError("inventory already contains entry with id {%s}" % entry.file_id) try: parent = self._byid[entry.parent_id] except KeyError: raise BzrError("parent_id {%s} not in inventory" % entry.parent_id) if parent.children.has_key(entry.name): raise BzrError("%s is already versioned" % appendpath(self.id2path(parent.file_id), entry.name)) self._byid[entry.file_id] = entry parent.children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" from bzrlib.errors import NotVersionedError parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: raise BzrError("cannot re-add root of inventory") if file_id == None: from bzrlib.branch import gen_file_id file_id = gen_file_id(relpath) parent_path = parts[:-1] parent_id = self.path2id(parent_path) if parent_id == None: raise NotVersionedError(parent_path) ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def to_element(self): """Convert to XML Element""" from bzrlib.xml import Element e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID)) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __eq__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 True """ if not isinstance(other, Inventory): return NotImplemented if len(self._byid) != len(other._byid): # shortcut: obviously not the same return False return self._byid == other._byid def __ne__(self, other): return not (self == other) def __hash__(self): raise ValueError('not hashable') def get_idpath(self, file_id): """Return a list of file_ids for the path to an entry. The list contains one element for each directory followed by the id of the file itself. So the length of the returned list is equal to the depth of the file in the tree, counting the root directory as depth 1. """ p = [] while file_id != None: try: ie = self._byid[file_id] except KeyError: raise BzrError("file_id {%s} not found in inventory" % file_id) p.insert(0, ie.file_id) file_id = ie.parent_id return p def id2path(self, file_id): """Return as a list the path to file_id.""" # get all names, skipping root p = [self[fid].name for fid in self.get_idpath(file_id)[1:]] return os.sep.join(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. Returns None iff the path is not found. """ if isinstance(name, types.StringTypes): name = splitpath(name) mutter("lookup path %r" % name) parent = self.root for f in name: try: cie = parent.children[f] assert cie.name == f assert cie.parent_id == parent.file_id parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): return self._byid.has_key(file_id) def rename(self, file_id, new_parent_id, new_name): """Move a file within the inventory. This can change either the name, or the parent, or both. This does not move the working file.""" if not is_valid_name(new_name): raise BzrError("not an acceptable filename: %r" % new_name) new_parent = self._byid[new_parent_id] if new_name in new_parent.children: raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id))) new_parent_idpath = self.get_idpath(new_parent_id) if file_id in new_parent_idpath: raise BzrError("cannot move directory %r into a subdirectory of itself, %r" % (self.id2path(file_id), self.id2path(new_parent_id))) file_ie = self._byid[file_id] old_parent = self._byid[file_ie.parent_id] # TODO: Don't leave things messed up if this fails del old_parent.children[file_ie.name] new_parent.children[new_name] = file_ie file_ie.name = new_name file_ie.parent_id = new_parent_id _NAME_RE = re.compile(r'^[^/\\]+$') def is_valid_name(name): return bool(_NAME_RE.match(name)) M 644 inline bzrlib/remotebranch.py data 7003 #! /usr/bin/env python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Proxy object for access to remote branches. At the moment remote branches are only for HTTP and only for read access. """ import gzip from cStringIO import StringIO import urllib2 from errors import BzrError, BzrCheckError from branch import Branch, BZR_BRANCH_FORMAT from trace import mutter # velocitynet.com.au transparently proxies connections and thereby # breaks keep-alive -- sucks! ENABLE_URLGRABBER = True if ENABLE_URLGRABBER: import urlgrabber import urlgrabber.keepalive urlgrabber.keepalive.DEBUG = 0 def get_url(path, compressed=False): try: url = path if compressed: url += '.gz' mutter("grab url %s" % url) url_f = urlgrabber.urlopen(url, keepalive=1, close_connection=0) if not compressed: return url_f else: return gzip.GzipFile(fileobj=StringIO(url_f.read())) except urllib2.URLError, e: raise BzrError("remote fetch failed: %r: %s" % (url, e)) else: def get_url(url, compressed=False): import urllib2 if compressed: url += '.gz' mutter("get_url %s" % url) url_f = urllib2.urlopen(url) if compressed: return gzip.GzipFile(fileobj=StringIO(url_f.read())) else: return url_f def _find_remote_root(url): """Return the prefix URL that corresponds to the branch root.""" orig_url = url while True: try: ff = get_url(url + '/.bzr/branch-format') fmt = ff.read() ff.close() fmt = fmt.rstrip('\r\n') if fmt != BZR_BRANCH_FORMAT.rstrip('\r\n'): raise BzrError("sorry, branch format %r not supported at url %s" % (fmt, url)) return url except urllib2.URLError: pass try: idx = url.rindex('/') except ValueError: raise BzrError('no branch root found for URL %s' % orig_url) url = url[:idx] class RemoteBranch(Branch): def __init__(self, baseurl, find_root=True): """Create new proxy for a remote branch.""" if find_root: self.baseurl = _find_remote_root(baseurl) else: self.baseurl = baseurl self._check_format() self.inventory_store = RemoteStore(baseurl + '/.bzr/inventory-store/') self.text_store = RemoteStore(baseurl + '/.bzr/text-store/') self.revision_store = RemoteStore(baseurl + '/.bzr/revision-store/') def __str__(self): b = getattr(self, 'baseurl', 'undefined') return '%s(%r)' % (self.__class__.__name__, b) __repr__ = __str__ def controlfile(self, filename, mode): if mode not in ('rb', 'rt', 'r'): raise BzrError("file mode %r not supported for remote branches" % mode) return get_url(self.baseurl + '/.bzr/' + filename, False) def lock_read(self): # no locking for remote branches yet pass def lock_write(self): from errors import LockError raise LockError("write lock not supported for remote branch %s" % self.baseurl) def unlock(self): pass def relpath(self, path): if not path.startswith(self.baseurl): raise BzrError('path %r is not under base URL %r' % (path, self.baseurl)) pl = len(self.baseurl) return path[pl:].lstrip('/') def get_revision(self, revision_id): from bzrlib.revision import Revision from bzrlib.xml import unpack_xml revf = self.revision_store[revision_id] r = unpack_xml(Revision, revf) if r.revision_id != revision_id: raise BzrCheckError('revision stored as {%s} actually contains {%s}' % (revision_id, r.revision_id)) return r class RemoteStore(object): def __init__(self, baseurl): self._baseurl = baseurl def _path(self, name): if '/' in name: raise ValueError('invalid store id', name) return self._baseurl + '/' + name def __getitem__(self, fileid): p = self._path(fileid) return get_url(p, compressed=True) def simple_walk(): """For experimental purposes, traverse many parts of a remote branch""" from bzrlib.revision import Revision from bzrlib.branch import Branch from bzrlib.inventory import Inventory from bzrlib.xml import unpack_xml got_invs = {} got_texts = {} print 'read history' history = get_url('/.bzr/revision-history').readlines() num_revs = len(history) for i, rev_id in enumerate(history): rev_id = rev_id.rstrip() print 'read revision %d/%d' % (i, num_revs) # python gzip needs a seekable file (!!) but the HTTP response # isn't, so we need to buffer it rev_f = get_url('/.bzr/revision-store/%s' % rev_id, compressed=True) rev = unpack_xml(Revision, rev_f) print rev.message inv_id = rev.inventory_id if inv_id not in got_invs: print 'get inventory %s' % inv_id inv_f = get_url('/.bzr/inventory-store/%s' % inv_id, compressed=True) inv = Inventory.read_xml(inv_f) print '%4d inventory entries' % len(inv) for path, ie in inv.iter_entries(): text_id = ie.text_id if text_id == None: continue if text_id in got_texts: continue print ' fetch %s text {%s}' % (path, text_id) text_f = get_url('/.bzr/text-store/%s' % text_id, compressed=True) got_texts[text_id] = True got_invs.add[inv_id] = True print '----' def try_me(): BASE_URL = 'http://bazaar-ng.org/bzr/bzr.dev/' b = RemoteBranch(BASE_URL) ## print '\n'.join(b.revision_history()) from log import show_log show_log(b) if __name__ == '__main__': try_me() M 644 inline bzrlib/revision.py data 6028 # (C) 2005 Canonical # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA class RevisionReference(object): """ Reference to a stored revision. Includes the revision_id and revision_sha1. """ revision_id = None revision_sha1 = None def __init__(self, revision_id, revision_sha1): if revision_id == None \ or isinstance(revision_id, basestring): self.revision_id = revision_id else: raise ValueError('bad revision_id %r' % revision_id) if revision_sha1 != None: if isinstance(revision_sha1, basestring) \ and len(revision_sha1) == 40: self.revision_sha1 = revision_sha1 else: raise ValueError('bad revision_sha1 %r' % revision_sha1) class Revision(object): """Single revision on a branch. Revisions may know their revision_hash, but only once they've been written out. This is not stored because you cannot write the hash into the file it describes. After bzr 0.0.5 revisions are allowed to have multiple parents. To support old clients this is written out in a slightly redundant form: the first parent as the predecessor. This will eventually be dropped. parents List of parent revisions, each is a RevisionReference. """ inventory_id = None inventory_sha1 = None revision_id = None timestamp = None message = None timezone = None committer = None def __init__(self, **args): self.__dict__.update(args) self.parents = [] def _get_precursor(self): from warnings import warn warn("Revision.precursor is deprecated", stacklevel=2) if self.parents: return self.parents[0].revision_id else: return None def _get_precursor_sha1(self): from warnings import warn warn("Revision.precursor_sha1 is deprecated", stacklevel=2) if self.parents: return self.parents[0].revision_sha1 else: return None def _fail(self): raise Exception("can't assign to precursor anymore") precursor = property(_get_precursor, _fail, _fail) precursor_sha1 = property(_get_precursor_sha1, _fail, _fail) def __repr__(self): return "" % self.revision_id def to_element(self): from bzrlib.xml import Element, SubElement root = Element('revision', committer = self.committer, timestamp = '%.9f' % self.timestamp, revision_id = self.revision_id, inventory_id = self.inventory_id, inventory_sha1 = self.inventory_sha1, ) if self.timezone: root.set('timezone', str(self.timezone)) root.text = '\n' msg = SubElement(root, 'message') msg.text = self.message msg.tail = '\n' if self.parents: # first parent stored as precursor for compatability with 0.0.5 and # earlier pr = self.parents[0] assert pr.revision_id root.set('precursor', pr.revision_id) if pr.revision_sha1: root.set('precursor_sha1', pr.revision_sha1) if self.parents: pelts = SubElement(root, 'parents') pelts.tail = pelts.text = '\n' for rr in self.parents: assert isinstance(rr, RevisionReference) p = SubElement(pelts, 'revision_ref') p.tail = '\n' assert rr.revision_id p.set('revision_id', rr.revision_id) if rr.revision_sha1: p.set('revision_sha1', rr.revision_sha1) return root def from_element(cls, elt): return unpack_revision(elt) from_element = classmethod(from_element) def unpack_revision(elt): """Convert XML element into Revision object.""" # is deprecated... from bzrlib.errors import BzrError if elt.tag not in ('revision', 'changeset'): raise BzrError("unexpected tag in revision file: %r" % elt) rev = Revision(committer = elt.get('committer'), timestamp = float(elt.get('timestamp')), revision_id = elt.get('revision_id'), inventory_id = elt.get('inventory_id'), inventory_sha1 = elt.get('inventory_sha1') ) precursor = elt.get('precursor') precursor_sha1 = elt.get('precursor_sha1') pelts = elt.find('parents') if precursor: # revisions written prior to 0.0.5 have a single precursor # give as an attribute rev_ref = RevisionReference(precursor, precursor_sha1) rev.parents.append(rev_ref) elif pelts: for p in pelts: assert p.tag == 'revision_ref', \ "bad parent node tag %r" % p.tag rev_ref = RevisionReference(p.get('revision_id'), p.get('revision_sha1')) rev.parents.append(rev_ref) v = elt.get('timezone') rev.timezone = v and int(v) rev.message = elt.findtext('message') # text of return rev M 644 inline bzrlib/upgrade.py data 5312 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def upgrade(branch): """ Upgrade branch to current format. This causes objects to be rewritten into the current format. If they change, their SHA-1 will of course change, which might break any later signatures, or backreferences that check the SHA-1. TODO: Check non-mainline revisions. """ import sys from bzrlib.trace import mutter from bzrlib.errors import BzrCheckError from bzrlib.progress import ProgressBar branch.lock_write() try: pb = ProgressBar(show_spinner=True) last_rev_id = None history = branch.revision_history() revno = 0 revcount = len(history) updated_revisions = [] # Set to True in the case that the previous revision entry # was updated, since this will affect future revision entries updated_previous_revision = False for rev_id in history: revno += 1 pb.update('upgrading revision', revno, revcount) rev = branch.get_revision(rev_id) if rev.revision_id != rev_id: raise BzrCheckError('wrong internal revision id in revision {%s}' % rev_id) last_rev_id = rev_id # if set to true, revision must be written out updated = False if rev.inventory_sha1 is None: rev.inventory_sha1 = branch.get_inventory_sha1(rev.inventory_id) updated = True mutter(" set inventory_sha1 on {%s}" % rev_id) for prr in rev.parents: actual_sha1 = branch.get_revision_sha1(prr.revision_id) if (updated_previous_revision or prr.revision_sha1 is None): if prr.revision_sha1 != actual_sha1: prr.revision_sha1 = actual_sha1 updated = True elif actual_sha1 != prr.revision_sha1: raise BzrCheckError("parent {%s} of {%s} sha1 mismatch: " "%s vs %s" % (prr.revision_id, rev_id, actual_sha1, prr.revision_sha1)) if updated: updated_previous_revision = True # We had to update this revision entries hashes # Now we need to write out a new value # This is a little bit invasive, since we are *rewriting* a # revision entry. I'm not supremely happy about it, but # there should be *some* way of making old entries have # the full meta information. import tempfile, os, errno from bzrlib.xml import pack_xml rev_tmp = tempfile.TemporaryFile() pack_xml(rev, rev_tmp) rev_tmp.seek(0) tmpfd, tmp_path = tempfile.mkstemp(prefix=rev_id, suffix='.gz', dir=branch.controlfilename('revision-store')) os.close(tmpfd) def special_rename(p1, p2): if sys.platform == 'win32': try: os.remove(p2) except OSError, e: if e.errno != errno.ENOENT: raise os.rename(p1, p2) try: # TODO: We may need to handle the case where the old revision # entry was not compressed (and thus did not end with .gz) # Remove the old revision entry out of the way rev_path = branch.controlfilename(['revision-store', rev_id+'.gz']) special_rename(rev_path, tmp_path) branch.revision_store.add(rev_tmp, rev_id) # Add the new one os.remove(tmp_path) # Remove the old name mutter(' Updated revision entry {%s}' % rev_id) except: # On any exception, restore the old entry special_rename(tmp_path, rev_path) raise rev_tmp.close() updated_revisions.append(rev_id) else: updated_previous_revision = False finally: branch.unlock() pb.clear() if updated_revisions: print '%d revisions updated to current format' % len(updated_revisions) M 644 inline bzrlib/xml.py data 1392 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """XML externalization support.""" # "XML is like violence: if it doesn't solve your problem, you aren't # using enough of it." -- various # importing this module is fairly slow because it has to load several ElementTree bits try: from cElementTree import ElementTree, SubElement, Element except ImportError: from elementtree.ElementTree import ElementTree, SubElement, Element def pack_xml(o, f): """Write object o to file f as XML. o must provide a to_element method. """ ElementTree(o.to_element()).write(f, 'utf-8') f.write('\n') def unpack_xml(cls, f): return cls.from_element(ElementTree().parse(f)) commit refs/heads/master mark :803 committer Martin Pool 1119957281 +1000 data 37 - Remove dead code from remotebranch from :802 M 644 inline bzrlib/remotebranch.py data 5130 #! /usr/bin/env python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Proxy object for access to remote branches. At the moment remote branches are only for HTTP and only for read access. """ import gzip from cStringIO import StringIO import urllib2 from errors import BzrError, BzrCheckError from branch import Branch, BZR_BRANCH_FORMAT from trace import mutter # velocitynet.com.au transparently proxies connections and thereby # breaks keep-alive -- sucks! ENABLE_URLGRABBER = True if ENABLE_URLGRABBER: import urlgrabber import urlgrabber.keepalive urlgrabber.keepalive.DEBUG = 0 def get_url(path, compressed=False): try: url = path if compressed: url += '.gz' mutter("grab url %s" % url) url_f = urlgrabber.urlopen(url, keepalive=1, close_connection=0) if not compressed: return url_f else: return gzip.GzipFile(fileobj=StringIO(url_f.read())) except urllib2.URLError, e: raise BzrError("remote fetch failed: %r: %s" % (url, e)) else: def get_url(url, compressed=False): import urllib2 if compressed: url += '.gz' mutter("get_url %s" % url) url_f = urllib2.urlopen(url) if compressed: return gzip.GzipFile(fileobj=StringIO(url_f.read())) else: return url_f def _find_remote_root(url): """Return the prefix URL that corresponds to the branch root.""" orig_url = url while True: try: ff = get_url(url + '/.bzr/branch-format') fmt = ff.read() ff.close() fmt = fmt.rstrip('\r\n') if fmt != BZR_BRANCH_FORMAT.rstrip('\r\n'): raise BzrError("sorry, branch format %r not supported at url %s" % (fmt, url)) return url except urllib2.URLError: pass try: idx = url.rindex('/') except ValueError: raise BzrError('no branch root found for URL %s' % orig_url) url = url[:idx] class RemoteBranch(Branch): def __init__(self, baseurl, find_root=True): """Create new proxy for a remote branch.""" if find_root: self.baseurl = _find_remote_root(baseurl) else: self.baseurl = baseurl self._check_format() self.inventory_store = RemoteStore(baseurl + '/.bzr/inventory-store/') self.text_store = RemoteStore(baseurl + '/.bzr/text-store/') self.revision_store = RemoteStore(baseurl + '/.bzr/revision-store/') def __str__(self): b = getattr(self, 'baseurl', 'undefined') return '%s(%r)' % (self.__class__.__name__, b) __repr__ = __str__ def controlfile(self, filename, mode): if mode not in ('rb', 'rt', 'r'): raise BzrError("file mode %r not supported for remote branches" % mode) return get_url(self.baseurl + '/.bzr/' + filename, False) def lock_read(self): # no locking for remote branches yet pass def lock_write(self): from errors import LockError raise LockError("write lock not supported for remote branch %s" % self.baseurl) def unlock(self): pass def relpath(self, path): if not path.startswith(self.baseurl): raise BzrError('path %r is not under base URL %r' % (path, self.baseurl)) pl = len(self.baseurl) return path[pl:].lstrip('/') def get_revision(self, revision_id): from bzrlib.revision import Revision from bzrlib.xml import unpack_xml revf = self.revision_store[revision_id] r = unpack_xml(Revision, revf) if r.revision_id != revision_id: raise BzrCheckError('revision stored as {%s} actually contains {%s}' % (revision_id, r.revision_id)) return r class RemoteStore(object): def __init__(self, baseurl): self._baseurl = baseurl def _path(self, name): if '/' in name: raise ValueError('invalid store id', name) return self._baseurl + '/' + name def __getitem__(self, fileid): p = self._path(fileid) return get_url(p, compressed=True) commit refs/heads/master mark :804 committer Martin Pool 1120013733 +1000 data 55 Patch from John: - StringIO mixes poorly with difflib from :803 M 644 inline bzrlib/diff.py data 15108 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from trace import mutter from errors import BzrError # TODO: Rather than building a changeset object, we should probably # invoke callbacks on an object. That object can either accumulate a # list, write them out directly, etc etc. def internal_diff(old_label, oldlines, new_label, newlines, to_file): import difflib # FIXME: difflib is wrong if there is no trailing newline. # The syntax used by patch seems to be "\ No newline at # end of file" following the last diff line from that # file. This is not trivial to insert into the # unified_diff output and it might be better to just fix # or replace that function. # In the meantime we at least make sure the patch isn't # mangled. # Special workaround for Python2.3, where difflib fails if # both sequences are empty. if not oldlines and not newlines: return nonl = False if oldlines and (oldlines[-1][-1] != '\n'): oldlines[-1] += '\n' nonl = True if newlines and (newlines[-1][-1] != '\n'): newlines[-1] += '\n' nonl = True ud = difflib.unified_diff(oldlines, newlines, fromfile=old_label, tofile=new_label) # work-around for difflib being too smart for its own good # if /dev/null is "1,0", patch won't recognize it as /dev/null if not oldlines: ud = list(ud) ud[2] = ud[2].replace('-1,0', '-0,0') elif not newlines: ud = list(ud) ud[2] = ud[2].replace('+1,0', '+0,0') for line in ud: to_file.write(line) if nonl: print >>to_file, "\\ No newline at end of file" print >>to_file def external_diff(old_label, oldlines, new_label, newlines, to_file, diff_opts): """Display a diff by calling out to the external diff program.""" import sys if to_file != sys.stdout: raise NotImplementedError("sorry, can't send external diff other than to stdout yet", to_file) # make sure our own output is properly ordered before the diff to_file.flush() from tempfile import NamedTemporaryFile import os oldtmpf = NamedTemporaryFile() newtmpf = NamedTemporaryFile() try: # TODO: perhaps a special case for comparing to or from the empty # sequence; can just use /dev/null on Unix # TODO: if either of the files being compared already exists as a # regular named file (e.g. in the working directory) then we can # compare directly to that, rather than copying it. oldtmpf.writelines(oldlines) newtmpf.writelines(newlines) oldtmpf.flush() newtmpf.flush() if not diff_opts: diff_opts = [] diffcmd = ['diff', '--label', old_label, oldtmpf.name, '--label', new_label, newtmpf.name] # diff only allows one style to be specified; they don't override. # note that some of these take optargs, and the optargs can be # directly appended to the options. # this is only an approximate parser; it doesn't properly understand # the grammar. for s in ['-c', '-u', '-C', '-U', '-e', '--ed', '-q', '--brief', '--normal', '-n', '--rcs', '-y', '--side-by-side', '-D', '--ifdef']: for j in diff_opts: if j.startswith(s): break else: continue break else: diffcmd.append('-u') if diff_opts: diffcmd.extend(diff_opts) rc = os.spawnvp(os.P_WAIT, 'diff', diffcmd) if rc != 0 and rc != 1: # returns 1 if files differ; that's OK if rc < 0: msg = 'signal %d' % (-rc) else: msg = 'exit code %d' % rc raise BzrError('external diff failed with %s; command: %r' % (rc, diffcmd)) finally: oldtmpf.close() # and delete newtmpf.close() def show_diff(b, revision, specific_files, external_diff_options=None): """Shortcut for showing the diff to the working tree. b Branch. revision None for each, or otherwise the old revision to compare against. The more general form is show_diff_trees(), where the caller supplies any two trees. """ import sys if revision == None: old_tree = b.basis_tree() else: old_tree = b.revision_tree(b.lookup_revision(revision)) new_tree = b.working_tree() show_diff_trees(old_tree, new_tree, sys.stdout, specific_files, external_diff_options) def show_diff_trees(old_tree, new_tree, to_file, specific_files=None, external_diff_options=None): """Show in text form the changes from one tree to another. to_files If set, include only changes to these files. external_diff_options If set, use an external GNU diff and pass these options. """ # TODO: Options to control putting on a prefix or suffix, perhaps as a format string old_label = '' new_label = '' DEVNULL = '/dev/null' # Windows users, don't panic about this filename -- it is a # special signal to GNU patch that the file should be created or # deleted respectively. # TODO: Generation of pseudo-diffs for added/deleted files could # be usefully made into a much faster special case. if external_diff_options: assert isinstance(external_diff_options, basestring) opts = external_diff_options.split() def diff_file(olab, olines, nlab, nlines, to_file): external_diff(olab, olines, nlab, nlines, to_file, opts) else: diff_file = internal_diff delta = compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=specific_files) for path, file_id, kind in delta.removed: print >>to_file, '*** removed %s %r' % (kind, path) if kind == 'file': diff_file(old_label + path, old_tree.get_file(file_id).readlines(), DEVNULL, [], to_file) for path, file_id, kind in delta.added: print >>to_file, '*** added %s %r' % (kind, path) if kind == 'file': diff_file(DEVNULL, [], new_label + path, new_tree.get_file(file_id).readlines(), to_file) for old_path, new_path, file_id, kind, text_modified in delta.renamed: print >>to_file, '*** renamed %s %r => %r' % (kind, old_path, new_path) if text_modified: diff_file(old_label + old_path, old_tree.get_file(file_id).readlines(), new_label + new_path, new_tree.get_file(file_id).readlines(), to_file) for path, file_id, kind in delta.modified: print >>to_file, '*** modified %s %r' % (kind, path) if kind == 'file': diff_file(old_label + path, old_tree.get_file(file_id).readlines(), new_label + path, new_tree.get_file(file_id).readlines(), to_file) class TreeDelta(object): """Describes changes from one tree to another. Contains four lists: added (path, id, kind) removed (path, id, kind) renamed (oldpath, newpath, id, kind, text_modified) modified (path, id, kind) unchanged (path, id, kind) Each id is listed only once. Files that are both modified and renamed are listed only in renamed, with the text_modified flag true. Files are only considered renamed if their name has changed or their parent directory has changed. Renaming a directory does not count as renaming all its contents. The lists are normally sorted when the delta is created. """ def __init__(self): self.added = [] self.removed = [] self.renamed = [] self.modified = [] self.unchanged = [] def __eq__(self, other): if not isinstance(other, TreeDelta): return False return self.added == other.added \ and self.removed == other.removed \ and self.renamed == other.renamed \ and self.modified == other.modified \ and self.unchanged == other.unchanged def __ne__(self, other): return not (self == other) def __repr__(self): return "TreeDelta(added=%r, removed=%r, renamed=%r, modified=%r," \ " unchanged=%r)" % (self.added, self.removed, self.renamed, self.modified, self.unchanged) def has_changed(self): changes = len(self.added) + len(self.removed) + len(self.renamed) changes += len(self.modified) return (changes != 0) def touches_file_id(self, file_id): """Return True if file_id is modified by this delta.""" for l in self.added, self.removed, self.modified: for v in l: if v[1] == file_id: return True for v in self.renamed: if v[2] == file_id: return True return False def show(self, to_file, show_ids=False, show_unchanged=False): def show_list(files): for path, fid, kind in files: if kind == 'directory': path += '/' elif kind == 'symlink': path += '@' if show_ids: print >>to_file, ' %-30s %s' % (path, fid) else: print >>to_file, ' ', path if self.removed: print >>to_file, 'removed:' show_list(self.removed) if self.added: print >>to_file, 'added:' show_list(self.added) if self.renamed: print >>to_file, 'renamed:' for oldpath, newpath, fid, kind, text_modified in self.renamed: if show_ids: print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid) else: print >>to_file, ' %s => %s' % (oldpath, newpath) if self.modified: print >>to_file, 'modified:' show_list(self.modified) if show_unchanged and self.unchanged: print >>to_file, 'unchanged:' show_list(self.unchanged) def compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=None): """Describe changes from one tree to another. Returns a TreeDelta with details of added, modified, renamed, and deleted entries. The root entry is specifically exempt. This only considers versioned files. want_unchanged If true, also list files unchanged from one version to the next. specific_files If true, only check for changes to specified names or files within them. """ from osutils import is_inside_any old_inv = old_tree.inventory new_inv = new_tree.inventory delta = TreeDelta() mutter('start compare_trees') # TODO: match for specific files can be rather smarter by finding # the IDs of those files up front and then considering only that. for file_id in old_tree: if file_id in new_tree: kind = old_inv.get_file_kind(file_id) assert kind == new_inv.get_file_kind(file_id) assert kind in ('file', 'directory', 'symlink', 'root_directory'), \ 'invalid file kind %r' % kind if kind == 'root_directory': continue old_path = old_inv.id2path(file_id) new_path = new_inv.id2path(file_id) old_ie = old_inv[file_id] new_ie = new_inv[file_id] if specific_files: if (not is_inside_any(specific_files, old_path) and not is_inside_any(specific_files, new_path)): continue if kind == 'file': old_sha1 = old_tree.get_file_sha1(file_id) new_sha1 = new_tree.get_file_sha1(file_id) text_modified = (old_sha1 != new_sha1) else: ## mutter("no text to check for %r %r" % (file_id, kind)) text_modified = False # TODO: Can possibly avoid calculating path strings if the # two files are unchanged and their names and parents are # the same and the parents are unchanged all the way up. # May not be worthwhile. if (old_ie.name != new_ie.name or old_ie.parent_id != new_ie.parent_id): delta.renamed.append((old_path, new_path, file_id, kind, text_modified)) elif text_modified: delta.modified.append((new_path, file_id, kind)) elif want_unchanged: delta.unchanged.append((new_path, file_id, kind)) else: kind = old_inv.get_file_kind(file_id) old_path = old_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, old_path): continue delta.removed.append((old_path, file_id, kind)) mutter('start looking for new files') for file_id in new_inv: if file_id in old_inv: continue new_path = new_inv.id2path(file_id) if specific_files: if not is_inside_any(specific_files, new_path): continue kind = new_inv.get_file_kind(file_id) delta.added.append((new_path, file_id, kind)) delta.removed.sort() delta.added.sort() delta.renamed.sort() delta.modified.sort() delta.unchanged.sort() return delta commit refs/heads/master mark :805 committer Martin Pool 1120018300 +1000 data 169 Merge John's log patch: implements bzr log --forward --verbose optimizes so that only logs to be printed are read (rather than reading all and filtering out unwanted). from :804 M 644 inline bzrlib/log.py data 9397 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Code to show logs of changes. Various flavors of log can be produced: * for one file, or the whole tree, and (not done yet) for files in a given directory * in "verbose" mode with a description of what changed from one version to the next * with file-ids and revision-ids shown * from last to first or (not anymore) from first to last; the default is "reversed" because it shows the likely most relevant and interesting information first * (not yet) in XML format """ from trace import mutter def find_touching_revisions(branch, file_id): """Yield a description of revisions which affect the file_id. Each returned element is (revno, revision_id, description) This is the list of revisions where the file is either added, modified, renamed or deleted. TODO: Perhaps some way to limit this to only particular revisions, or to traverse a non-mainline set of revisions? """ last_ie = None last_path = None revno = 1 for revision_id in branch.revision_history(): this_inv = branch.get_revision_inventory(revision_id) if file_id in this_inv: this_ie = this_inv[file_id] this_path = this_inv.id2path(file_id) else: this_ie = this_path = None # now we know how it was last time, and how it is in this revision. # are those two states effectively the same or not? if not this_ie and not last_ie: # not present in either pass elif this_ie and not last_ie: yield revno, revision_id, "added " + this_path elif not this_ie and last_ie: # deleted here yield revno, revision_id, "deleted " + last_path elif this_path != last_path: yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path)) elif (this_ie.text_size != last_ie.text_size or this_ie.text_sha1 != last_ie.text_sha1): yield revno, revision_id, "modified " + this_path last_ie = this_ie last_path = this_path revno += 1 def show_log(branch, lf, specific_fileid=None, verbose=False, direction='reverse', start_revision=None, end_revision=None): """Write out human-readable log of commits to this branch. lf LogFormatter object to show the output. specific_fileid If true, list only the commits affecting the specified file, rather than all commits. verbose If true show added/changed/deleted/renamed files. direction 'reverse' (default) is latest to earliest; 'forward' is earliest to latest. start_revision If not None, only show revisions >= start_revision end_revision If not None, only show revisions <= end_revision """ from bzrlib.osutils import format_date from bzrlib.errors import BzrCheckError from bzrlib.textui import show_status from warnings import warn if not isinstance(lf, LogFormatter): warn("not a LogFormatter instance: %r" % lf) if specific_fileid: mutter('get log for file_id %r' % specific_fileid) which_revs = branch.enum_history(direction) which_revs = [x for x in which_revs if ( (start_revision is None or x[0] >= start_revision) and (end_revision is None or x[0] <= end_revision))] if not (verbose or specific_fileid): # no need to know what changed between revisions with_deltas = deltas_for_log_dummy(branch, which_revs) elif direction == 'reverse': with_deltas = deltas_for_log_reverse(branch, which_revs) else: with_deltas = deltas_for_log_forward(branch, which_revs) for revno, rev, delta in with_deltas: if specific_fileid: if not delta.touches_file_id(specific_fileid): continue if not verbose: # although we calculated it, throw it away without display delta = None lf.show(revno, rev, delta) def deltas_for_log_dummy(branch, which_revs): for revno, revision_id in which_revs: yield revno, branch.get_revision(revision_id), None def deltas_for_log_reverse(branch, which_revs): """Compute deltas for display in reverse log. Given a sequence of (revno, revision_id) pairs, return (revno, rev, delta). The delta is from the given revision to the next one in the sequence, which makes sense if the log is being displayed from newest to oldest. """ from tree import EmptyTree from diff import compare_trees last_revno = last_revision_id = last_tree = None for revno, revision_id in which_revs: this_tree = branch.revision_tree(revision_id) this_revision = branch.get_revision(revision_id) if last_revno: yield last_revno, last_revision, compare_trees(this_tree, last_tree, False) last_revno = revno last_revision = this_revision last_tree = this_tree if last_revno: if last_revno == 1: this_tree = EmptyTree() else: this_revno = last_revno - 1 this_revision_id = branch.revision_history()[this_revno] this_tree = branch.revision_tree(this_revision_id) yield last_revno, last_revision, compare_trees(this_tree, last_tree, False) def deltas_for_log_forward(branch, which_revs): """Compute deltas for display in forward log. Given a sequence of (revno, revision_id) pairs, return (revno, rev, delta). The delta is from the given revision to the next one in the sequence, which makes sense if the log is being displayed from newest to oldest. """ from tree import EmptyTree from diff import compare_trees last_revno = last_revision_id = last_tree = None for revno, revision_id in which_revs: this_tree = branch.revision_tree(revision_id) this_revision = branch.get_revision(revision_id) if not last_revno: if revno == 1: last_tree = EmptyTree() else: last_revno = revno - 1 last_revision_id = branch.revision_history()[last_revno] last_tree = branch.revision_tree(last_revision_id) yield revno, this_revision, compare_trees(last_tree, this_tree, False) last_revno = revno last_revision = this_revision last_tree = this_tree class LogFormatter(object): """Abstract class to display log messages.""" def __init__(self, to_file, show_ids=False, show_timezone=False): self.to_file = to_file self.show_ids = show_ids self.show_timezone = show_timezone class LongLogFormatter(LogFormatter): def show(self, revno, rev, delta): from osutils import format_date to_file = self.to_file print >>to_file, '-' * 60 print >>to_file, 'revno:', revno if self.show_ids: print >>to_file, 'revision-id:', rev.revision_id print >>to_file, 'committer:', rev.committer print >>to_file, 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0, self.show_timezone)) print >>to_file, 'message:' if not rev.message: print >>to_file, ' (no message)' else: for l in rev.message.split('\n'): print >>to_file, ' ' + l if delta != None: delta.show(to_file, self.show_ids) class ShortLogFormatter(LogFormatter): def show(self, revno, rev, delta): from bzrlib.osutils import format_date to_file = self.to_file print >>to_file, "%5d %s\t%s" % (revno, rev.committer, format_date(rev.timestamp, rev.timezone or 0, self.show_timezone)) if self.show_ids: print >>to_file, ' revision-id:', rev.revision_id if not rev.message: print >>to_file, ' (no message)' else: for l in rev.message.split('\n'): print >>to_file, ' ' + l if delta != None: delta.show(to_file, self.show_ids) print FORMATTERS = {'long': LongLogFormatter, 'short': ShortLogFormatter, } def log_formatter(name, *args, **kwargs): from bzrlib.errors import BzrCommandError try: return FORMATTERS[name](*args, **kwargs) except IndexError: raise BzrCommandError("unknown log formatter: %r" % name) commit refs/heads/master mark :806 committer Martin Pool 1120095506 +1000 data 78 - Cope if locale.getpreferredencoding() returns None Reported by Uros Trebec from :805 M 644 inline bzrlib/__init__.py data 1917 # (C) 2005 Canonical Development Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr library""" from branch import Branch, ScratchBranch, find_branch from errors import BzrError BZRDIR = ".bzr" DEFAULT_IGNORE = ['.bzr.log', '*~', '#*#', '*$', '.#*', '.*.sw[nop]', '.*.tmp', '*.tmp', '*.bak', '*.BAK', '*.orig', '*.o', '*.obj', '*.a', '*.py[oc]', '*.so', '*.exe', '*.elc', '{arch}', 'CVS', 'CVS.adm', '.svn', '_darcs', 'SCCS', 'RCS', '*,v', 'BitKeeper', '.git', 'TAGS', '.make.state', '.sconsign', '.tmp*', '.del-*'] IGNORE_FILENAME = ".bzrignore" import locale user_encoding = locale.getpreferredencoding() or 'ascii' del locale __copyright__ = "Copyright 2005 Canonical Development Ltd." __author__ = "Martin Pool " __version__ = '0.0.5' def get_bzr_revision(): """If bzr is run from a branch, return (revno,revid) or None""" try: branch = Branch(__path__[0]) rh = branch.revision_history() if rh: return len(rh), rh[-1] else: return None except BzrError: return None commit refs/heads/master mark :807 committer Martin Pool 1120453979 +1000 data 52 - New log --long option Patch from William Dodé from :806 M 644 inline bzrlib/commands.py data 53550 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import BzrError, BzrCheckError, BzrCommandError from bzrlib.branch import find_branch from bzrlib import BZRDIR plugin_cmds = {} def register_command(cmd): "Utility function to help register a command" global plugin_cmds k = cmd.__name__ if k.startswith("cmd_"): k_unsquished = _unsquish_command_name(k) else: k_unsquished = k if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = cmd else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r' % sys.modules[cmd.__module__]) def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _get_cmd_dict(plugins_override=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v # If we didn't load plugins, the plugin_cmds dict will be empty if plugins_override: d.update(plugin_cmds) else: d2 = plugin_cmds.copy() d2.update(d) d = d2 return d def get_all_cmds(plugins_override=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(plugins_override=plugins_override).iteritems(): yield k,v def get_cmd_class(cmd, plugins_override=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(plugins_override=plugins_override) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() for opt in self.takes_options: if not opt in OPTIONS: raise BzrError("Unknown option '%s' returned by external command %s" % (opt, path)) # TODO: Is there any way to check takes_args is valid here? self.takes_args = pipe.readline().split() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-usage'" % path) pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-help'" % path) def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: optname = name.replace('_','-') value = kargs[name] if OPTIONS.has_key(optname): # it's an option opts.append('--%s' % optname) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = find_branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): from bzrlib.xml import pack_xml pack_xml(find_branch('.').get_revision(revision_id), sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print find_branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): from bzrlib.add import smart_add smart_add(file_list, verbose, not no_recurse) class cmd_mkdir(Command): """Create a new versioned directory. This is equivalent to creating the directory and then adding it. """ takes_args = ['dir+'] def run(self, dir_list): b = None for d in dir_list: os.mkdir(d) if not b: b = find_branch(d) b.add([d], verbose=True) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print find_branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = find_branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = find_branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = find_branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import tempfile from shutil import rmtree import errno br_to = find_branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if e.errno != errno.ENOENT: raise if location is None: if stored_loc is None: raise BzrCommandError("No pull location known or specified.") else: print "Using last location: %s" % stored_loc location = stored_loc cache_root = tempfile.mkdtemp() from bzrlib.branch import DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: from branch import find_cached_branch, DivergedBranches br_from = find_cached_branch(location, cache_root) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged." " Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") finally: rmtree(cache_root) class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. To retrieve the branch as of a particular revision, supply the --revision parameter, as in "branch foo/bar -r 5". """ takes_args = ['from_location', 'to_location?'] takes_options = ['revision'] def run(self, from_location, to_location=None, revision=None): import errno from bzrlib.merge import merge from bzrlib.branch import DivergedBranches, NoSuchRevision, \ find_cached_branch, Branch from shutil import rmtree from meta_store import CachedStore import tempfile cache_root = tempfile.mkdtemp() try: try: br_from = find_cached_branch(from_location, cache_root) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not' ' exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location.rstrip("/\\")) try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already' ' exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) try: br_to.update_revisions(br_from, stop_revision=revision) except NoSuchRevision: rmtree(to_location) msg = "The branch %s has no revision %d." % (from_location, revision) raise BzrCommandError(msg) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False, ignore_zero=True) from_location = pull_loc(br_from) br_to.controlfile("x-pull", "wb").write(from_location + "\n") finally: rmtree(cache_root) def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = find_branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = find_branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = find_branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: raise BzrError("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = find_branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: raise BzrError("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in find_branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in find_branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): from bzrlib.branch import Branch Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = find_branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = find_branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): import statcache b = find_branch('.') inv = b.read_working_inventory() sc = statcache.update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[statcache.SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = find_branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision','long'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None, long=False): from bzrlib.branch import find_branch from bzrlib.log import log_formatter, show_log import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') if long: log_format = 'long' else: log_format = 'short' lf = log_formatter(log_format, show_ids=show_ids, to_file=outf, show_timezone=timezone) show_log(b, lf, file_id, verbose=verbose, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = find_branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = find_branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): from bzrlib.osutils import quotefn for f in find_branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = find_branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = find_branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print find_branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, exports to a directory (equivalent to --format=dir).""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format'] def run(self, dest, revision=None, format='dir'): b = find_branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest, format) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = find_branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit from bzrlib.osutils import get_text_message ## Warning: shadows builtin file() if not message and not file: import cStringIO stdout = sys.stdout catcher = cStringIO.StringIO() sys.stdout = catcher cmd_status({"file_list":selected_list}, {}) info = catcher.getvalue() sys.stdout = stdout message = get_text_message(info) if message is None: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = find_branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.check import check check(find_branch(dir)) class cmd_upgrade(Command): """Upgrade branch storage to current format. This should normally be used only after the check command tells you to run it. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.upgrade import upgrade upgrade(find_branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest return int(not selftest()) class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Restore selected files from a previous revision. """ takes_args = ['file+'] def run(self, file_list): from bzrlib.branch import find_branch if not file_list: file_list = ['.'] b = find_branch(file_list[0]) b.revert([b.relpath(f) for f in file_list]) class cmd_merge_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): import statcache b = find_branch('.') statcache.update_cache(b.base, b.read_working_inventory()) class cmd_plugins(Command): """List plugins""" hidden = True def run(self): import bzrlib.plugin from pprint import pprint pprint(bzrlib.plugin.all_plugins) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'update': None, 'long': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', 'l': 'long', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) >>> parse_args('log -r 500'.split()) (['log'], {'revision': 500}) >>> parse_args('log -r500:600'.split()) (['log'], {'revision': [500, 600]}) >>> parse_args('log -vr500:600'.split()) (['log'], {'verbose': True, 'revision': [500, 600]}) >>> parse_args('log -rv500:600'.split()) #the r takes an argument Traceback (most recent call last): ... ValueError: invalid literal for int(): v500 """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: raise BzrError('unknown long option %r' % a) else: shortopt = a[1:] if shortopt in SHORT_OPTIONS: # Multi-character options must have a space to delimit # their value optname = SHORT_OPTIONS[shortopt] else: # Single character short options, can be chained, # and have their value appended to their name shortopt = a[1:2] if shortopt not in SHORT_OPTIONS: # We didn't find the multi-character name, and we # didn't find the single char name raise BzrError('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if a[2:]: # There are extra things on this option # see if it is the value, or if it is another # short option optargfn = OPTIONS[optname] if optargfn is None: # This option does not take an argument, so the # next entry is another short option, pack it back # into the list argv.insert(0, '-' + a[2:]) else: # This option takes an argument, so pack it # into the array optarg = a[2:] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? raise BzrError('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: raise BzrError('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: raise BzrError('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def _parse_master_args(argv): """Parse the arguments that always go with the original command. These are things like bzr --no-plugins, etc. There are now 2 types of option flags. Ones that come *before* the command, and ones that come *after* the command. Ones coming *before* the command are applied against all possible commands. And are generally applied before plugins are loaded. The current list are: --builtin Allow plugins to load, but don't let them override builtin commands, they will still be allowed if they do not override a builtin. --no-plugins Don't load any plugins. This lets you get back to official source behavior. --profile Enable the hotspot profile before running the command. For backwards compatibility, this is also a non-master option. --version Spit out the version of bzr that is running and exit. This is also a non-master option. --help Run help and exit, also a non-master option (I think that should stay, though) >>> argv, opts = _parse_master_args(['bzr', '--test']) Traceback (most recent call last): ... BzrCommandError: Invalid master option: 'test' >>> argv, opts = _parse_master_args(['bzr', '--version', 'command']) >>> print argv ['command'] >>> print opts['version'] True >>> argv, opts = _parse_master_args(['bzr', '--profile', 'command', '--more-options']) >>> print argv ['command', '--more-options'] >>> print opts['profile'] True >>> argv, opts = _parse_master_args(['bzr', '--no-plugins', 'command']) >>> print argv ['command'] >>> print opts['no-plugins'] True >>> print opts['profile'] False >>> argv, opts = _parse_master_args(['bzr', 'command', '--profile']) >>> print argv ['command', '--profile'] >>> print opts['profile'] False """ master_opts = {'builtin':False, 'no-plugins':False, 'version':False, 'profile':False, 'help':False } # This is the point where we could hook into argv[0] to determine # what front-end is supposed to be run # For now, we are just ignoring it. cmd_name = argv.pop(0) for arg in argv[:]: if arg[:2] != '--': # at the first non-option, we return the rest break arg = arg[2:] # Remove '--' if arg not in master_opts: # We could say that this is not an error, that we should # just let it be handled by the main section instead raise BzrCommandError('Invalid master option: %r' % arg) argv.pop(0) # We are consuming this entry master_opts[arg] = True return argv, master_opts def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: # some options like --builtin and --no-plugins have special effects argv, master_opts = _parse_master_args(argv) if not master_opts['no-plugins']: from bzrlib.plugin import load_plugins load_plugins() args, opts = parse_args(argv) if master_opts['help']: from bzrlib.help import help if argv: help(argv[0]) else: help() return 0 if 'help' in opts: from bzrlib.help import help if args: help(args[0]) else: help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 plugins_override = not (master_opts['builtin']) canonical_cmd, cmd_class = get_cmd_class(cmd, plugins_override=plugins_override) profile = master_opts['profile'] # For backwards compatibility, I would rather stick with --profile being a # master/global option if 'profile' in opts: profile = True del opts['profile'] # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): bzrlib.trace.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: import errno quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :808 committer Martin Pool 1120454831 +1000 data 29 - Note new --long log options from :807 M 644 inline NEWS data 9006 DEVELOPMENT HEAD NEW FEATURES: * Python plugins, automatically loaded from the directories on BZR_PLUGIN_PATH or ~/.bzr.conf/plugins by default. * New 'bzr mkdir' command. * Commit mesage is fetched from an editor if not given on the command line; patch from Torsten Marek. CHANGES: * New ``bzr upgrade`` command to upgrade the format of a branch, replacing ``bzr check --update``. * Files within store directories are no longer marked readonly on disk. * Changed ``bzr log`` output to a more compact form suggested by John A Meinel. Old format is available with the ``--long`` or ``-l`` option, patched by William Dodé. bzr-0.0.5 2005-06-15 CHANGES: * ``bzr`` with no command now shows help rather than giving an error. Suggested by Michael Ellerman. * ``bzr status`` output format changed, because svn-style output doesn't really match the model of bzr. Now files are grouped by status and can be shown with their IDs. ``bzr status --all`` shows all versioned files and unknown files but not ignored files. * ``bzr log`` runs from most-recent to least-recent, the reverse of the previous order. The previous behaviour can be obtained with the ``--forward`` option. * ``bzr inventory`` by default shows only filenames, and also ids if ``--show-ids`` is given, in which case the id is the second field. ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New ``bzr ignore PATTERN`` command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.sw[nop] .git .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. File may be in a branch other than the working directory. * ``bzr log`` and ``bzr root`` can be given an http URL instead of a filename. * Commands can now be defined by external programs or scripts in a directory on $BZRPATH. * New "stat cache" avoids reading the contents of files if they haven't changed since the previous time. * If the Python interpreter is too old, try to find a better one or give an error. Based on a patch from Fredrik Lundh. * New optional parameter ``bzr info [BRANCH]``. * New form ``bzr commit SELECTED`` to commit only selected files. * New form ``bzr log -r FROM:TO`` shows changes in selected range; contributed by John A Meinel. * New option ``bzr diff --diff-options 'OPTS'`` allows passing options through to an external GNU diff. * New option ``bzr add --no-recurse`` to add a directory but not their contents. * ``bzr --version`` now shows more information if bzr is being run from a branch. BUG FIXES: * Fixed diff format so that added and removed files will be handled properly by patch. Fix from Lalo Martins. * Various fixes for files whose names contain spaces or other metacharacters. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. * testbzr requires python2.4, but can be used to test bzr running under a different version. * Tests added for many other changes in this release. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. * New developer commands ``added``, ``modified``. PORTABILITY: * Cope on Windows on python2.3 by using the weaker random seed. 2.4 is now only recommended. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. commit refs/heads/master mark :809 committer Martin Pool 1120454996 +1000 data 38 - stubbed out call to merge_core tests from :808 M 644 inline bzrlib/selftest/__init__.py data 9135 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") raise class CommandFailed(Exception): pass class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr, so that we can run it # through a specified Python intepreter OVERRIDE_PYTHON = None # to run with alternative python 'python' BZRPATH = 'bzr' _log_buf = "" def formcmd(self, cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = self.BZRPATH if self.OVERRIDE_PYTHON: cmd.insert(0, self.OVERRIDE_PYTHON) self.log('$ %r' % cmd) return cmd def runcmd(self, cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = self.formcmd(cmd) self.log('$ ' + ' '.join(cmd)) actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(self, cmd, retcode=0): """Run a command and return its output""" cmd = self.formcmd(cmd) child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG) outd, errd = child.communicate() self.log(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def build_tree(self, shape): """Build a test tree according to a pattern. shape is a sequence of file specifications. If the final character is '/', a directory is created. This doesn't add anything to a branch. """ # XXX: It's OK to just create them using forward slashes on windows? import os for name in shape: assert isinstance(name, basestring) if name[-1] == '/': os.mkdir(name[:-1]) else: f = file(name, 'wt') print >>f, "contents of", name f.close() def log(self, msg): """Log a message to a progress file""" self._log_buf = self._log_buf + str(msg) + '\n' print >>self.TEST_LOG, msg def check_inventory_shape(self, inv, shape): """ Compare an inventory to a list of expected names. Fail if they are not precisely equal. """ extras = [] shape = list(shape) # copy for path, ie in inv.entries(): name = path.replace('\\', '/') if ie.kind == 'dir': name = name + '/' if name in shape: shape.remove(name) else: extras.append(name) if shape: self.fail("expected paths not found in inventory: %r" % shape) if extras: self.fail("unexpected paths found in inventory: %r" % extras) def check_file_contents(self, filename, expect): self.log("check contents of file %s" % filename) contents = file(filename, 'r').read() if contents != expect: self.log("expected: %r" % expected) self.log("actually: %r" % contents) self.fail("contents of %s not as expected") class InTempDir(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.test_dir = os.path.join(self.TEST_ROOT, self.__class__.__name__) os.mkdir(self.test_dir) os.chdir(self.test_dir) def tearDown(self): import os os.chdir(self.TEST_ROOT) class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ def __init__(self, out): self.out = out TestResult.__init__(self) def startTest(self, test): # TODO: Maybe show test.shortDescription somewhere? print >>self.out, '%-60.60s' % test.id(), self.out.flush() TestResult.startTest(self, test) def stopTest(self, test): # print TestResult.stopTest(self, test) def addError(self, test, err): print >>self.out, 'ERROR' TestResult.addError(self, test, err) _show_test_failure('error', test, err, self.out) def addFailure(self, test, err): print >>self.out, 'FAILURE' TestResult.addFailure(self, test, err) _show_test_failure('failure', test, err, self.out) def addSuccess(self, test): print >>self.out, 'OK' TestResult.addSuccess(self, test) def selftest(): from unittest import TestLoader, TestSuite import bzrlib, bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, bzrlib.commands import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning import bzrlib.merge_core from doctest import DocTestSuite import os import shutil import time import sys TestBase.BZRPATH = os.path.join(os.path.realpath(os.path.dirname(bzrlib.__path__[0])), 'bzr') print '%-30s %s' % ('bzr binary', TestBase.BZRPATH) _setup_test_log() _setup_test_dir() print suite = TestSuite() tl = TestLoader() # should also test bzrlib.merge_core, but they seem to be out of date with # the code. for m in bzrlib.selftest.whitebox, \ bzrlib.selftest.versioning: suite.addTest(tl.loadTestsFromModule(m)) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands: suite.addTest(DocTestSuite(m)) suite.addTest(bzrlib.selftest.blackbox.suite()) # save stdout & stderr so there's no leakage from code-under-test real_stdout = sys.stdout real_stderr = sys.stderr sys.stdout = sys.stderr = TestBase.TEST_LOG try: result = _MyResult(real_stdout) suite.run(result) finally: sys.stdout = real_stdout sys.stderr = real_stderr _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os log_filename = os.path.abspath('testbzr.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "bzr tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_ROOT = os.path.abspath("testbzr.tmp") print '%-30s %s' % ('running tests in', TestBase.TEST_ROOT) if os.path.exists(TestBase.TEST_ROOT): shutil.rmtree(TestBase.TEST_ROOT) os.mkdir(TestBase.TEST_ROOT) os.chdir(TestBase.TEST_ROOT) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_ROOT, '.bzr')) def _show_results(result): print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, exc_info, out): from traceback import print_exception print >>out, '-' * 60 print >>out, case desc = case.shortDescription() if desc: print >>out, ' (%s)' % desc print_exception(exc_info[0], exc_info[1], exc_info[2], None, out) if isinstance(case, TestBase): print >>out print >>out, 'log from this test:' print >>out, case._log_buf print >>out, '-' * 60 commit refs/heads/master mark :810 committer Martin Pool 1120462412 +1000 data 35 - New validate_revision_id function from :809 M 644 inline bzrlib/revision.py data 6393 # (C) 2005 Canonical # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA class RevisionReference(object): """ Reference to a stored revision. Includes the revision_id and revision_sha1. """ revision_id = None revision_sha1 = None def __init__(self, revision_id, revision_sha1): if revision_id == None \ or isinstance(revision_id, basestring): self.revision_id = revision_id else: raise ValueError('bad revision_id %r' % revision_id) if revision_sha1 != None: if isinstance(revision_sha1, basestring) \ and len(revision_sha1) == 40: self.revision_sha1 = revision_sha1 else: raise ValueError('bad revision_sha1 %r' % revision_sha1) class Revision(object): """Single revision on a branch. Revisions may know their revision_hash, but only once they've been written out. This is not stored because you cannot write the hash into the file it describes. After bzr 0.0.5 revisions are allowed to have multiple parents. To support old clients this is written out in a slightly redundant form: the first parent as the predecessor. This will eventually be dropped. parents List of parent revisions, each is a RevisionReference. """ inventory_id = None inventory_sha1 = None revision_id = None timestamp = None message = None timezone = None committer = None def __init__(self, **args): self.__dict__.update(args) self.parents = [] def _get_precursor(self): from warnings import warn warn("Revision.precursor is deprecated", stacklevel=2) if self.parents: return self.parents[0].revision_id else: return None def _get_precursor_sha1(self): from warnings import warn warn("Revision.precursor_sha1 is deprecated", stacklevel=2) if self.parents: return self.parents[0].revision_sha1 else: return None def _fail(self): raise Exception("can't assign to precursor anymore") precursor = property(_get_precursor, _fail, _fail) precursor_sha1 = property(_get_precursor_sha1, _fail, _fail) def __repr__(self): return "" % self.revision_id def to_element(self): from bzrlib.xml import Element, SubElement root = Element('revision', committer = self.committer, timestamp = '%.9f' % self.timestamp, revision_id = self.revision_id, inventory_id = self.inventory_id, inventory_sha1 = self.inventory_sha1, ) if self.timezone: root.set('timezone', str(self.timezone)) root.text = '\n' msg = SubElement(root, 'message') msg.text = self.message msg.tail = '\n' if self.parents: # first parent stored as precursor for compatability with 0.0.5 and # earlier pr = self.parents[0] assert pr.revision_id root.set('precursor', pr.revision_id) if pr.revision_sha1: root.set('precursor_sha1', pr.revision_sha1) if self.parents: pelts = SubElement(root, 'parents') pelts.tail = pelts.text = '\n' for rr in self.parents: assert isinstance(rr, RevisionReference) p = SubElement(pelts, 'revision_ref') p.tail = '\n' assert rr.revision_id p.set('revision_id', rr.revision_id) if rr.revision_sha1: p.set('revision_sha1', rr.revision_sha1) return root def from_element(cls, elt): return unpack_revision(elt) from_element = classmethod(from_element) def unpack_revision(elt): """Convert XML element into Revision object.""" # is deprecated... from bzrlib.errors import BzrError if elt.tag not in ('revision', 'changeset'): raise BzrError("unexpected tag in revision file: %r" % elt) rev = Revision(committer = elt.get('committer'), timestamp = float(elt.get('timestamp')), revision_id = elt.get('revision_id'), inventory_id = elt.get('inventory_id'), inventory_sha1 = elt.get('inventory_sha1') ) precursor = elt.get('precursor') precursor_sha1 = elt.get('precursor_sha1') pelts = elt.find('parents') if precursor: # revisions written prior to 0.0.5 have a single precursor # give as an attribute rev_ref = RevisionReference(precursor, precursor_sha1) rev.parents.append(rev_ref) elif pelts: for p in pelts: assert p.tag == 'revision_ref', \ "bad parent node tag %r" % p.tag rev_ref = RevisionReference(p.get('revision_id'), p.get('revision_sha1')) rev.parents.append(rev_ref) v = elt.get('timezone') rev.timezone = v and int(v) rev.message = elt.findtext('message') # text of return rev REVISION_ID_RE = None def validate_revision_id(rid): """Check rid is syntactically valid for a revision id.""" global REVISION_ID_RE if not REVISION_ID_RE: import re REVISION_ID_RE = re.compile('[\w.-]+@[\w.-]+-+\d+-[0-9a-f]+\Z') if not REVISION_ID_RE.match(rid): raise ValueError("malformed revision-id %r" % rid) commit refs/heads/master mark :811 committer Martin Pool 1120462450 +1000 data 36 - Test case for validate_revision_id from :810 M 644 inline bzrlib/selftest/whitebox.py data 5209 #! /usr/bin/python import os import unittest from bzrlib.selftest import InTempDir, TestBase from bzrlib.branch import ScratchBranch, Branch from bzrlib.errors import NotBranchError, NotVersionedError class Unknowns(InTempDir): def runTest(self): b = Branch('.', init=True) self.build_tree(['hello.txt', 'hello.txt~']) self.assertEquals(list(b.unknowns()), ['hello.txt']) class ValidateRevisionId(TestBase): def runTest(self): from bzrlib.revision import validate_revision_id validate_revision_id('mbp@sourcefrog.net-20050311061123-96a255005c7c9dbe') self.assertRaises(ValueError, validate_revision_id, ' asdkjas') self.assertRaises(ValueError, validate_revision_id, 'mbp@sourcefrog.net-20050311061123-96a255005c7c9dbe\n') self.assertRaises(ValueError, validate_revision_id, ' mbp@sourcefrog.net-20050311061123-96a255005c7c9dbe') self.assertRaises(ValueError, validate_revision_id, 'Martin Pool -20050311061123-96a255005c7c9dbe') class Revert(InTempDir): """Test selected-file revert""" def runTest(self): b = Branch('.', init=True) self.build_tree(['hello.txt']) file('hello.txt', 'w').write('initial hello') self.assertRaises(NotVersionedError, b.revert, ['hello.txt']) b.add(['hello.txt']) b.commit('create initial hello.txt') self.check_file_contents('hello.txt', 'initial hello') file('hello.txt', 'w').write('new hello') self.check_file_contents('hello.txt', 'new hello') # revert file modified since last revision b.revert(['hello.txt']) self.check_file_contents('hello.txt', 'initial hello') self.check_file_contents('hello.txt~', 'new hello') # reverting again clobbers the backup b.revert(['hello.txt']) self.check_file_contents('hello.txt', 'initial hello') self.check_file_contents('hello.txt~', 'initial hello') class RenameDirs(InTempDir): """Test renaming directories and the files within them.""" def runTest(self): b = Branch('.', init=True) self.build_tree(['dir/', 'dir/sub/', 'dir/sub/file']) b.add(['dir', 'dir/sub', 'dir/sub/file']) b.commit('create initial state') # TODO: lift out to a test helper that checks the shape of # an inventory revid = b.revision_history()[0] self.log('first revision_id is {%s}' % revid) inv = b.get_revision_inventory(revid) self.log('contents of inventory: %r' % inv.entries()) self.check_inventory_shape(inv, ['dir', 'dir/sub', 'dir/sub/file']) b.rename_one('dir', 'newdir') self.check_inventory_shape(b.inventory, ['newdir', 'newdir/sub', 'newdir/sub/file']) b.rename_one('newdir/sub', 'newdir/newsub') self.check_inventory_shape(b.inventory, ['newdir', 'newdir/newsub', 'newdir/newsub/file']) class BranchPathTestCase(TestBase): """test for branch path lookups Branch.relpath and bzrlib.branch._relpath do a simple but subtle job: given a path (either relative to cwd or absolute), work out if it is inside a branch and return the path relative to the base. """ def runTest(self): from bzrlib.branch import _relpath import tempfile, shutil savedir = os.getcwdu() dtmp = tempfile.mkdtemp() def rp(p): return _relpath(dtmp, p) try: # check paths inside dtmp while standing outside it self.assertEqual(rp(os.path.join(dtmp, 'foo')), 'foo') # root = nothing self.assertEqual(rp(dtmp), '') self.assertRaises(NotBranchError, rp, '/etc') # now some near-miss operations -- note that # os.path.commonprefix gets these wrong! self.assertRaises(NotBranchError, rp, dtmp.rstrip('\\/') + '2') self.assertRaises(NotBranchError, rp, dtmp.rstrip('\\/') + '2/foo') # now operations based on relpath of files in current # directory, or nearby os.chdir(dtmp) self.assertEqual(rp('foo/bar/quux'), 'foo/bar/quux') self.assertEqual(rp('foo'), 'foo') self.assertEqual(rp('./foo'), 'foo') self.assertEqual(rp(os.path.abspath('foo')), 'foo') self.assertRaises(NotBranchError, rp, '../foo') finally: os.chdir(savedir) shutil.rmtree(dtmp) commit refs/heads/master mark :812 committer Martin Pool 1120462459 +1000 data 39 - rename control file to pending-merges from :811 M 644 inline doc/formats.txt data 8439 ***************** Bazaar-NG formats ***************** .. contents:: Since branches are working directories there is just a single directory format. There is one metadata directory called ``.bzr`` at the top of each tree. Control files inside ``.bzr`` are never touched by patches and should not normally be edited by the user. These files are designed so that repository-level operations are ACID without depending on atomic operations spanning multiple files. There are two particular cases: aborting a transaction in the middle, and contention from multiple processes. We also need to be careful to flush files to disk at appropriate points; even this may not be totally safe if the filesystem does not guarantee ordering between multiple file changes, so we need to be sure to roll back. The design must also be such that the directory can simply be copied and that hardlinked directories will work. (So we must always replace files, never just append.) A cache is kept under here of easily-accessible information about previous revisions. This should be under a single directory so that it can be easily identified, excluded from backups, removed, etc. This might contain pristine tree from previous revisions, manifests and inventories, etc. It might also contain working directories when building a commit, etc. Call this maybe ``cache`` or ``tmp``. I wonder if we should use .zip files for revisions and cacherevs rather than tar files so that random access is easier/more efficient. There is a Python library ``zipfile``. Signing XML files ***************** bzr relies on storing hashes or GPG signatures of various XML files. There can be multiple equivalent representations of the same XML tree, but these will have different byte-by-byte hashes. Once signed files are written out, they must be stored byte-for-byte and never re-encoded or renormalized, because that would break their hash or signature. Branch metadata *************** All inside ``.bzr`` ``README`` Tells people not to touch anything here. ``branch-format`` Identifies the parent as a Bazaar-NG branch; contains the overall branch metadata format as a string. ``pristine-directory`` Identifies that this is a pristine directory and may not be committed to. ``patches/`` Directory containing all patches applied to this branch, one per file. Patches are stored as compressed deltas. We also store the hash of the delta, hash of the before and after manifests, and optionally a GPG signature. ``cache/`` Contains various cached data that can be destroyed and will be recreated. (It should not be modified.) ``cache/pristine/`` Contains cached full trees for selected previous revisions, used when generating diffs, etc. ``cache/inventory/`` Contains cached inventories of previous revisions. ``cache/snapshot/`` Contains tarballs of cached revisions of the tree, named by their revision id. These can also be removed, but ``patch-history`` File containing the UUIDs of all patches taken in this branch, in the order they were taken. Each commit adds exactly one line to this file; lines are never removed or reordered. ``merged-patches`` List of foreign patches that have been merged into this branch. Must have no entries in common with ``patch-history``. Commits that include merges add to this file; lines are never removed or reordered. ``pending-merges`` List (one per line) of non-mainline revisions that have been merged and are waiting to be committed. ``branch-name`` User-qualified name of the branch, for the purpose of describing the origin of patches, e.g. ``mbp@sourcefrog.net/distcc--main``. ``friends`` List of branches from which we have pulled; file containing a list of pairs of branch-name and location. ``parent`` Default pull/push target. ``pending-inventory`` Mapping from UUIDs to file name in the current working directory. ``branch-lock`` Lock held while modifying the branch, to protect against clashing updates. Locking ******* Is locking a good strategy? Perhaps somekind of read-copy-update or seq-lock based mechanism would work better? If we do use a locking algorithm, is it OK to rely on filesystem locking or do we need our own mechanism? I think most hosts should have reasonable ``flock()`` or equivalent, even on NFS. One risk is that on NFS it is easy to have broken locking and not know it, so it might be better to have something that will fail safe. Filesystem locks go away if the machine crashes or the process is terminated; this can be a feature in that we do not need to deal with stale locks but also a feature in that the lock itself does not indicate cleanup may be needed. robertc points out that tla converged on renaming a directory as a mechanism: this is one thing which is known to be atomic on almost all filesystems. Apparently renaming files, creating directories, making symlinks etc are not good enough. Delta ***** XML document plus a bag of patches, expressing the difference between two revisions. May be a partial delta. * list of entries * entry * parent directory (if any) * before-name or null if new * after-name or null if deleted * uuid * type (dir, file, symlink, ...) * patch type (patch, full-text, xdelta, ...) * patch filename (?) Inventory ********* XML document; series of entries. (Quite similar to the svn ``entries`` file; perhaps should even have that name.) Stored identified by its hash. An inventory is stored for recorded revisions, also a ``pending-inventory`` for a working directory. Revision ******** XML document. Stored identified by its hash. committer RFC-2822-style name of the committer. Should match the key used to sign the revision. comment multi-line free-form text; whitespace and line breaks preserved timestamp As floating-point seconds since epoch. branch name Name of the branch to which this was originally committed. (I'm not totally satisfied that this is the right way to do it; the results will be a bit weird when a series of revisions pass through variously named branches.) inventory_hash Acts as a pointer to the inventory for this revision. parents Zero, one, or more references to parent revisions. For each the revision-id and the revision file's hash are given. The first parent is by convention the revision in whose working tree the new revision was created. precursor Must be equal to the first parent, if any are given. For compatibility with bzr 0.0.5 and earlier; eventually will be removed. merged-branches Revision ids of complete branches merged into this revision. If a revision is listed, that revision and transitively its predecessor and all other merged-branches are merged. This is empty except where cherry-picks have occurred. merged-patches Revision ids of cherry-picked patches. Patches whose branches are merged need not be listed here. Listing a revision ID implies that only the change of that particular revision from its predecessor has been merged in. This is empty except where cherry-picks have occurred. The transitive closure avoids Arch's problem of needing to list a large number of previous revisions. As ddaa writes: Continuation revisions (created by tla tag or baz branch) are associated to a patchlog whose New-patches header lists the revisions associated to all the patchlogs present in the tree. That was introduced as an optimisation so the set of patchlogs in any revision could be determined solely by examining the patchlogs of ancestor revisions in the same branch. This behaves well as long as the total count of patchlog is reasonably small or new branches are not very frequent. A continuation revision on $tree currently creates a patchlog of about 500K. This patchlog is present in all descendent of the revision, and all revisions that merges it. It may be useful at some times to keep a cache of all the branches, or all the revisions, present in the history of a branch, so that we do need to walk the whole history of the branch to build this list. ---- Proposed changes **************** * Don't store parent-id in all revisions, but rather have nodes that contain entries for children? * Assign an id to the root of the tree, perhaps listed in the top of the inventory? commit refs/heads/master mark :813 committer Martin Pool 1120462484 +1000 data 3 doc from :812 M 644 inline bzrlib/check.py data 6783 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def check(branch): """Run consistency checks on a branch. TODO: Also check non-mainline revisions mentioned as parents. TODO: Check for extra files in the control directory. """ from bzrlib.trace import mutter from bzrlib.errors import BzrCheckError from bzrlib.osutils import fingerprint_file from bzrlib.progress import ProgressBar branch.lock_read() try: pb = ProgressBar(show_spinner=True) last_rev_id = None missing_inventory_sha_cnt = 0 missing_revision_sha_cnt = 0 history = branch.revision_history() revno = 0 revcount = len(history) # for all texts checked, text_id -> sha1 checked_texts = {} for rev_id in history: revno += 1 pb.update('checking revision', revno, revcount) mutter(' revision {%s}' % rev_id) rev = branch.get_revision(rev_id) if rev.revision_id != rev_id: raise BzrCheckError('wrong internal revision id in revision {%s}' % rev_id) # check the previous history entry is a parent of this entry if rev.parents: if last_rev_id is None: raise BzrCheckError("revision {%s} has %d parents, but is the " "start of the branch" % (rev_id, len(rev.parents))) for prr in rev.parents: if prr.revision_id == last_rev_id: break else: raise BzrCheckError("previous revision {%s} not listed among " "parents of {%s}" % (last_rev_id, rev_id)) for prr in rev.parents: if prr.revision_sha1 is None: missing_revision_sha_cnt += 1 continue prid = prr.revision_id actual_sha = branch.get_revision_sha1(prid) if prr.revision_sha1 != actual_sha: raise BzrCheckError("mismatched revision sha1 for " "parent {%s} of {%s}: %s vs %s" % (prid, rev_id, prr.revision_sha1, actual_sha)) elif last_rev_id: raise BzrCheckError("revision {%s} has no parents listed but preceded " "by {%s}" % (rev_id, last_rev_id)) ## TODO: Check all the required fields are present on the revision. if rev.inventory_sha1: inv_sha1 = branch.get_inventory_sha1(rev.inventory_id) if inv_sha1 != rev.inventory_sha1: raise BzrCheckError('Inventory sha1 hash doesn\'t match' ' value in revision {%s}' % rev_id) else: missing_inventory_sha_cnt += 1 mutter("no inventory_sha1 on revision {%s}" % rev_id) inv = branch.get_inventory(rev.inventory_id) seen_ids = {} seen_names = {} ## p('revision %d/%d file ids' % (revno, revcount)) for file_id in inv: if file_id in seen_ids: raise BzrCheckError('duplicated file_id {%s} ' 'in inventory for revision {%s}' % (file_id, rev_id)) seen_ids[file_id] = True i = 0 for file_id in inv: i += 1 if i & 31 == 0: pb.tick() ie = inv[file_id] if ie.parent_id != None: if ie.parent_id not in seen_ids: raise BzrCheckError('missing parent {%s} in inventory for revision {%s}' % (ie.parent_id, rev_id)) if ie.kind == 'file': if ie.text_id in checked_texts: fp = checked_texts[ie.text_id] else: if not ie.text_id in branch.text_store: raise BzrCheckError('text {%s} not in text_store' % ie.text_id) tf = branch.text_store[ie.text_id] fp = fingerprint_file(tf) checked_texts[ie.text_id] = fp if ie.text_size != fp['size']: raise BzrCheckError('text {%s} wrong size' % ie.text_id) if ie.text_sha1 != fp['sha1']: raise BzrCheckError('text {%s} wrong sha1' % ie.text_id) elif ie.kind == 'directory': if ie.text_sha1 != None or ie.text_size != None or ie.text_id != None: raise BzrCheckError('directory {%s} has text in revision {%s}' % (file_id, rev_id)) pb.tick() for path, ie in inv.iter_entries(): if path in seen_names: raise BzrCheckError('duplicated path %s ' 'in inventory for revision {%s}' % (path, rev_id)) seen_names[path] = True last_rev_id = rev_id finally: branch.unlock() pb.clear() print 'checked %d revisions, %d file texts' % (revcount, len(checked_texts)) if missing_inventory_sha_cnt: print '%d revisions are missing inventory_sha1' % missing_inventory_sha_cnt if missing_revision_sha_cnt: print '%d parent links are missing revision_sha1' % missing_revision_sha_cnt if (missing_inventory_sha_cnt or missing_revision_sha_cnt): print ' (use "bzr upgrade" to fix them)' commit refs/heads/master mark :814 committer Martin Pool 1120462910 +1000 data 38 - allow doubled-dashes in revision ids from :813 M 644 inline bzrlib/revision.py data 6396 # (C) 2005 Canonical # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA class RevisionReference(object): """ Reference to a stored revision. Includes the revision_id and revision_sha1. """ revision_id = None revision_sha1 = None def __init__(self, revision_id, revision_sha1): if revision_id == None \ or isinstance(revision_id, basestring): self.revision_id = revision_id else: raise ValueError('bad revision_id %r' % revision_id) if revision_sha1 != None: if isinstance(revision_sha1, basestring) \ and len(revision_sha1) == 40: self.revision_sha1 = revision_sha1 else: raise ValueError('bad revision_sha1 %r' % revision_sha1) class Revision(object): """Single revision on a branch. Revisions may know their revision_hash, but only once they've been written out. This is not stored because you cannot write the hash into the file it describes. After bzr 0.0.5 revisions are allowed to have multiple parents. To support old clients this is written out in a slightly redundant form: the first parent as the predecessor. This will eventually be dropped. parents List of parent revisions, each is a RevisionReference. """ inventory_id = None inventory_sha1 = None revision_id = None timestamp = None message = None timezone = None committer = None def __init__(self, **args): self.__dict__.update(args) self.parents = [] def _get_precursor(self): from warnings import warn warn("Revision.precursor is deprecated", stacklevel=2) if self.parents: return self.parents[0].revision_id else: return None def _get_precursor_sha1(self): from warnings import warn warn("Revision.precursor_sha1 is deprecated", stacklevel=2) if self.parents: return self.parents[0].revision_sha1 else: return None def _fail(self): raise Exception("can't assign to precursor anymore") precursor = property(_get_precursor, _fail, _fail) precursor_sha1 = property(_get_precursor_sha1, _fail, _fail) def __repr__(self): return "" % self.revision_id def to_element(self): from bzrlib.xml import Element, SubElement root = Element('revision', committer = self.committer, timestamp = '%.9f' % self.timestamp, revision_id = self.revision_id, inventory_id = self.inventory_id, inventory_sha1 = self.inventory_sha1, ) if self.timezone: root.set('timezone', str(self.timezone)) root.text = '\n' msg = SubElement(root, 'message') msg.text = self.message msg.tail = '\n' if self.parents: # first parent stored as precursor for compatability with 0.0.5 and # earlier pr = self.parents[0] assert pr.revision_id root.set('precursor', pr.revision_id) if pr.revision_sha1: root.set('precursor_sha1', pr.revision_sha1) if self.parents: pelts = SubElement(root, 'parents') pelts.tail = pelts.text = '\n' for rr in self.parents: assert isinstance(rr, RevisionReference) p = SubElement(pelts, 'revision_ref') p.tail = '\n' assert rr.revision_id p.set('revision_id', rr.revision_id) if rr.revision_sha1: p.set('revision_sha1', rr.revision_sha1) return root def from_element(cls, elt): return unpack_revision(elt) from_element = classmethod(from_element) def unpack_revision(elt): """Convert XML element into Revision object.""" # is deprecated... from bzrlib.errors import BzrError if elt.tag not in ('revision', 'changeset'): raise BzrError("unexpected tag in revision file: %r" % elt) rev = Revision(committer = elt.get('committer'), timestamp = float(elt.get('timestamp')), revision_id = elt.get('revision_id'), inventory_id = elt.get('inventory_id'), inventory_sha1 = elt.get('inventory_sha1') ) precursor = elt.get('precursor') precursor_sha1 = elt.get('precursor_sha1') pelts = elt.find('parents') if precursor: # revisions written prior to 0.0.5 have a single precursor # give as an attribute rev_ref = RevisionReference(precursor, precursor_sha1) rev.parents.append(rev_ref) elif pelts: for p in pelts: assert p.tag == 'revision_ref', \ "bad parent node tag %r" % p.tag rev_ref = RevisionReference(p.get('revision_id'), p.get('revision_sha1')) rev.parents.append(rev_ref) v = elt.get('timezone') rev.timezone = v and int(v) rev.message = elt.findtext('message') # text of return rev REVISION_ID_RE = None def validate_revision_id(rid): """Check rid is syntactically valid for a revision id.""" global REVISION_ID_RE if not REVISION_ID_RE: import re REVISION_ID_RE = re.compile('[\w.-]+@[\w.-]+--?\d+--?[0-9a-f]+\Z') if not REVISION_ID_RE.match(rid): raise ValueError("malformed revision-id %r" % rid) commit refs/heads/master mark :815 committer Martin Pool 1120464411 +1000 data 46 - track pending-merges - unit tests for this from :814 M 644 inline bzrlib/branch.py data 40111 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, splitpath, \ sha_file, appendpath, file_kind from bzrlib.errors import BzrError BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def find_cached_branch(f, cache_root, **args): from remotebranch import RemoteBranch br = find_branch(f, **args) def cacheify(br, store_name): from meta_store import CachedStore cache_path = os.path.join(cache_root, store_name) os.mkdir(cache_path) new_store = CachedStore(getattr(br, store_name), cache_path) setattr(br, store_name, new_store) if isinstance(br, RemoteBranch): cacheify(br, 'inventory_store') cacheify(br, 'text_store') cacheify(br, 'revision_store') return br def _relpath(base, path): """Return path relative to base, or raise exception. The path may be either an absolute path or a path relative to the current working directory. Lifted out of Branch.relpath for ease of testing. os.path.commonprefix (python2.4) has a bad bug that it works just on string prefixes, assuming that '/u' is a prefix of '/u2'. This avoids that problem.""" rp = os.path.abspath(path) s = [] head = rp while len(head) >= len(base): if head == base: break head, tail = os.path.split(head) if tail: s.insert(0, tail) else: from errors import NotBranchError raise NotBranchError("path %r is not within branch %r" % (rp, base)) return os.sep.join(s) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head class DivergedBranches(Exception): def __init__(self, branch1, branch2): self.branch1 = branch1 self.branch2 = branch2 Exception.__init__(self, "These branches have diverged.") class NoSuchRevision(BzrError): def __init__(self, branch, revision): self.branch = branch self.revision = revision msg = "Branch %s has no revision %d" % (branch, revision) BzrError.__init__(self, msg) ###################################################################### # branch objects class Branch(object): """Branch holding a history of revisions. base Base directory of the branch. _lock_mode None, or 'r' or 'w' _lock_count If _lock_mode is true, a positive count of the number of times the lock has been taken. _lock Lock object from bzrlib.lock. """ base = None _lock_mode = None _lock_count = None _lock = None def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ from bzrlib.store import ImmutableStore if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): from errors import NotBranchError raise NotBranchError("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def __del__(self): if self._lock_mode or self._lock: from warnings import warn warn("branch %r was not explicitly unlocked" % self) self._lock.unlock() def lock_write(self): if self._lock_mode: if self._lock_mode != 'w': from errors import LockError raise LockError("can't upgrade to a write lock from %r" % self._lock_mode) self._lock_count += 1 else: from bzrlib.lock import WriteLock self._lock = WriteLock(self.controlfilename('branch-lock')) self._lock_mode = 'w' self._lock_count = 1 def lock_read(self): if self._lock_mode: assert self._lock_mode in ('r', 'w'), \ "invalid lock mode %r" % self._lock_mode self._lock_count += 1 else: from bzrlib.lock import ReadLock self._lock = ReadLock(self.controlfilename('branch-lock')) self._lock_mode = 'r' self._lock_count = 1 def unlock(self): if not self._lock_mode: from errors import LockError raise LockError('branch %r is not locked' % (self)) if self._lock_count > 1: self._lock_count -= 1 else: self._lock.unlock() self._lock = None self._lock_mode = self._lock_count = None def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" return _relpath(self.base, path) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, basestring): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): from bzrlib.inventory import Inventory from bzrlib.xml import pack_xml os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.\n") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock', 'pending-merges'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) pack_xml(Inventory(), self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: raise BzrError('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" from bzrlib.inventory import Inventory from bzrlib.xml import unpack_xml from time import time before = time() self.lock_read() try: # ElementTree does its own conversion from UTF-8, so open in # binary. inv = unpack_xml(Inventory, self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time() - before)) return inv finally: self.unlock() def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ from bzrlib.atomicfile import AtomicFile from bzrlib.xml import pack_xml self.lock_write() try: f = AtomicFile(self.controlfilename('inventory'), 'wb') try: pack_xml(inv, f) f.commit() finally: f.close() finally: self.unlock() mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. files List of paths to add, relative to the base of the tree. ids If set, use these instead of automatically generated ids. Must be the same length as the list of files, but may contain None for ids that are to be autogenerated. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ from bzrlib.textui import show_status # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, basestring): assert(ids is None or isinstance(ids, basestring)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) self.lock_write() try: inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): raise BzrError("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: raise BzrError("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: print 'added', quotefn(f) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) finally: self.unlock() def print_file(self, file, revno): """Print `file` to stdout.""" self.lock_read() try: tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: raise BzrError("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) finally: self.unlock() def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ from bzrlib.textui import show_status ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, basestring): files = [files] self.lock_write() try: tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: raise BzrError("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) finally: self.unlock() # FIXME: this doesn't need to be a branch method def set_inventory(self, new_inventory_list): from bzrlib.inventory import Inventory, InventoryEntry inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): from bzrlib.atomicfile import AtomicFile mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() + [revision_id] f = AtomicFile(self.controlfilename('revision-history')) try: for rev_id in rev_history: print >>f, rev_id f.commit() finally: f.close() def get_revision(self, revision_id): """Return the Revision object for a named revision""" from bzrlib.revision import Revision from bzrlib.xml import unpack_xml self.lock_read() try: if not revision_id or not isinstance(revision_id, basestring): raise ValueError('invalid revision-id: %r' % revision_id) r = unpack_xml(Revision, self.revision_store[revision_id]) finally: self.unlock() assert r.revision_id == revision_id return r def get_revision_sha1(self, revision_id): """Hash the stored value of a revision, and return it.""" # In the future, revision entries will be signed. At that # point, it is probably best *not* to include the signature # in the revision hash. Because that lets you re-sign # the revision, (add signatures/remove signatures) and still # have all hash pointers stay consistent. # But for now, just hash the contents. return sha_file(self.revision_store[revision_id]) def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" from bzrlib.inventory import Inventory from bzrlib.xml import unpack_xml return unpack_xml(Inventory, self.inventory_store[inventory_id]) def get_inventory_sha1(self, inventory_id): """Return the sha1 hash of the inventory entry """ return sha_file(self.inventory_store[inventory_id]) def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" if revision_id == None: from bzrlib.inventory import Inventory return Inventory() else: return self.get_inventory(self.get_revision(revision_id).inventory_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self.lock_read() try: return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] finally: self.unlock() def common_ancestor(self, other, self_revno=None, other_revno=None): """ >>> import commit >>> sb = ScratchBranch(files=['foo', 'foo~']) >>> sb.common_ancestor(sb) == (None, None) True >>> commit.commit(sb, "Committing first revision", verbose=False) >>> sb.common_ancestor(sb)[0] 1 >>> clone = sb.clone() >>> commit.commit(sb, "Committing second revision", verbose=False) >>> sb.common_ancestor(sb)[0] 2 >>> sb.common_ancestor(clone)[0] 1 >>> commit.commit(clone, "Committing divergent second revision", ... verbose=False) >>> sb.common_ancestor(clone)[0] 1 >>> sb.common_ancestor(clone) == clone.common_ancestor(sb) True >>> sb.common_ancestor(sb) != clone.common_ancestor(clone) True >>> clone2 = sb.clone() >>> sb.common_ancestor(clone2)[0] 2 >>> sb.common_ancestor(clone2, self_revno=1)[0] 1 >>> sb.common_ancestor(clone2, other_revno=1)[0] 1 """ my_history = self.revision_history() other_history = other.revision_history() if self_revno is None: self_revno = len(my_history) if other_revno is None: other_revno = len(other_history) indices = range(min((self_revno, other_revno))) indices.reverse() for r in indices: if my_history[r] == other_history[r]: return r+1, my_history[r] return None, None def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def missing_revisions(self, other, stop_revision=None): """ If self and other have not diverged, return a list of the revisions present in other, but missing from self. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch() >>> br2 = ScratchBranch() >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [u'REVISION-ID-1'] >>> br2.missing_revisions(br1) [] >>> commit(br1, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-2A") >>> br1.missing_revisions(br2) [u'REVISION-ID-2A'] >>> commit(br1, "lala!", rev_id="REVISION-ID-2B") >>> br1.missing_revisions(br2) Traceback (most recent call last): DivergedBranches: These branches have diverged. """ self_history = self.revision_history() self_len = len(self_history) other_history = other.revision_history() other_len = len(other_history) common_index = min(self_len, other_len) -1 if common_index >= 0 and \ self_history[common_index] != other_history[common_index]: raise DivergedBranches(self, other) if stop_revision is None: stop_revision = other_len elif stop_revision > other_len: raise NoSuchRevision(self, stop_revision) return other_history[self_len:stop_revision] def update_revisions(self, other, stop_revision=None): """Pull in all new revisions from other branch. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch(files=['foo', 'bar']) >>> br1.add('foo') >>> br1.add('bar') >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False) >>> br2 = ScratchBranch() >>> br2.update_revisions(br1) Added 2 texts. Added 1 inventories. Added 1 revisions. >>> br2.revision_history() [u'REVISION-ID-1'] >>> br2.update_revisions(br1) Added 0 texts. Added 0 inventories. Added 0 revisions. >>> br1.text_store.total_size() == br2.text_store.total_size() True """ from bzrlib.progress import ProgressBar try: set except NameError: from sets import Set as set pb = ProgressBar() pb.update('comparing histories') revision_ids = self.missing_revisions(other, stop_revision) if hasattr(other.revision_store, "prefetch"): other.revision_store.prefetch(revision_ids) if hasattr(other.inventory_store, "prefetch"): inventory_ids = [other.get_revision(r).inventory_id for r in revision_ids] other.inventory_store.prefetch(inventory_ids) revisions = [] needed_texts = set() i = 0 for rev_id in revision_ids: i += 1 pb.update('fetching revision', i, len(revision_ids)) rev = other.get_revision(rev_id) revisions.append(rev) inv = other.get_inventory(str(rev.inventory_id)) for key, entry in inv.iter_entries(): if entry.text_id is None: continue if entry.text_id not in self.text_store: needed_texts.add(entry.text_id) pb.clear() count = self.text_store.copy_multi(other.text_store, needed_texts) print "Added %d texts." % count inventory_ids = [ f.inventory_id for f in revisions ] count = self.inventory_store.copy_multi(other.inventory_store, inventory_ids) print "Added %d inventories." % count revision_ids = [ f.revision_id for f in revisions] count = self.revision_store.copy_multi(other.revision_store, revision_ids) for revision_id in revision_ids: self.append_revision(revision_id) print "Added %d revisions." % count def commit(self, *args, **kw): from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" from bzrlib.tree import EmptyTree, RevisionTree # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ from bzrlib.tree import EmptyTree, RevisionTree r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self.lock_write() try: tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): raise BzrError("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): raise BzrError("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: raise BzrError("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): raise BzrError("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': raise BzrError("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self.lock_write() try: ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): raise BzrError("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): raise BzrError("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': raise BzrError("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): raise BzrError("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): raise BzrError("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: raise BzrError("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): raise BzrError("destination %r already exists" % dest_path) if f_id in to_idpath: raise BzrError("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() def revert(self, filenames, old_tree=None, backups=True): """Restore selected files to the versions from a previous tree. backups If true (default) backups are made of files before they're renamed. """ from bzrlib.errors import NotVersionedError, BzrError from bzrlib.atomicfile import AtomicFile from bzrlib.osutils import backup_file inv = self.read_working_inventory() if old_tree is None: old_tree = self.basis_tree() old_inv = old_tree.inventory nids = [] for fn in filenames: file_id = inv.path2id(fn) if not file_id: raise NotVersionedError("not a versioned file", fn) if not old_inv.has_id(file_id): raise BzrError("file not present in old tree", fn, file_id) nids.append((fn, file_id)) # TODO: Rename back if it was previously at a different location # TODO: If given a directory, restore the entire contents from # the previous version. # TODO: Make a backup to a temporary file. # TODO: If the file previously didn't exist, delete it? for fn, file_id in nids: backup_file(fn) f = AtomicFile(fn, 'wb') try: f.write(old_tree.get_file(file_id).read()) f.commit() finally: f.close() def pending_merges(self): """Return a list of pending merges. These are revisions that have been merged into the working directory but not yet committed. """ cfn = self.controlfilename('pending-merges') if not os.path.exists(cfn): return [] p = [] for l in self.controlfile('pending-merges', 'r').readlines(): p.append(l.rstrip('\n')) return p def add_pending_merge(self, revision_id): from bzrlib.revision import validate_revision_id validate_revision_id(revision_id) p = self.pending_merges() if revision_id in p: return p.append(revision_id) self.set_pending_merges(p) def set_pending_merges(self, rev_list): from bzrlib.atomicfile import AtomicFile self.lock_write() try: f = AtomicFile(self.controlfilename('pending-merges')) try: for l in rev_list: print >>f, l f.commit() finally: f.close() finally: self.unlock() class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[], base=None): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ from tempfile import mkdtemp init = False if base is None: base = mkdtemp() init = True Branch.__init__(self, base, init=init) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def clone(self): """ >>> orig = ScratchBranch(files=["file1", "file2"]) >>> clone = orig.clone() >>> os.path.samefile(orig.base, clone.base) False >>> os.path.isfile(os.path.join(clone.base, "file1")) True """ from shutil import copytree from tempfile import mkdtemp base = mkdtemp() os.rmdir(base) copytree(self.base, base, symlinks=True) return ScratchBranch(base=base) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" from shutil import rmtree try: if self.base: mutter("delete ScratchBranch %s" % self.base) rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re from binascii import hexlify from time import time # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time()), s)) M 644 inline bzrlib/commit.py data 10594 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # FIXME: "bzr commit doc/format" commits doc/format.txt! def commit(branch, message, timestamp=None, timezone=None, committer=None, verbose=True, specific_files=None, rev_id=None): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. specific_files If true, commit only those files. rev_id If set, use this as the new revision id. Useful for test or import commands that need to tightly control what revisions are assigned. If you duplicate a revision id that exists elsewhere it is your own fault. If null (default), a time/random revision id is generated. """ import time, tempfile from bzrlib.osutils import local_time_offset, username from bzrlib.branch import gen_file_id from bzrlib.errors import BzrError from bzrlib.revision import Revision, RevisionReference from bzrlib.trace import mutter, note from bzrlib.xml import pack_xml branch.lock_write() try: # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_tree = branch.working_tree() work_inv = work_tree.inventory basis = branch.basis_tree() basis_inv = basis.inventory if verbose: note('looking for changes...') pending_merges = branch.pending_merges() missing_ids, new_inv = _gather_commit(branch, work_tree, work_inv, basis_inv, specific_files, verbose) for file_id in missing_ids: # Any files that have been deleted are now removed from the # working inventory. Files that were not selected for commit # are left as they were in the working inventory and ommitted # from the revision inventory. # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itbranch. if work_inv.has_id(file_id): del work_inv[file_id] if rev_id is None: rev_id = _gen_revision_id(time.time()) inv_id = rev_id inv_tmp = tempfile.TemporaryFile() pack_xml(new_inv, inv_tmp) inv_tmp.seek(0) branch.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) # We could also just sha hash the inv_tmp file # however, in the case that branch.inventory_store.add() # ever actually does anything special inv_sha1 = branch.get_inventory_sha1(inv_id) branch._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, message = message, inventory_id=inv_id, inventory_sha1=inv_sha1, revision_id=rev_id) precursor_id = branch.last_patch() if precursor_id: precursor_sha1 = branch.get_revision_sha1(precursor_id) rev.parents = [RevisionReference(precursor_id, precursor_sha1)] rev_tmp = tempfile.TemporaryFile() pack_xml(rev, rev_tmp) rev_tmp.seek(0) branch.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (branch.revno() + 1)) branch.append_revision(rev_id) if verbose: note("commited r%d" % branch.revno()) finally: branch.unlock() def _gen_revision_id(when): """Return new revision-id.""" from binascii import hexlify from osutils import rand_bytes, compact_date, user_email s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def _gather_commit(branch, work_tree, work_inv, basis_inv, specific_files, verbose): """Build inventory preparatory to commit. This adds any changed files into the text store, and sets their test-id, sha and size in the returned inventory appropriately. missing_ids Modified to hold a list of files that have been deleted from the working directory; these should be removed from the working inventory. """ from bzrlib.inventory import Inventory from osutils import isdir, isfile, sha_string, quotefn, \ local_time_offset, username, kind_marker, is_inside_any from branch import gen_file_id from errors import BzrError from revision import Revision from bzrlib.trace import mutter, note inv = Inventory() missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). p = branch.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if specific_files and not is_inside_any(specific_files, path): if basis_inv.has_id(file_id): # carry over with previous state inv.add(basis_inv[file_id].copy()) else: # omit this from committed inventory pass continue if not work_tree.has_id(file_id): if verbose: print('deleted %s%s' % (path, kind_marker(entry.kind))) mutter(" file is missing, removing from inventory") missing_ids.append(file_id) continue # this is present in the new inventory; may be new, modified or # unchanged. old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] entry = entry.copy() inv.add(entry) if old_ie: old_kind = old_ie.kind if old_kind != entry.kind: raise BzrError("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): raise BzrError("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): raise BzrError("%s is entered as file but is not a file" % quotefn(p)) new_sha1 = work_tree.get_file_sha1(file_id) if (old_ie and old_ie.text_sha1 == new_sha1): ## assert content == basis.get_file(file_id).read() entry.text_id = old_ie.text_id entry.text_sha1 = new_sha1 entry.text_size = old_ie.text_size mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: content = file(p, 'rb').read() # calculate the sha again, just in case the file contents # changed since we updated the cache entry.text_sha1 = sha_string(content) entry.text_size = len(content) entry.text_id = gen_file_id(entry.name) branch.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: marked = path + kind_marker(entry.kind) if not old_ie: print 'added', marked elif old_ie == entry: pass # unchanged elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): print 'modified', marked else: print 'renamed', marked return missing_ids, inv M 644 inline bzrlib/selftest/whitebox.py data 5945 #! /usr/bin/python import os import unittest from bzrlib.selftest import InTempDir, TestBase from bzrlib.branch import ScratchBranch, Branch from bzrlib.errors import NotBranchError, NotVersionedError class Unknowns(InTempDir): def runTest(self): b = Branch('.', init=True) self.build_tree(['hello.txt', 'hello.txt~']) self.assertEquals(list(b.unknowns()), ['hello.txt']) class ValidateRevisionId(TestBase): def runTest(self): from bzrlib.revision import validate_revision_id validate_revision_id('mbp@sourcefrog.net-20050311061123-96a255005c7c9dbe') self.assertRaises(ValueError, validate_revision_id, ' asdkjas') self.assertRaises(ValueError, validate_revision_id, 'mbp@sourcefrog.net-20050311061123-96a255005c7c9dbe\n') self.assertRaises(ValueError, validate_revision_id, ' mbp@sourcefrog.net-20050311061123-96a255005c7c9dbe') self.assertRaises(ValueError, validate_revision_id, 'Martin Pool -20050311061123-96a255005c7c9dbe') class PendingMerges(InTempDir): """Tracking pending-merged revisions.""" def runTest(self): b = Branch('.', init=True) self.assertEquals(b.pending_merges(), []) b.add_pending_merge('foo@azkhazan-123123-abcabc') self.assertEquals(b.pending_merges(), ['foo@azkhazan-123123-abcabc']) b.add_pending_merge('foo@azkhazan-123123-abcabc') self.assertEquals(b.pending_merges(), ['foo@azkhazan-123123-abcabc']) b.add_pending_merge('wibble@fofof--20050401--1928390812') self.assertEquals(b.pending_merges(), ['foo@azkhazan-123123-abcabc', 'wibble@fofof--20050401--1928390812']) class Revert(InTempDir): """Test selected-file revert""" def runTest(self): b = Branch('.', init=True) self.build_tree(['hello.txt']) file('hello.txt', 'w').write('initial hello') self.assertRaises(NotVersionedError, b.revert, ['hello.txt']) b.add(['hello.txt']) b.commit('create initial hello.txt') self.check_file_contents('hello.txt', 'initial hello') file('hello.txt', 'w').write('new hello') self.check_file_contents('hello.txt', 'new hello') # revert file modified since last revision b.revert(['hello.txt']) self.check_file_contents('hello.txt', 'initial hello') self.check_file_contents('hello.txt~', 'new hello') # reverting again clobbers the backup b.revert(['hello.txt']) self.check_file_contents('hello.txt', 'initial hello') self.check_file_contents('hello.txt~', 'initial hello') class RenameDirs(InTempDir): """Test renaming directories and the files within them.""" def runTest(self): b = Branch('.', init=True) self.build_tree(['dir/', 'dir/sub/', 'dir/sub/file']) b.add(['dir', 'dir/sub', 'dir/sub/file']) b.commit('create initial state') # TODO: lift out to a test helper that checks the shape of # an inventory revid = b.revision_history()[0] self.log('first revision_id is {%s}' % revid) inv = b.get_revision_inventory(revid) self.log('contents of inventory: %r' % inv.entries()) self.check_inventory_shape(inv, ['dir', 'dir/sub', 'dir/sub/file']) b.rename_one('dir', 'newdir') self.check_inventory_shape(b.inventory, ['newdir', 'newdir/sub', 'newdir/sub/file']) b.rename_one('newdir/sub', 'newdir/newsub') self.check_inventory_shape(b.inventory, ['newdir', 'newdir/newsub', 'newdir/newsub/file']) class BranchPathTestCase(TestBase): """test for branch path lookups Branch.relpath and bzrlib.branch._relpath do a simple but subtle job: given a path (either relative to cwd or absolute), work out if it is inside a branch and return the path relative to the base. """ def runTest(self): from bzrlib.branch import _relpath import tempfile, shutil savedir = os.getcwdu() dtmp = tempfile.mkdtemp() def rp(p): return _relpath(dtmp, p) try: # check paths inside dtmp while standing outside it self.assertEqual(rp(os.path.join(dtmp, 'foo')), 'foo') # root = nothing self.assertEqual(rp(dtmp), '') self.assertRaises(NotBranchError, rp, '/etc') # now some near-miss operations -- note that # os.path.commonprefix gets these wrong! self.assertRaises(NotBranchError, rp, dtmp.rstrip('\\/') + '2') self.assertRaises(NotBranchError, rp, dtmp.rstrip('\\/') + '2/foo') # now operations based on relpath of files in current # directory, or nearby os.chdir(dtmp) self.assertEqual(rp('foo/bar/quux'), 'foo/bar/quux') self.assertEqual(rp('foo'), 'foo') self.assertEqual(rp('./foo'), 'foo') self.assertEqual(rp(os.path.abspath('foo')), 'foo') self.assertRaises(NotBranchError, rp, '../foo') finally: os.chdir(savedir) shutil.rmtree(dtmp) commit refs/heads/master mark :816 committer Martin Pool 1120478238 +1000 data 144 - don't write precursor field in new revision xml - make parents more primary; remove more precursor code - test commit of revision with parents from :815 M 644 inline bzrlib/commit.py data 10737 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # FIXME: "bzr commit doc/format" commits doc/format.txt! def commit(branch, message, timestamp=None, timezone=None, committer=None, verbose=True, specific_files=None, rev_id=None): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. specific_files If true, commit only those files. rev_id If set, use this as the new revision id. Useful for test or import commands that need to tightly control what revisions are assigned. If you duplicate a revision id that exists elsewhere it is your own fault. If null (default), a time/random revision id is generated. """ import time, tempfile from bzrlib.osutils import local_time_offset, username from bzrlib.branch import gen_file_id from bzrlib.errors import BzrError from bzrlib.revision import Revision, RevisionReference from bzrlib.trace import mutter, note from bzrlib.xml import pack_xml branch.lock_write() try: # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_tree = branch.working_tree() work_inv = work_tree.inventory basis = branch.basis_tree() basis_inv = basis.inventory if verbose: note('looking for changes...') pending_merges = branch.pending_merges() missing_ids, new_inv = _gather_commit(branch, work_tree, work_inv, basis_inv, specific_files, verbose) for file_id in missing_ids: # Any files that have been deleted are now removed from the # working inventory. Files that were not selected for commit # are left as they were in the working inventory and ommitted # from the revision inventory. # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itbranch. if work_inv.has_id(file_id): del work_inv[file_id] if rev_id is None: rev_id = _gen_revision_id(time.time()) inv_id = rev_id inv_tmp = tempfile.TemporaryFile() pack_xml(new_inv, inv_tmp) inv_tmp.seek(0) branch.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) # We could also just sha hash the inv_tmp file # however, in the case that branch.inventory_store.add() # ever actually does anything special inv_sha1 = branch.get_inventory_sha1(inv_id) branch._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, message = message, inventory_id=inv_id, inventory_sha1=inv_sha1, revision_id=rev_id) rev.parents = [] precursor_id = branch.last_patch() if precursor_id: precursor_sha1 = branch.get_revision_sha1(precursor_id) rev.parents.append(RevisionReference(precursor_id, precursor_sha1)) for merge_rev in pending_merges: rev.parents.append(RevisionReference(merge_rev)) rev_tmp = tempfile.TemporaryFile() pack_xml(rev, rev_tmp) rev_tmp.seek(0) branch.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (branch.revno() + 1)) branch.append_revision(rev_id) if verbose: note("commited r%d" % branch.revno()) finally: branch.unlock() def _gen_revision_id(when): """Return new revision-id.""" from binascii import hexlify from osutils import rand_bytes, compact_date, user_email s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def _gather_commit(branch, work_tree, work_inv, basis_inv, specific_files, verbose): """Build inventory preparatory to commit. This adds any changed files into the text store, and sets their test-id, sha and size in the returned inventory appropriately. missing_ids Modified to hold a list of files that have been deleted from the working directory; these should be removed from the working inventory. """ from bzrlib.inventory import Inventory from osutils import isdir, isfile, sha_string, quotefn, \ local_time_offset, username, kind_marker, is_inside_any from branch import gen_file_id from errors import BzrError from revision import Revision from bzrlib.trace import mutter, note inv = Inventory() missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). p = branch.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if specific_files and not is_inside_any(specific_files, path): if basis_inv.has_id(file_id): # carry over with previous state inv.add(basis_inv[file_id].copy()) else: # omit this from committed inventory pass continue if not work_tree.has_id(file_id): if verbose: print('deleted %s%s' % (path, kind_marker(entry.kind))) mutter(" file is missing, removing from inventory") missing_ids.append(file_id) continue # this is present in the new inventory; may be new, modified or # unchanged. old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] entry = entry.copy() inv.add(entry) if old_ie: old_kind = old_ie.kind if old_kind != entry.kind: raise BzrError("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): raise BzrError("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): raise BzrError("%s is entered as file but is not a file" % quotefn(p)) new_sha1 = work_tree.get_file_sha1(file_id) if (old_ie and old_ie.text_sha1 == new_sha1): ## assert content == basis.get_file(file_id).read() entry.text_id = old_ie.text_id entry.text_sha1 = new_sha1 entry.text_size = old_ie.text_size mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: content = file(p, 'rb').read() # calculate the sha again, just in case the file contents # changed since we updated the cache entry.text_sha1 = sha_string(content) entry.text_size = len(content) entry.text_id = gen_file_id(entry.name) branch.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: marked = path + kind_marker(entry.kind) if not old_ie: print 'added', marked elif old_ie == entry: pass # unchanged elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): print 'modified', marked else: print 'renamed', marked return missing_ids, inv M 644 inline bzrlib/revision.py data 5343 # (C) 2005 Canonical # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA class RevisionReference(object): """ Reference to a stored revision. Includes the revision_id and revision_sha1. """ revision_id = None revision_sha1 = None def __init__(self, revision_id, revision_sha1=None): if revision_id == None \ or isinstance(revision_id, basestring): self.revision_id = revision_id else: raise ValueError('bad revision_id %r' % revision_id) if revision_sha1 != None: if isinstance(revision_sha1, basestring) \ and len(revision_sha1) == 40: self.revision_sha1 = revision_sha1 else: raise ValueError('bad revision_sha1 %r' % revision_sha1) class Revision(object): """Single revision on a branch. Revisions may know their revision_hash, but only once they've been written out. This is not stored because you cannot write the hash into the file it describes. After bzr 0.0.5 revisions are allowed to have multiple parents. parents List of parent revisions, each is a RevisionReference. """ inventory_id = None inventory_sha1 = None revision_id = None timestamp = None message = None timezone = None committer = None def __init__(self, **args): self.__dict__.update(args) self.parents = [] def __repr__(self): return "" % self.revision_id def to_element(self): from bzrlib.xml import Element, SubElement root = Element('revision', committer = self.committer, timestamp = '%.9f' % self.timestamp, revision_id = self.revision_id, inventory_id = self.inventory_id, inventory_sha1 = self.inventory_sha1, ) if self.timezone: root.set('timezone', str(self.timezone)) root.text = '\n' msg = SubElement(root, 'message') msg.text = self.message msg.tail = '\n' if self.parents: pelts = SubElement(root, 'parents') pelts.tail = pelts.text = '\n' for rr in self.parents: assert isinstance(rr, RevisionReference) p = SubElement(pelts, 'revision_ref') p.tail = '\n' assert rr.revision_id p.set('revision_id', rr.revision_id) if rr.revision_sha1: p.set('revision_sha1', rr.revision_sha1) return root def from_element(cls, elt): return unpack_revision(elt) from_element = classmethod(from_element) def unpack_revision(elt): """Convert XML element into Revision object.""" # is deprecated... from bzrlib.errors import BzrError if elt.tag not in ('revision', 'changeset'): raise BzrError("unexpected tag in revision file: %r" % elt) rev = Revision(committer = elt.get('committer'), timestamp = float(elt.get('timestamp')), revision_id = elt.get('revision_id'), inventory_id = elt.get('inventory_id'), inventory_sha1 = elt.get('inventory_sha1') ) precursor = elt.get('precursor') precursor_sha1 = elt.get('precursor_sha1') pelts = elt.find('parents') if pelts: for p in pelts: assert p.tag == 'revision_ref', \ "bad parent node tag %r" % p.tag rev_ref = RevisionReference(p.get('revision_id'), p.get('revision_sha1')) rev.parents.append(rev_ref) if precursor: # must be consistent prec_parent = rev.parents[0].revision_id assert prec_parent == precursor elif precursor: # revisions written prior to 0.0.5 have a single precursor # give as an attribute rev_ref = RevisionReference(precursor, precursor_sha1) rev.parents.append(rev_ref) v = elt.get('timezone') rev.timezone = v and int(v) rev.message = elt.findtext('message') # text of return rev REVISION_ID_RE = None def validate_revision_id(rid): """Check rid is syntactically valid for a revision id.""" global REVISION_ID_RE if not REVISION_ID_RE: import re REVISION_ID_RE = re.compile('[\w.-]+@[\w.-]+--?\d+--?[0-9a-f]+\Z') if not REVISION_ID_RE.match(rid): raise ValueError("malformed revision-id %r" % rid) M 644 inline bzrlib/selftest/whitebox.py data 6339 #! /usr/bin/python import os import unittest from bzrlib.selftest import InTempDir, TestBase from bzrlib.branch import ScratchBranch, Branch from bzrlib.errors import NotBranchError, NotVersionedError class Unknowns(InTempDir): def runTest(self): b = Branch('.', init=True) self.build_tree(['hello.txt', 'hello.txt~']) self.assertEquals(list(b.unknowns()), ['hello.txt']) class ValidateRevisionId(TestBase): def runTest(self): from bzrlib.revision import validate_revision_id validate_revision_id('mbp@sourcefrog.net-20050311061123-96a255005c7c9dbe') self.assertRaises(ValueError, validate_revision_id, ' asdkjas') self.assertRaises(ValueError, validate_revision_id, 'mbp@sourcefrog.net-20050311061123-96a255005c7c9dbe\n') self.assertRaises(ValueError, validate_revision_id, ' mbp@sourcefrog.net-20050311061123-96a255005c7c9dbe') self.assertRaises(ValueError, validate_revision_id, 'Martin Pool -20050311061123-96a255005c7c9dbe') class PendingMerges(InTempDir): """Tracking pending-merged revisions.""" def runTest(self): b = Branch('.', init=True) self.assertEquals(b.pending_merges(), []) b.add_pending_merge('foo@azkhazan-123123-abcabc') self.assertEquals(b.pending_merges(), ['foo@azkhazan-123123-abcabc']) b.add_pending_merge('foo@azkhazan-123123-abcabc') self.assertEquals(b.pending_merges(), ['foo@azkhazan-123123-abcabc']) b.add_pending_merge('wibble@fofof--20050401--1928390812') self.assertEquals(b.pending_merges(), ['foo@azkhazan-123123-abcabc', 'wibble@fofof--20050401--1928390812']) b.commit("commit from base with two merges") rev = b.get_revision(b.revision_history()[0]) self.assertEquals(len(rev.parents), 2) self.assertEquals(rev.parents[0].revision_id, 'foo@azkhazan-123123-abcabc') self.assertEquals(rev.parents[1].revision_id, 'wibble@fofof--20050401--1928390812') class Revert(InTempDir): """Test selected-file revert""" def runTest(self): b = Branch('.', init=True) self.build_tree(['hello.txt']) file('hello.txt', 'w').write('initial hello') self.assertRaises(NotVersionedError, b.revert, ['hello.txt']) b.add(['hello.txt']) b.commit('create initial hello.txt') self.check_file_contents('hello.txt', 'initial hello') file('hello.txt', 'w').write('new hello') self.check_file_contents('hello.txt', 'new hello') # revert file modified since last revision b.revert(['hello.txt']) self.check_file_contents('hello.txt', 'initial hello') self.check_file_contents('hello.txt~', 'new hello') # reverting again clobbers the backup b.revert(['hello.txt']) self.check_file_contents('hello.txt', 'initial hello') self.check_file_contents('hello.txt~', 'initial hello') class RenameDirs(InTempDir): """Test renaming directories and the files within them.""" def runTest(self): b = Branch('.', init=True) self.build_tree(['dir/', 'dir/sub/', 'dir/sub/file']) b.add(['dir', 'dir/sub', 'dir/sub/file']) b.commit('create initial state') # TODO: lift out to a test helper that checks the shape of # an inventory revid = b.revision_history()[0] self.log('first revision_id is {%s}' % revid) inv = b.get_revision_inventory(revid) self.log('contents of inventory: %r' % inv.entries()) self.check_inventory_shape(inv, ['dir', 'dir/sub', 'dir/sub/file']) b.rename_one('dir', 'newdir') self.check_inventory_shape(b.inventory, ['newdir', 'newdir/sub', 'newdir/sub/file']) b.rename_one('newdir/sub', 'newdir/newsub') self.check_inventory_shape(b.inventory, ['newdir', 'newdir/newsub', 'newdir/newsub/file']) class BranchPathTestCase(TestBase): """test for branch path lookups Branch.relpath and bzrlib.branch._relpath do a simple but subtle job: given a path (either relative to cwd or absolute), work out if it is inside a branch and return the path relative to the base. """ def runTest(self): from bzrlib.branch import _relpath import tempfile, shutil savedir = os.getcwdu() dtmp = tempfile.mkdtemp() def rp(p): return _relpath(dtmp, p) try: # check paths inside dtmp while standing outside it self.assertEqual(rp(os.path.join(dtmp, 'foo')), 'foo') # root = nothing self.assertEqual(rp(dtmp), '') self.assertRaises(NotBranchError, rp, '/etc') # now some near-miss operations -- note that # os.path.commonprefix gets these wrong! self.assertRaises(NotBranchError, rp, dtmp.rstrip('\\/') + '2') self.assertRaises(NotBranchError, rp, dtmp.rstrip('\\/') + '2/foo') # now operations based on relpath of files in current # directory, or nearby os.chdir(dtmp) self.assertEqual(rp('foo/bar/quux'), 'foo/bar/quux') self.assertEqual(rp('foo'), 'foo') self.assertEqual(rp('./foo'), 'foo') self.assertEqual(rp(os.path.abspath('foo')), 'foo') self.assertRaises(NotBranchError, rp, '../foo') finally: os.chdir(savedir) shutil.rmtree(dtmp) commit refs/heads/master mark :817 committer Martin Pool 1120479100 +1000 data 176 - Deferred patch that uses Python ndiff format. This highlights the changes within a line, which is rather nice. But it also outputs the entire file, which is less good. from :816 M 644 inline patches/ndiff.patch data 4896 *** modified file 'bzrlib/commands.py' --- bzrlib/commands.py +++ bzrlib/commands.py @@ -721,10 +721,11 @@ """ takes_args = ['file*'] - takes_options = ['revision', 'diff-options'] + takes_options = ['revision', 'diff-options', 'ndiff'] aliases = ['di', 'dif'] - def run(self, revision=None, file_list=None, diff_options=None): + def run(self, revision=None, file_list=None, diff_options=None, + ndiff=None): from bzrlib.diff import show_diff if file_list: @@ -735,9 +736,15 @@ file_list = None else: b = find_branch('.') - + + if ndiff: + format = 'ndiff' + else: + format = None + show_diff(b, revision, specific_files=file_list, - external_diff_options=diff_options) + external_diff_options=diff_options, + format=format) @@ -1344,6 +1351,7 @@ 'format': unicode, 'forward': None, 'message': unicode, + 'ndiff': None, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, *** modified file 'bzrlib/diff.py' --- bzrlib/diff.py +++ bzrlib/diff.py @@ -70,6 +70,13 @@ print >>to_file + +def internal_ndiff(old_label, oldlines, + new_label, newlines, + to_file): + """Show diff in python-specific ndiff format.""" + from difflib import ndiff + to_file.writelines(ndiff(oldlines, newlines)) def external_diff(old_label, oldlines, new_label, newlines, to_file, @@ -152,7 +159,8 @@ -def show_diff(b, revision, specific_files, external_diff_options=None): +def show_diff(b, revision, specific_files, external_diff_options=None, + format=None): """Shortcut for showing the diff to the working tree. b @@ -160,6 +168,9 @@ revision None for each, or otherwise the old revision to compare against. + + format + 'unified', 'context', 'ndiff', 'external' The more general form is show_diff_trees(), where the caller supplies any two trees. @@ -174,12 +185,13 @@ new_tree = b.working_tree() show_diff_trees(old_tree, new_tree, sys.stdout, specific_files, - external_diff_options) + external_diff_options, format) def show_diff_trees(old_tree, new_tree, to_file, specific_files=None, - external_diff_options=None): + external_diff_options=None, + format=None): """Show in text form the changes from one tree to another. to_files @@ -204,10 +216,12 @@ if external_diff_options: assert isinstance(external_diff_options, basestring) opts = external_diff_options.split() - def diff_file(olab, olines, nlab, nlines, to_file): + def diff_fn(olab, olines, nlab, nlines, to_file): external_diff(olab, olines, nlab, nlines, to_file, opts) + elif format == 'ndiff': + diff_fn = internal_ndiff else: - diff_file = internal_diff + diff_fn = internal_diff delta = compare_trees(old_tree, new_tree, want_unchanged=False, @@ -216,7 +230,7 @@ for path, file_id, kind in delta.removed: print >>to_file, '*** removed %s %r' % (kind, path) if kind == 'file': - diff_file(old_label + path, + diff_fn(old_label + path, old_tree.get_file(file_id).readlines(), DEVNULL, [], @@ -225,7 +239,7 @@ for path, file_id, kind in delta.added: print >>to_file, '*** added %s %r' % (kind, path) if kind == 'file': - diff_file(DEVNULL, + diff_fn(DEVNULL, [], new_label + path, new_tree.get_file(file_id).readlines(), @@ -234,7 +248,7 @@ for old_path, new_path, file_id, kind, text_modified in delta.renamed: print >>to_file, '*** renamed %s %r => %r' % (kind, old_path, new_path) if text_modified: - diff_file(old_label + old_path, + diff_fn(old_label + old_path, old_tree.get_file(file_id).readlines(), new_label + new_path, new_tree.get_file(file_id).readlines(), @@ -243,7 +257,7 @@ for path, file_id, kind in delta.modified: print >>to_file, '*** modified %s %r' % (kind, path) if kind == 'file': - diff_file(old_label + path, + diff_fn(old_label + path, old_tree.get_file(file_id).readlines(), new_label + path, new_tree.get_file(file_id).readlines(), commit refs/heads/master mark :818 committer Martin Pool 1120479754 +1000 data 44 - Clear pending-merge list when committing. from :817 M 644 inline TODO data 13080 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * -r option should take a revision-id as well as a revno. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * ``bzr log DIR`` should give changes to any files within DIR. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. Perhaps ~/.bzr/http-cache. Baz has a fairly simple cache under ~/.arch-cache, containing revision information encoded almost as a bunch of archives. Perhaps we could simply store full paths. * Maybe also store directories in the statcache so that we can quickly identify that they still exist. * Diff should show timestamps; for files from the working directory we can use the file itself; for files from a revision we should use the commit time of the revision. * Perhaps split command infrastructure from the actual command definitions. * Cleaner support for negative boolean options like --no-recurse. * Statcache should possibly map all file paths to / separators * quotefn doubles all backslashes on Windows; this is probably not the best thing to do. What would be a better way to safely represent filenames? Perhaps we could doublequote things containing spaces, on the principle that filenames containing quotes are unlikely? Nice for humans; less good for machine parsing. * Patches should probably use only forward slashes, even on Windows, otherwise Unix patch can't apply them. (?) * Branch.update_revisions() inefficiently fetches revisions from the remote server twice; once to find out what text and inventory they need and then again to actually get the thing. This is a bit inefficient. One complicating factor here is that we don't really want to have revisions present in the revision-store until all their constituent parts are also stored. The basic problem is that RemoteBranch.get_revision() and similar methods return object, but what we really want is the raw XML, which can be popped into our own store. That needs to be refactored. * ``bzr status FOO`` where foo is ignored should say so. * ``bzr mkdir A...`` should just create and add A. * Guard against repeatedly merging any particular patch. Medium things ------------- * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. We should be able to just get the id for the selected files, look up their location and diff just those files. No need to traverse the entire inventories. * ``bzr status DIR`` or ``bzr diff DIR`` should report on all changes under that directory. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from ElementTree to an object when it is read in, but rather wait until the program actually wants to know about that node. * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. - Selected-file commit - Impossible selected-file commit: adding things in non-versioned directories, crossing renames, etc. * Write a reproducible benchmark, perhaps importing various kernel versions. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Paranoid mode where we never trust SHA-1 matches. * Don't commit if there are no changes unless forced. * --dry-run mode for commit? (Or maybe just run with check-command=false?) * Generally, be a bit more verbose unless --silent is specified. * Function that finds all changes to files under a given directory; perhaps log should use this if a directory is given. * XML attributes might have trouble with filenames containing \n and \r. Do we really want to support this? I think perhaps not. * Remember execute bits, so that exports will work OK. * Unify smart_add and plain Branch.add(); perhaps smart_add should just build a list of files to add and pass that to the regular add function. * Function to list a directory, saying in which revision each file was last modified. Useful for web and gui interfaces, and slow to compute one file at a time. * unittest is standard, but the results are kind of ugly; would be nice to make it cleaner. * Check locking is correct during merge-related operations. * Perhaps attempts to get locks should timeout after some period of time, or at least display a progress message. * Split out upgrade functionality from check command into a separate ``bzr upgrade``. * Don't pass around command classes but rather pass objects. This'd make it cleaner to construct objects wrapping external commands. * Track all merged-in revisions in a versioned add-only metafile. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. Possibly this should be done by splitting the commit function into several parts (under a single interface). It is already rather large. Decomposition: - find tree modifications and prepare in-memory inventory - export that inventory to a temporary directory - run the test in that temporary directory - if that succeeded, continue to actually finish the commit What should be done with the text of modified files while this is underway? I don't think we want to count on holding them in memory and we can't trust the working files to stay in one place so I suppose we need to move them into the text store, or otherwise into a temporary directory. If the commit does not actually complete, we would rather the content was not left behind in the stores. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` M 644 inline bzrlib/commit.py data 10776 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # FIXME: "bzr commit doc/format" commits doc/format.txt! def commit(branch, message, timestamp=None, timezone=None, committer=None, verbose=True, specific_files=None, rev_id=None): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. specific_files If true, commit only those files. rev_id If set, use this as the new revision id. Useful for test or import commands that need to tightly control what revisions are assigned. If you duplicate a revision id that exists elsewhere it is your own fault. If null (default), a time/random revision id is generated. """ import time, tempfile from bzrlib.osutils import local_time_offset, username from bzrlib.branch import gen_file_id from bzrlib.errors import BzrError from bzrlib.revision import Revision, RevisionReference from bzrlib.trace import mutter, note from bzrlib.xml import pack_xml branch.lock_write() try: # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_tree = branch.working_tree() work_inv = work_tree.inventory basis = branch.basis_tree() basis_inv = basis.inventory if verbose: note('looking for changes...') pending_merges = branch.pending_merges() missing_ids, new_inv = _gather_commit(branch, work_tree, work_inv, basis_inv, specific_files, verbose) for file_id in missing_ids: # Any files that have been deleted are now removed from the # working inventory. Files that were not selected for commit # are left as they were in the working inventory and ommitted # from the revision inventory. # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itbranch. if work_inv.has_id(file_id): del work_inv[file_id] if rev_id is None: rev_id = _gen_revision_id(time.time()) inv_id = rev_id inv_tmp = tempfile.TemporaryFile() pack_xml(new_inv, inv_tmp) inv_tmp.seek(0) branch.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) # We could also just sha hash the inv_tmp file # however, in the case that branch.inventory_store.add() # ever actually does anything special inv_sha1 = branch.get_inventory_sha1(inv_id) branch._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, message = message, inventory_id=inv_id, inventory_sha1=inv_sha1, revision_id=rev_id) rev.parents = [] precursor_id = branch.last_patch() if precursor_id: precursor_sha1 = branch.get_revision_sha1(precursor_id) rev.parents.append(RevisionReference(precursor_id, precursor_sha1)) for merge_rev in pending_merges: rev.parents.append(RevisionReference(merge_rev)) rev_tmp = tempfile.TemporaryFile() pack_xml(rev, rev_tmp) rev_tmp.seek(0) branch.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (branch.revno() + 1)) branch.append_revision(rev_id) branch.set_pending_merges([]) if verbose: note("commited r%d" % branch.revno()) finally: branch.unlock() def _gen_revision_id(when): """Return new revision-id.""" from binascii import hexlify from osutils import rand_bytes, compact_date, user_email s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def _gather_commit(branch, work_tree, work_inv, basis_inv, specific_files, verbose): """Build inventory preparatory to commit. This adds any changed files into the text store, and sets their test-id, sha and size in the returned inventory appropriately. missing_ids Modified to hold a list of files that have been deleted from the working directory; these should be removed from the working inventory. """ from bzrlib.inventory import Inventory from osutils import isdir, isfile, sha_string, quotefn, \ local_time_offset, username, kind_marker, is_inside_any from branch import gen_file_id from errors import BzrError from revision import Revision from bzrlib.trace import mutter, note inv = Inventory() missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). p = branch.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if specific_files and not is_inside_any(specific_files, path): if basis_inv.has_id(file_id): # carry over with previous state inv.add(basis_inv[file_id].copy()) else: # omit this from committed inventory pass continue if not work_tree.has_id(file_id): if verbose: print('deleted %s%s' % (path, kind_marker(entry.kind))) mutter(" file is missing, removing from inventory") missing_ids.append(file_id) continue # this is present in the new inventory; may be new, modified or # unchanged. old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] entry = entry.copy() inv.add(entry) if old_ie: old_kind = old_ie.kind if old_kind != entry.kind: raise BzrError("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): raise BzrError("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): raise BzrError("%s is entered as file but is not a file" % quotefn(p)) new_sha1 = work_tree.get_file_sha1(file_id) if (old_ie and old_ie.text_sha1 == new_sha1): ## assert content == basis.get_file(file_id).read() entry.text_id = old_ie.text_id entry.text_sha1 = new_sha1 entry.text_size = old_ie.text_size mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: content = file(p, 'rb').read() # calculate the sha again, just in case the file contents # changed since we updated the cache entry.text_sha1 = sha_string(content) entry.text_size = len(content) entry.text_id = gen_file_id(entry.name) branch.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: marked = path + kind_marker(entry.kind) if not old_ie: print 'added', marked elif old_ie == entry: pass # unchanged elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): print 'modified', marked else: print 'renamed', marked return missing_ids, inv M 644 inline bzrlib/selftest/whitebox.py data 6452 #! /usr/bin/python import os import unittest from bzrlib.selftest import InTempDir, TestBase from bzrlib.branch import ScratchBranch, Branch from bzrlib.errors import NotBranchError, NotVersionedError class Unknowns(InTempDir): def runTest(self): b = Branch('.', init=True) self.build_tree(['hello.txt', 'hello.txt~']) self.assertEquals(list(b.unknowns()), ['hello.txt']) class ValidateRevisionId(TestBase): def runTest(self): from bzrlib.revision import validate_revision_id validate_revision_id('mbp@sourcefrog.net-20050311061123-96a255005c7c9dbe') self.assertRaises(ValueError, validate_revision_id, ' asdkjas') self.assertRaises(ValueError, validate_revision_id, 'mbp@sourcefrog.net-20050311061123-96a255005c7c9dbe\n') self.assertRaises(ValueError, validate_revision_id, ' mbp@sourcefrog.net-20050311061123-96a255005c7c9dbe') self.assertRaises(ValueError, validate_revision_id, 'Martin Pool -20050311061123-96a255005c7c9dbe') class PendingMerges(InTempDir): """Tracking pending-merged revisions.""" def runTest(self): b = Branch('.', init=True) self.assertEquals(b.pending_merges(), []) b.add_pending_merge('foo@azkhazan-123123-abcabc') self.assertEquals(b.pending_merges(), ['foo@azkhazan-123123-abcabc']) b.add_pending_merge('foo@azkhazan-123123-abcabc') self.assertEquals(b.pending_merges(), ['foo@azkhazan-123123-abcabc']) b.add_pending_merge('wibble@fofof--20050401--1928390812') self.assertEquals(b.pending_merges(), ['foo@azkhazan-123123-abcabc', 'wibble@fofof--20050401--1928390812']) b.commit("commit from base with two merges") rev = b.get_revision(b.revision_history()[0]) self.assertEquals(len(rev.parents), 2) self.assertEquals(rev.parents[0].revision_id, 'foo@azkhazan-123123-abcabc') self.assertEquals(rev.parents[1].revision_id, 'wibble@fofof--20050401--1928390812') # list should be cleared when we do a commit self.assertEquals(b.pending_merges(), []) class Revert(InTempDir): """Test selected-file revert""" def runTest(self): b = Branch('.', init=True) self.build_tree(['hello.txt']) file('hello.txt', 'w').write('initial hello') self.assertRaises(NotVersionedError, b.revert, ['hello.txt']) b.add(['hello.txt']) b.commit('create initial hello.txt') self.check_file_contents('hello.txt', 'initial hello') file('hello.txt', 'w').write('new hello') self.check_file_contents('hello.txt', 'new hello') # revert file modified since last revision b.revert(['hello.txt']) self.check_file_contents('hello.txt', 'initial hello') self.check_file_contents('hello.txt~', 'new hello') # reverting again clobbers the backup b.revert(['hello.txt']) self.check_file_contents('hello.txt', 'initial hello') self.check_file_contents('hello.txt~', 'initial hello') class RenameDirs(InTempDir): """Test renaming directories and the files within them.""" def runTest(self): b = Branch('.', init=True) self.build_tree(['dir/', 'dir/sub/', 'dir/sub/file']) b.add(['dir', 'dir/sub', 'dir/sub/file']) b.commit('create initial state') # TODO: lift out to a test helper that checks the shape of # an inventory revid = b.revision_history()[0] self.log('first revision_id is {%s}' % revid) inv = b.get_revision_inventory(revid) self.log('contents of inventory: %r' % inv.entries()) self.check_inventory_shape(inv, ['dir', 'dir/sub', 'dir/sub/file']) b.rename_one('dir', 'newdir') self.check_inventory_shape(b.inventory, ['newdir', 'newdir/sub', 'newdir/sub/file']) b.rename_one('newdir/sub', 'newdir/newsub') self.check_inventory_shape(b.inventory, ['newdir', 'newdir/newsub', 'newdir/newsub/file']) class BranchPathTestCase(TestBase): """test for branch path lookups Branch.relpath and bzrlib.branch._relpath do a simple but subtle job: given a path (either relative to cwd or absolute), work out if it is inside a branch and return the path relative to the base. """ def runTest(self): from bzrlib.branch import _relpath import tempfile, shutil savedir = os.getcwdu() dtmp = tempfile.mkdtemp() def rp(p): return _relpath(dtmp, p) try: # check paths inside dtmp while standing outside it self.assertEqual(rp(os.path.join(dtmp, 'foo')), 'foo') # root = nothing self.assertEqual(rp(dtmp), '') self.assertRaises(NotBranchError, rp, '/etc') # now some near-miss operations -- note that # os.path.commonprefix gets these wrong! self.assertRaises(NotBranchError, rp, dtmp.rstrip('\\/') + '2') self.assertRaises(NotBranchError, rp, dtmp.rstrip('\\/') + '2/foo') # now operations based on relpath of files in current # directory, or nearby os.chdir(dtmp) self.assertEqual(rp('foo/bar/quux'), 'foo/bar/quux') self.assertEqual(rp('foo'), 'foo') self.assertEqual(rp('./foo'), 'foo') self.assertEqual(rp(os.path.abspath('foo')), 'foo') self.assertRaises(NotBranchError, rp, '../foo') finally: os.chdir(savedir) shutil.rmtree(dtmp) commit refs/heads/master mark :819 committer Martin Pool 1120479962 +1000 data 79 - check command checks that all inventory-ids are the same as in the revision. from :818 M 644 inline bzrlib/check.py data 7081 # Copyright (C) 2004, 2005 by Martin Pool # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def check(branch): """Run consistency checks on a branch. TODO: Also check non-mainline revisions mentioned as parents. TODO: Check for extra files in the control directory. """ from bzrlib.trace import mutter from bzrlib.errors import BzrCheckError from bzrlib.osutils import fingerprint_file from bzrlib.progress import ProgressBar branch.lock_read() try: pb = ProgressBar(show_spinner=True) last_rev_id = None missing_inventory_sha_cnt = 0 missing_revision_sha_cnt = 0 history = branch.revision_history() revno = 0 revcount = len(history) mismatch_inv_id = [] # for all texts checked, text_id -> sha1 checked_texts = {} for rev_id in history: revno += 1 pb.update('checking revision', revno, revcount) mutter(' revision {%s}' % rev_id) rev = branch.get_revision(rev_id) if rev.revision_id != rev_id: raise BzrCheckError('wrong internal revision id in revision {%s}' % rev_id) # check the previous history entry is a parent of this entry if rev.parents: if last_rev_id is None: raise BzrCheckError("revision {%s} has %d parents, but is the " "start of the branch" % (rev_id, len(rev.parents))) for prr in rev.parents: if prr.revision_id == last_rev_id: break else: raise BzrCheckError("previous revision {%s} not listed among " "parents of {%s}" % (last_rev_id, rev_id)) for prr in rev.parents: if prr.revision_sha1 is None: missing_revision_sha_cnt += 1 continue prid = prr.revision_id actual_sha = branch.get_revision_sha1(prid) if prr.revision_sha1 != actual_sha: raise BzrCheckError("mismatched revision sha1 for " "parent {%s} of {%s}: %s vs %s" % (prid, rev_id, prr.revision_sha1, actual_sha)) elif last_rev_id: raise BzrCheckError("revision {%s} has no parents listed but preceded " "by {%s}" % (rev_id, last_rev_id)) if rev.inventory_id != rev_id: mismatch_inv_id.append(rev_id) ## TODO: Check all the required fields are present on the revision. if rev.inventory_sha1: inv_sha1 = branch.get_inventory_sha1(rev.inventory_id) if inv_sha1 != rev.inventory_sha1: raise BzrCheckError('Inventory sha1 hash doesn\'t match' ' value in revision {%s}' % rev_id) else: missing_inventory_sha_cnt += 1 mutter("no inventory_sha1 on revision {%s}" % rev_id) inv = branch.get_inventory(rev.inventory_id) seen_ids = {} seen_names = {} ## p('revision %d/%d file ids' % (revno, revcount)) for file_id in inv: if file_id in seen_ids: raise BzrCheckError('duplicated file_id {%s} ' 'in inventory for revision {%s}' % (file_id, rev_id)) seen_ids[file_id] = True i = 0 for file_id in inv: i += 1 if i & 31 == 0: pb.tick() ie = inv[file_id] if ie.parent_id != None: if ie.parent_id not in seen_ids: raise BzrCheckError('missing parent {%s} in inventory for revision {%s}' % (ie.parent_id, rev_id)) if ie.kind == 'file': if ie.text_id in checked_texts: fp = checked_texts[ie.text_id] else: if not ie.text_id in branch.text_store: raise BzrCheckError('text {%s} not in text_store' % ie.text_id) tf = branch.text_store[ie.text_id] fp = fingerprint_file(tf) checked_texts[ie.text_id] = fp if ie.text_size != fp['size']: raise BzrCheckError('text {%s} wrong size' % ie.text_id) if ie.text_sha1 != fp['sha1']: raise BzrCheckError('text {%s} wrong sha1' % ie.text_id) elif ie.kind == 'directory': if ie.text_sha1 != None or ie.text_size != None or ie.text_id != None: raise BzrCheckError('directory {%s} has text in revision {%s}' % (file_id, rev_id)) pb.tick() for path, ie in inv.iter_entries(): if path in seen_names: raise BzrCheckError('duplicated path %s ' 'in inventory for revision {%s}' % (path, rev_id)) seen_names[path] = True last_rev_id = rev_id finally: branch.unlock() pb.clear() print 'checked %d revisions, %d file texts' % (revcount, len(checked_texts)) if missing_inventory_sha_cnt: print '%d revisions are missing inventory_sha1' % missing_inventory_sha_cnt if missing_revision_sha_cnt: print '%d parent links are missing revision_sha1' % missing_revision_sha_cnt if (missing_inventory_sha_cnt or missing_revision_sha_cnt): print ' (use "bzr upgrade" to fix them)' if mismatch_inv_id: print '%d revisions have mismatched inventory ids:' % len(mismatch_inv_id) for rev_id in mismatch_inv_id: print ' ', rev_id M 644 inline doc/formats.txt data 8604 ***************** Bazaar-NG formats ***************** .. contents:: Since branches are working directories there is just a single directory format. There is one metadata directory called ``.bzr`` at the top of each tree. Control files inside ``.bzr`` are never touched by patches and should not normally be edited by the user. These files are designed so that repository-level operations are ACID without depending on atomic operations spanning multiple files. There are two particular cases: aborting a transaction in the middle, and contention from multiple processes. We also need to be careful to flush files to disk at appropriate points; even this may not be totally safe if the filesystem does not guarantee ordering between multiple file changes, so we need to be sure to roll back. The design must also be such that the directory can simply be copied and that hardlinked directories will work. (So we must always replace files, never just append.) A cache is kept under here of easily-accessible information about previous revisions. This should be under a single directory so that it can be easily identified, excluded from backups, removed, etc. This might contain pristine tree from previous revisions, manifests and inventories, etc. It might also contain working directories when building a commit, etc. Call this maybe ``cache`` or ``tmp``. I wonder if we should use .zip files for revisions and cacherevs rather than tar files so that random access is easier/more efficient. There is a Python library ``zipfile``. Signing XML files ***************** bzr relies on storing hashes or GPG signatures of various XML files. There can be multiple equivalent representations of the same XML tree, but these will have different byte-by-byte hashes. Once signed files are written out, they must be stored byte-for-byte and never re-encoded or renormalized, because that would break their hash or signature. Branch metadata *************** All inside ``.bzr`` ``README`` Tells people not to touch anything here. ``branch-format`` Identifies the parent as a Bazaar-NG branch; contains the overall branch metadata format as a string. ``pristine-directory`` Identifies that this is a pristine directory and may not be committed to. ``patches/`` Directory containing all patches applied to this branch, one per file. Patches are stored as compressed deltas. We also store the hash of the delta, hash of the before and after manifests, and optionally a GPG signature. ``cache/`` Contains various cached data that can be destroyed and will be recreated. (It should not be modified.) ``cache/pristine/`` Contains cached full trees for selected previous revisions, used when generating diffs, etc. ``cache/inventory/`` Contains cached inventories of previous revisions. ``cache/snapshot/`` Contains tarballs of cached revisions of the tree, named by their revision id. These can also be removed, but ``patch-history`` File containing the UUIDs of all patches taken in this branch, in the order they were taken. Each commit adds exactly one line to this file; lines are never removed or reordered. ``merged-patches`` List of foreign patches that have been merged into this branch. Must have no entries in common with ``patch-history``. Commits that include merges add to this file; lines are never removed or reordered. ``pending-merges`` List (one per line) of non-mainline revisions that have been merged and are waiting to be committed. ``branch-name`` User-qualified name of the branch, for the purpose of describing the origin of patches, e.g. ``mbp@sourcefrog.net/distcc--main``. ``friends`` List of branches from which we have pulled; file containing a list of pairs of branch-name and location. ``parent`` Default pull/push target. ``pending-inventory`` Mapping from UUIDs to file name in the current working directory. ``branch-lock`` Lock held while modifying the branch, to protect against clashing updates. Locking ******* Is locking a good strategy? Perhaps somekind of read-copy-update or seq-lock based mechanism would work better? If we do use a locking algorithm, is it OK to rely on filesystem locking or do we need our own mechanism? I think most hosts should have reasonable ``flock()`` or equivalent, even on NFS. One risk is that on NFS it is easy to have broken locking and not know it, so it might be better to have something that will fail safe. Filesystem locks go away if the machine crashes or the process is terminated; this can be a feature in that we do not need to deal with stale locks but also a feature in that the lock itself does not indicate cleanup may be needed. robertc points out that tla converged on renaming a directory as a mechanism: this is one thing which is known to be atomic on almost all filesystems. Apparently renaming files, creating directories, making symlinks etc are not good enough. Delta ***** XML document plus a bag of patches, expressing the difference between two revisions. May be a partial delta. * list of entries * entry * parent directory (if any) * before-name or null if new * after-name or null if deleted * uuid * type (dir, file, symlink, ...) * patch type (patch, full-text, xdelta, ...) * patch filename (?) Inventory ********* XML document; series of entries. (Quite similar to the svn ``entries`` file; perhaps should even have that name.) Stored identified by its hash. An inventory is stored for recorded revisions, also a ``pending-inventory`` for a working directory. Inventories always have the same id as the revision they correspond to. bzr up to 0.0.5 explicitly stores an inventory-id; in future versions this may be implied. Revision ******** XML document. Stored identified by its hash. committer RFC-2822-style name of the committer. Should match the key used to sign the revision. comment multi-line free-form text; whitespace and line breaks preserved timestamp As floating-point seconds since epoch. branch name Name of the branch to which this was originally committed. (I'm not totally satisfied that this is the right way to do it; the results will be a bit weird when a series of revisions pass through variously named branches.) inventory_hash Acts as a pointer to the inventory for this revision. parents Zero, one, or more references to parent revisions. For each the revision-id and the revision file's hash are given. The first parent is by convention the revision in whose working tree the new revision was created. precursor Must be equal to the first parent, if any are given. For compatibility with bzr 0.0.5 and earlier; eventually will be removed. merged-branches Revision ids of complete branches merged into this revision. If a revision is listed, that revision and transitively its predecessor and all other merged-branches are merged. This is empty except where cherry-picks have occurred. merged-patches Revision ids of cherry-picked patches. Patches whose branches are merged need not be listed here. Listing a revision ID implies that only the change of that particular revision from its predecessor has been merged in. This is empty except where cherry-picks have occurred. The transitive closure avoids Arch's problem of needing to list a large number of previous revisions. As ddaa writes: Continuation revisions (created by tla tag or baz branch) are associated to a patchlog whose New-patches header lists the revisions associated to all the patchlogs present in the tree. That was introduced as an optimisation so the set of patchlogs in any revision could be determined solely by examining the patchlogs of ancestor revisions in the same branch. This behaves well as long as the total count of patchlog is reasonably small or new branches are not very frequent. A continuation revision on $tree currently creates a patchlog of about 500K. This patchlog is present in all descendent of the revision, and all revisions that merges it. It may be useful at some times to keep a cache of all the branches, or all the revisions, present in the history of a branch, so that we do need to walk the whole history of the branch to build this list. ---- Proposed changes **************** * Don't store parent-id in all revisions, but rather have nodes that contain entries for children? * Assign an id to the root of the tree, perhaps listed in the top of the inventory? commit refs/heads/master mark :820 committer Martin Pool 1120480113 +1000 data 72 - faster Branch.get_revision_inventory now we know the ids are the same from :819 M 644 inline bzrlib/branch.py data 40208 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, splitpath, \ sha_file, appendpath, file_kind from bzrlib.errors import BzrError BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n" ## TODO: Maybe include checks for common corruption of newlines, etc? def find_branch(f, **args): if f and (f.startswith('http://') or f.startswith('https://')): import remotebranch return remotebranch.RemoteBranch(f, **args) else: return Branch(f, **args) def find_cached_branch(f, cache_root, **args): from remotebranch import RemoteBranch br = find_branch(f, **args) def cacheify(br, store_name): from meta_store import CachedStore cache_path = os.path.join(cache_root, store_name) os.mkdir(cache_path) new_store = CachedStore(getattr(br, store_name), cache_path) setattr(br, store_name, new_store) if isinstance(br, RemoteBranch): cacheify(br, 'inventory_store') cacheify(br, 'text_store') cacheify(br, 'revision_store') return br def _relpath(base, path): """Return path relative to base, or raise exception. The path may be either an absolute path or a path relative to the current working directory. Lifted out of Branch.relpath for ease of testing. os.path.commonprefix (python2.4) has a bad bug that it works just on string prefixes, assuming that '/u' is a prefix of '/u2'. This avoids that problem.""" rp = os.path.abspath(path) s = [] head = rp while len(head) >= len(base): if head == base: break head, tail = os.path.split(head) if tail: s.insert(0, tail) else: from errors import NotBranchError raise NotBranchError("path %r is not within branch %r" % (rp, base)) return os.sep.join(s) def find_branch_root(f=None): """Find the branch root enclosing f, or pwd. f may be a filename or a URL. It is not necessary that f exists. Basically we keep looking up until we find the control directory or run into the root.""" if f == None: f = os.getcwd() elif hasattr(os.path, 'realpath'): f = os.path.realpath(f) else: f = os.path.abspath(f) if not os.path.exists(f): raise BzrError('%r does not exist' % f) orig_f = f while True: if os.path.exists(os.path.join(f, bzrlib.BZRDIR)): return f head, tail = os.path.split(f) if head == f: # reached the root, whatever that may be raise BzrError('%r is not in a branch' % orig_f) f = head class DivergedBranches(Exception): def __init__(self, branch1, branch2): self.branch1 = branch1 self.branch2 = branch2 Exception.__init__(self, "These branches have diverged.") class NoSuchRevision(BzrError): def __init__(self, branch, revision): self.branch = branch self.revision = revision msg = "Branch %s has no revision %d" % (branch, revision) BzrError.__init__(self, msg) ###################################################################### # branch objects class Branch(object): """Branch holding a history of revisions. base Base directory of the branch. _lock_mode None, or 'r' or 'w' _lock_count If _lock_mode is true, a positive count of the number of times the lock has been taken. _lock Lock object from bzrlib.lock. """ base = None _lock_mode = None _lock_count = None _lock = None def __init__(self, base, init=False, find_root=True): """Create new branch object at a particular location. base -- Base directory for the branch. init -- If True, create new control files in a previously unversioned directory. If False, the branch must already be versioned. find_root -- If true and init is false, find the root of the existing branch containing base. In the test suite, creation of new trees is tested using the `ScratchBranch` class. """ from bzrlib.store import ImmutableStore if init: self.base = os.path.realpath(base) self._make_control() elif find_root: self.base = find_branch_root(base) else: self.base = os.path.realpath(base) if not isdir(self.controlfilename('.')): from errors import NotBranchError raise NotBranchError("not a bzr branch: %s" % quotefn(base), ['use "bzr init" to initialize a new working tree', 'current bzr can only operate from top-of-tree']) self._check_format() self.text_store = ImmutableStore(self.controlfilename('text-store')) self.revision_store = ImmutableStore(self.controlfilename('revision-store')) self.inventory_store = ImmutableStore(self.controlfilename('inventory-store')) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.base) __repr__ = __str__ def __del__(self): if self._lock_mode or self._lock: from warnings import warn warn("branch %r was not explicitly unlocked" % self) self._lock.unlock() def lock_write(self): if self._lock_mode: if self._lock_mode != 'w': from errors import LockError raise LockError("can't upgrade to a write lock from %r" % self._lock_mode) self._lock_count += 1 else: from bzrlib.lock import WriteLock self._lock = WriteLock(self.controlfilename('branch-lock')) self._lock_mode = 'w' self._lock_count = 1 def lock_read(self): if self._lock_mode: assert self._lock_mode in ('r', 'w'), \ "invalid lock mode %r" % self._lock_mode self._lock_count += 1 else: from bzrlib.lock import ReadLock self._lock = ReadLock(self.controlfilename('branch-lock')) self._lock_mode = 'r' self._lock_count = 1 def unlock(self): if not self._lock_mode: from errors import LockError raise LockError('branch %r is not locked' % (self)) if self._lock_count > 1: self._lock_count -= 1 else: self._lock.unlock() self._lock = None self._lock_mode = self._lock_count = None def abspath(self, name): """Return absolute filename for something in the branch""" return os.path.join(self.base, name) def relpath(self, path): """Return path relative to this branch of something inside it. Raises an error if path is not in this branch.""" return _relpath(self.base, path) def controlfilename(self, file_or_path): """Return location relative to branch.""" if isinstance(file_or_path, basestring): file_or_path = [file_or_path] return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path) def controlfile(self, file_or_path, mode='r'): """Open a control file for this branch. There are two classes of file in the control directory: text and binary. binary files are untranslated byte streams. Text control files are stored with Unix newlines and in UTF-8, even if the platform or locale defaults are different. Controlfiles should almost never be opened in write mode but rather should be atomically copied and replaced using atomicfile. """ fn = self.controlfilename(file_or_path) if mode == 'rb' or mode == 'wb': return file(fn, mode) elif mode == 'r' or mode == 'w': # open in binary mode anyhow so there's no newline translation; # codecs uses line buffering by default; don't want that. import codecs return codecs.open(fn, mode + 'b', 'utf-8', buffering=60000) else: raise BzrError("invalid controlfile mode %r" % mode) def _make_control(self): from bzrlib.inventory import Inventory from bzrlib.xml import pack_xml os.mkdir(self.controlfilename([])) self.controlfile('README', 'w').write( "This is a Bazaar-NG control directory.\n" "Do not change any files in this directory.\n") self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT) for d in ('text-store', 'inventory-store', 'revision-store'): os.mkdir(self.controlfilename(d)) for f in ('revision-history', 'merged-patches', 'pending-merged-patches', 'branch-name', 'branch-lock', 'pending-merges'): self.controlfile(f, 'w').write('') mutter('created control directory in ' + self.base) pack_xml(Inventory(), self.controlfile('inventory','w')) def _check_format(self): """Check this branch format is supported. The current tool only supports the current unstable format. In the future, we might need different in-memory Branch classes to support downlevel branches. But not yet. """ # This ignores newlines so that we can open branches created # on Windows from Linux and so on. I think it might be better # to always make all internal files in unix format. fmt = self.controlfile('branch-format', 'r').read() fmt.replace('\r\n', '') if fmt != BZR_BRANCH_FORMAT: raise BzrError('sorry, branch format %r not supported' % fmt, ['use a different bzr version', 'or remove the .bzr directory and "bzr init" again']) def read_working_inventory(self): """Read the working inventory.""" from bzrlib.inventory import Inventory from bzrlib.xml import unpack_xml from time import time before = time() self.lock_read() try: # ElementTree does its own conversion from UTF-8, so open in # binary. inv = unpack_xml(Inventory, self.controlfile('inventory', 'rb')) mutter("loaded inventory of %d items in %f" % (len(inv), time() - before)) return inv finally: self.unlock() def _write_inventory(self, inv): """Update the working inventory. That is to say, the inventory describing changes underway, that will be committed to the next revision. """ from bzrlib.atomicfile import AtomicFile from bzrlib.xml import pack_xml self.lock_write() try: f = AtomicFile(self.controlfilename('inventory'), 'wb') try: pack_xml(inv, f) f.commit() finally: f.close() finally: self.unlock() mutter('wrote working inventory') inventory = property(read_working_inventory, _write_inventory, None, """Inventory for the working copy.""") def add(self, files, verbose=False, ids=None): """Make files versioned. Note that the command line normally calls smart_add instead. This puts the files in the Added state, so that they will be recorded by the next commit. files List of paths to add, relative to the base of the tree. ids If set, use these instead of automatically generated ids. Must be the same length as the list of files, but may contain None for ids that are to be autogenerated. TODO: Perhaps have an option to add the ids even if the files do not (yet) exist. TODO: Perhaps return the ids of the files? But then again it is easy to retrieve them if they're needed. TODO: Adding a directory should optionally recurse down and add all non-ignored children. Perhaps do that in a higher-level method. """ from bzrlib.textui import show_status # TODO: Re-adding a file that is removed in the working copy # should probably put it back with the previous ID. if isinstance(files, basestring): assert(ids is None or isinstance(ids, basestring)) files = [files] if ids is not None: ids = [ids] if ids is None: ids = [None] * len(files) else: assert(len(ids) == len(files)) self.lock_write() try: inv = self.read_working_inventory() for f,file_id in zip(files, ids): if is_control_file(f): raise BzrError("cannot add control file %s" % quotefn(f)) fp = splitpath(f) if len(fp) == 0: raise BzrError("cannot add top-level %r" % f) fullpath = os.path.normpath(self.abspath(f)) try: kind = file_kind(fullpath) except OSError: # maybe something better? raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if kind != 'file' and kind != 'directory': raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f)) if file_id is None: file_id = gen_file_id(f) inv.add_path(f, kind=kind, file_id=file_id) if verbose: print 'added', quotefn(f) mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind)) self._write_inventory(inv) finally: self.unlock() def print_file(self, file, revno): """Print `file` to stdout.""" self.lock_read() try: tree = self.revision_tree(self.lookup_revision(revno)) # use inventory as it was in that revision file_id = tree.inventory.path2id(file) if not file_id: raise BzrError("%r is not present in revision %d" % (file, revno)) tree.print_file(file_id) finally: self.unlock() def remove(self, files, verbose=False): """Mark nominated files for removal from the inventory. This does not remove their text. This does not run on TODO: Refuse to remove modified files unless --force is given? TODO: Do something useful with directories. TODO: Should this remove the text or not? Tough call; not removing may be useful and the user can just use use rm, and is the opposite of add. Removing it is consistent with most other tools. Maybe an option. """ from bzrlib.textui import show_status ## TODO: Normalize names ## TODO: Remove nested loops; better scalability if isinstance(files, basestring): files = [files] self.lock_write() try: tree = self.working_tree() inv = tree.inventory # do this before any modifications for f in files: fid = inv.path2id(f) if not fid: raise BzrError("cannot remove unversioned file %s" % quotefn(f)) mutter("remove inventory entry %s {%s}" % (quotefn(f), fid)) if verbose: # having remove it, it must be either ignored or unknown if tree.is_ignored(f): new_status = 'I' else: new_status = '?' show_status(new_status, inv[fid].kind, quotefn(f)) del inv[fid] self._write_inventory(inv) finally: self.unlock() # FIXME: this doesn't need to be a branch method def set_inventory(self, new_inventory_list): from bzrlib.inventory import Inventory, InventoryEntry inv = Inventory() for path, file_id, parent, kind in new_inventory_list: name = os.path.basename(path) if name == "": continue inv.add(InventoryEntry(file_id, name, kind, parent)) self._write_inventory(inv) def unknowns(self): """Return all unknown files. These are files in the working directory that are not versioned or control files or ignored. >>> b = ScratchBranch(files=['foo', 'foo~']) >>> list(b.unknowns()) ['foo'] >>> b.add('foo') >>> list(b.unknowns()) [] >>> b.remove('foo') >>> list(b.unknowns()) ['foo'] """ return self.working_tree().unknowns() def append_revision(self, revision_id): from bzrlib.atomicfile import AtomicFile mutter("add {%s} to revision-history" % revision_id) rev_history = self.revision_history() + [revision_id] f = AtomicFile(self.controlfilename('revision-history')) try: for rev_id in rev_history: print >>f, rev_id f.commit() finally: f.close() def get_revision(self, revision_id): """Return the Revision object for a named revision""" from bzrlib.revision import Revision from bzrlib.xml import unpack_xml self.lock_read() try: if not revision_id or not isinstance(revision_id, basestring): raise ValueError('invalid revision-id: %r' % revision_id) r = unpack_xml(Revision, self.revision_store[revision_id]) finally: self.unlock() assert r.revision_id == revision_id return r def get_revision_sha1(self, revision_id): """Hash the stored value of a revision, and return it.""" # In the future, revision entries will be signed. At that # point, it is probably best *not* to include the signature # in the revision hash. Because that lets you re-sign # the revision, (add signatures/remove signatures) and still # have all hash pointers stay consistent. # But for now, just hash the contents. return sha_file(self.revision_store[revision_id]) def get_inventory(self, inventory_id): """Get Inventory object by hash. TODO: Perhaps for this and similar methods, take a revision parameter which can be either an integer revno or a string hash.""" from bzrlib.inventory import Inventory from bzrlib.xml import unpack_xml return unpack_xml(Inventory, self.inventory_store[inventory_id]) def get_inventory_sha1(self, inventory_id): """Return the sha1 hash of the inventory entry """ return sha_file(self.inventory_store[inventory_id]) def get_revision_inventory(self, revision_id): """Return inventory of a past revision.""" # bzr 0.0.6 imposes the constraint that the inventory_id # must be the same as its revision, so this is trivial. if revision_id == None: from bzrlib.inventory import Inventory return Inventory() else: return self.get_inventory(revision_id) def revision_history(self): """Return sequence of revision hashes on to this branch. >>> ScratchBranch().revision_history() [] """ self.lock_read() try: return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()] finally: self.unlock() def common_ancestor(self, other, self_revno=None, other_revno=None): """ >>> import commit >>> sb = ScratchBranch(files=['foo', 'foo~']) >>> sb.common_ancestor(sb) == (None, None) True >>> commit.commit(sb, "Committing first revision", verbose=False) >>> sb.common_ancestor(sb)[0] 1 >>> clone = sb.clone() >>> commit.commit(sb, "Committing second revision", verbose=False) >>> sb.common_ancestor(sb)[0] 2 >>> sb.common_ancestor(clone)[0] 1 >>> commit.commit(clone, "Committing divergent second revision", ... verbose=False) >>> sb.common_ancestor(clone)[0] 1 >>> sb.common_ancestor(clone) == clone.common_ancestor(sb) True >>> sb.common_ancestor(sb) != clone.common_ancestor(clone) True >>> clone2 = sb.clone() >>> sb.common_ancestor(clone2)[0] 2 >>> sb.common_ancestor(clone2, self_revno=1)[0] 1 >>> sb.common_ancestor(clone2, other_revno=1)[0] 1 """ my_history = self.revision_history() other_history = other.revision_history() if self_revno is None: self_revno = len(my_history) if other_revno is None: other_revno = len(other_history) indices = range(min((self_revno, other_revno))) indices.reverse() for r in indices: if my_history[r] == other_history[r]: return r+1, my_history[r] return None, None def enum_history(self, direction): """Return (revno, revision_id) for history of branch. direction 'forward' is from earliest to latest 'reverse' is from latest to earliest """ rh = self.revision_history() if direction == 'forward': i = 1 for rid in rh: yield i, rid i += 1 elif direction == 'reverse': i = len(rh) while i > 0: yield i, rh[i-1] i -= 1 else: raise ValueError('invalid history direction', direction) def revno(self): """Return current revision number for this branch. That is equivalent to the number of revisions committed to this branch. """ return len(self.revision_history()) def last_patch(self): """Return last patch hash, or None if no history. """ ph = self.revision_history() if ph: return ph[-1] else: return None def missing_revisions(self, other, stop_revision=None): """ If self and other have not diverged, return a list of the revisions present in other, but missing from self. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch() >>> br2 = ScratchBranch() >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [u'REVISION-ID-1'] >>> br2.missing_revisions(br1) [] >>> commit(br1, "lala!", rev_id="REVISION-ID-1") >>> br1.missing_revisions(br2) [] >>> commit(br2, "lala!", rev_id="REVISION-ID-2A") >>> br1.missing_revisions(br2) [u'REVISION-ID-2A'] >>> commit(br1, "lala!", rev_id="REVISION-ID-2B") >>> br1.missing_revisions(br2) Traceback (most recent call last): DivergedBranches: These branches have diverged. """ self_history = self.revision_history() self_len = len(self_history) other_history = other.revision_history() other_len = len(other_history) common_index = min(self_len, other_len) -1 if common_index >= 0 and \ self_history[common_index] != other_history[common_index]: raise DivergedBranches(self, other) if stop_revision is None: stop_revision = other_len elif stop_revision > other_len: raise NoSuchRevision(self, stop_revision) return other_history[self_len:stop_revision] def update_revisions(self, other, stop_revision=None): """Pull in all new revisions from other branch. >>> from bzrlib.commit import commit >>> bzrlib.trace.silent = True >>> br1 = ScratchBranch(files=['foo', 'bar']) >>> br1.add('foo') >>> br1.add('bar') >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False) >>> br2 = ScratchBranch() >>> br2.update_revisions(br1) Added 2 texts. Added 1 inventories. Added 1 revisions. >>> br2.revision_history() [u'REVISION-ID-1'] >>> br2.update_revisions(br1) Added 0 texts. Added 0 inventories. Added 0 revisions. >>> br1.text_store.total_size() == br2.text_store.total_size() True """ from bzrlib.progress import ProgressBar try: set except NameError: from sets import Set as set pb = ProgressBar() pb.update('comparing histories') revision_ids = self.missing_revisions(other, stop_revision) if hasattr(other.revision_store, "prefetch"): other.revision_store.prefetch(revision_ids) if hasattr(other.inventory_store, "prefetch"): inventory_ids = [other.get_revision(r).inventory_id for r in revision_ids] other.inventory_store.prefetch(inventory_ids) revisions = [] needed_texts = set() i = 0 for rev_id in revision_ids: i += 1 pb.update('fetching revision', i, len(revision_ids)) rev = other.get_revision(rev_id) revisions.append(rev) inv = other.get_inventory(str(rev.inventory_id)) for key, entry in inv.iter_entries(): if entry.text_id is None: continue if entry.text_id not in self.text_store: needed_texts.add(entry.text_id) pb.clear() count = self.text_store.copy_multi(other.text_store, needed_texts) print "Added %d texts." % count inventory_ids = [ f.inventory_id for f in revisions ] count = self.inventory_store.copy_multi(other.inventory_store, inventory_ids) print "Added %d inventories." % count revision_ids = [ f.revision_id for f in revisions] count = self.revision_store.copy_multi(other.revision_store, revision_ids) for revision_id in revision_ids: self.append_revision(revision_id) print "Added %d revisions." % count def commit(self, *args, **kw): from bzrlib.commit import commit commit(self, *args, **kw) def lookup_revision(self, revno): """Return revision hash for revision number.""" if revno == 0: return None try: # list is 0-based; revisions are 1-based return self.revision_history()[revno-1] except IndexError: raise BzrError("no such revision %s" % revno) def revision_tree(self, revision_id): """Return Tree for a revision on this branch. `revision_id` may be None for the null revision, in which case an `EmptyTree` is returned.""" from bzrlib.tree import EmptyTree, RevisionTree # TODO: refactor this to use an existing revision object # so we don't need to read it in twice. if revision_id == None: return EmptyTree() else: inv = self.get_revision_inventory(revision_id) return RevisionTree(self.text_store, inv) def working_tree(self): """Return a `Tree` for the working copy.""" from workingtree import WorkingTree return WorkingTree(self.base, self.read_working_inventory()) def basis_tree(self): """Return `Tree` object for last revision. If there are no revisions yet, return an `EmptyTree`. """ from bzrlib.tree import EmptyTree, RevisionTree r = self.last_patch() if r == None: return EmptyTree() else: return RevisionTree(self.text_store, self.get_revision_inventory(r)) def rename_one(self, from_rel, to_rel): """Rename one file. This can change the directory or the filename or both. """ self.lock_write() try: tree = self.working_tree() inv = tree.inventory if not tree.has_filename(from_rel): raise BzrError("can't rename: old working file %r does not exist" % from_rel) if tree.has_filename(to_rel): raise BzrError("can't rename: new working file %r already exists" % to_rel) file_id = inv.path2id(from_rel) if file_id == None: raise BzrError("can't rename: old name %r is not versioned" % from_rel) if inv.path2id(to_rel): raise BzrError("can't rename: new name %r is already versioned" % to_rel) to_dir, to_tail = os.path.split(to_rel) to_dir_id = inv.path2id(to_dir) if to_dir_id == None and to_dir != '': raise BzrError("can't determine destination directory id for %r" % to_dir) mutter("rename_one:") mutter(" file_id {%s}" % file_id) mutter(" from_rel %r" % from_rel) mutter(" to_rel %r" % to_rel) mutter(" to_dir %r" % to_dir) mutter(" to_dir_id {%s}" % to_dir_id) inv.rename(file_id, to_dir_id, to_tail) print "%s => %s" % (from_rel, to_rel) from_abs = self.abspath(from_rel) to_abs = self.abspath(to_rel) try: os.rename(from_abs, to_abs) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (from_abs, to_abs, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() def move(self, from_paths, to_name): """Rename files. to_name must exist as a versioned directory. If to_name exists and is a directory, the files are moved into it, keeping their old names. If it is a directory, Note that to_name is only the last component of the new name; this doesn't change the directory. """ self.lock_write() try: ## TODO: Option to move IDs only assert not isinstance(from_paths, basestring) tree = self.working_tree() inv = tree.inventory to_abs = self.abspath(to_name) if not isdir(to_abs): raise BzrError("destination %r is not a directory" % to_abs) if not tree.has_filename(to_name): raise BzrError("destination %r not in working directory" % to_abs) to_dir_id = inv.path2id(to_name) if to_dir_id == None and to_name != '': raise BzrError("destination %r is not a versioned directory" % to_name) to_dir_ie = inv[to_dir_id] if to_dir_ie.kind not in ('directory', 'root_directory'): raise BzrError("destination %r is not a directory" % to_abs) to_idpath = inv.get_idpath(to_dir_id) for f in from_paths: if not tree.has_filename(f): raise BzrError("%r does not exist in working tree" % f) f_id = inv.path2id(f) if f_id == None: raise BzrError("%r is not versioned" % f) name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) if tree.has_filename(dest_path): raise BzrError("destination %r already exists" % dest_path) if f_id in to_idpath: raise BzrError("can't move %r to a subdirectory of itself" % f) # OK, so there's a race here, it's possible that someone will # create a file in this interval and then the rename might be # left half-done. But we should have caught most problems. for f in from_paths: name_tail = splitpath(f)[-1] dest_path = appendpath(to_name, name_tail) print "%s => %s" % (f, dest_path) inv.rename(inv.path2id(f), to_dir_id, name_tail) try: os.rename(self.abspath(f), self.abspath(dest_path)) except OSError, e: raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]), ["rename rolled back"]) self._write_inventory(inv) finally: self.unlock() def revert(self, filenames, old_tree=None, backups=True): """Restore selected files to the versions from a previous tree. backups If true (default) backups are made of files before they're renamed. """ from bzrlib.errors import NotVersionedError, BzrError from bzrlib.atomicfile import AtomicFile from bzrlib.osutils import backup_file inv = self.read_working_inventory() if old_tree is None: old_tree = self.basis_tree() old_inv = old_tree.inventory nids = [] for fn in filenames: file_id = inv.path2id(fn) if not file_id: raise NotVersionedError("not a versioned file", fn) if not old_inv.has_id(file_id): raise BzrError("file not present in old tree", fn, file_id) nids.append((fn, file_id)) # TODO: Rename back if it was previously at a different location # TODO: If given a directory, restore the entire contents from # the previous version. # TODO: Make a backup to a temporary file. # TODO: If the file previously didn't exist, delete it? for fn, file_id in nids: backup_file(fn) f = AtomicFile(fn, 'wb') try: f.write(old_tree.get_file(file_id).read()) f.commit() finally: f.close() def pending_merges(self): """Return a list of pending merges. These are revisions that have been merged into the working directory but not yet committed. """ cfn = self.controlfilename('pending-merges') if not os.path.exists(cfn): return [] p = [] for l in self.controlfile('pending-merges', 'r').readlines(): p.append(l.rstrip('\n')) return p def add_pending_merge(self, revision_id): from bzrlib.revision import validate_revision_id validate_revision_id(revision_id) p = self.pending_merges() if revision_id in p: return p.append(revision_id) self.set_pending_merges(p) def set_pending_merges(self, rev_list): from bzrlib.atomicfile import AtomicFile self.lock_write() try: f = AtomicFile(self.controlfilename('pending-merges')) try: for l in rev_list: print >>f, l f.commit() finally: f.close() finally: self.unlock() class ScratchBranch(Branch): """Special test class: a branch that cleans up after itself. >>> b = ScratchBranch() >>> isdir(b.base) True >>> bd = b.base >>> b.destroy() >>> isdir(bd) False """ def __init__(self, files=[], dirs=[], base=None): """Make a test branch. This creates a temporary directory and runs init-tree in it. If any files are listed, they are created in the working copy. """ from tempfile import mkdtemp init = False if base is None: base = mkdtemp() init = True Branch.__init__(self, base, init=init) for d in dirs: os.mkdir(self.abspath(d)) for f in files: file(os.path.join(self.base, f), 'w').write('content of %s' % f) def clone(self): """ >>> orig = ScratchBranch(files=["file1", "file2"]) >>> clone = orig.clone() >>> os.path.samefile(orig.base, clone.base) False >>> os.path.isfile(os.path.join(clone.base, "file1")) True """ from shutil import copytree from tempfile import mkdtemp base = mkdtemp() os.rmdir(base) copytree(self.base, base, symlinks=True) return ScratchBranch(base=base) def __del__(self): self.destroy() def destroy(self): """Destroy the test branch, removing the scratch directory.""" from shutil import rmtree try: if self.base: mutter("delete ScratchBranch %s" % self.base) rmtree(self.base) except OSError, e: # Work around for shutil.rmtree failing on Windows when # readonly files are encountered mutter("hit exception in destroying ScratchBranch: %s" % e) for root, dirs, files in os.walk(self.base, topdown=False): for name in files: os.chmod(os.path.join(root, name), 0700) rmtree(self.base) self.base = None ###################################################################### # predicates def is_control_file(filename): ## FIXME: better check filename = os.path.normpath(filename) while filename != '': head, tail = os.path.split(filename) ## mutter('check %r for control file' % ((head, tail), )) if tail == bzrlib.BZRDIR: return True if filename == head: break filename = head return False def gen_file_id(name): """Return new file id. This should probably generate proper UUIDs, but for the moment we cope with just randomness because running uuidgen every time is slow.""" import re from binascii import hexlify from time import time # get last component idx = name.rfind('/') if idx != -1: name = name[idx+1 : ] idx = name.rfind('\\') if idx != -1: name = name[idx+1 : ] # make it not a hidden file name = name.lstrip('.') # remove any wierd characters; we don't escape them but rather # just pull them out name = re.sub(r'[^\w.]', '', name) s = hexlify(rand_bytes(8)) return '-'.join((name, compact_date(time()), s)) commit refs/heads/master mark :821 committer Martin Pool 1120482535 +1000 data 69 - start code for built-in diff3-style resolve - test cases for this from :820 M 644 inline bzrlib/merge3.py data 2859 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def intersect(ra, rb): """Given two ranges return the range where they intersect or None. >>> intersect((0, 10), (0, 6)) (0, 6) >>> intersect((0, 10), (5, 15)) (5, 10) >>> intersect((0, 10), (10, 15)) >>> intersect((0, 9), (10, 15)) >>> intersect((0, 9), (7, 15)) (7, 9) """ assert ra[0] <= ra[1] assert rb[0] <= rb[1] sa = max(ra[0], rb[0]) sb = min(ra[1], rb[1]) if sa < sb: return sa, sb else: return None class Merge3(object): """3-way merge of texts. Given BASE, OTHER, THIS, tries to produce a combined text incorporating the changes from both BASE->OTHER and BASE->THIS. All three will typically be sequences of lines.""" def __init__(self, base, a, b): self.base = base self.a = a self.b = b #from difflib import SequenceMatcher #self.a_ops = SequenceMatcher(None, self.base, self.a).get_opcodes() #self.b_ops = SequenceMatcher(None, self.base, self.b).get_opcodes() def find_conflicts(self): """Return a list of conflict regions. Each entry is given as (base1, base2, a1, a2, b1, b2). This indicates that the range [base1,base2] can be replaced by either [a1,a2] or [b1,b2]. """ def find_unconflicted(self): """Return a list of ranges in base that are not conflicted.""" from difflib import SequenceMatcher am = SequenceMatcher(None, self.base, self.a).get_matching_blocks() bm = SequenceMatcher(None, self.base, self.b).get_matching_blocks() unc = [] while am and bm: # there is an unconflicted block at i; how long does it # extend? until whichever one ends earlier. a1 = am[0][0] a2 = a1 + am[0][2] b1 = bm[0][0] b2 = b1 + bm[0][2] i = intersect((a1, a2), (b1, b2)) if i: unc.append(i) if a2 < b2: del am[0] else: del bm[0] return unc M 644 inline bzrlib/selftest/merge3.py data 2124 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from bzrlib.selftest import InTempDir, TestBase from bzrlib.merge3 import Merge3 class NoConflicts(TestBase): """No conflicts because only one side changed""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (1, 2)]) class NoChanges(TestBase): """No conflicts because nothing changed""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', 'bbb'], ['aaa', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 2)]) class InsertClash(TestBase): """Both try to insert lines in the same place.""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', '222', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (1, 2)]) class ReplaceClash(TestBase): """Both try to insert lines in the same place.""" def runTest(self): m3 = Merge3(['aaa', '000', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', '222', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (2, 3)]) M 644 inline bzrlib/selftest/__init__.py data 9236 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") raise class CommandFailed(Exception): pass class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr, so that we can run it # through a specified Python intepreter OVERRIDE_PYTHON = None # to run with alternative python 'python' BZRPATH = 'bzr' _log_buf = "" def formcmd(self, cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = self.BZRPATH if self.OVERRIDE_PYTHON: cmd.insert(0, self.OVERRIDE_PYTHON) self.log('$ %r' % cmd) return cmd def runcmd(self, cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = self.formcmd(cmd) self.log('$ ' + ' '.join(cmd)) actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(self, cmd, retcode=0): """Run a command and return its output""" cmd = self.formcmd(cmd) child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG) outd, errd = child.communicate() self.log(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def build_tree(self, shape): """Build a test tree according to a pattern. shape is a sequence of file specifications. If the final character is '/', a directory is created. This doesn't add anything to a branch. """ # XXX: It's OK to just create them using forward slashes on windows? import os for name in shape: assert isinstance(name, basestring) if name[-1] == '/': os.mkdir(name[:-1]) else: f = file(name, 'wt') print >>f, "contents of", name f.close() def log(self, msg): """Log a message to a progress file""" self._log_buf = self._log_buf + str(msg) + '\n' print >>self.TEST_LOG, msg def check_inventory_shape(self, inv, shape): """ Compare an inventory to a list of expected names. Fail if they are not precisely equal. """ extras = [] shape = list(shape) # copy for path, ie in inv.entries(): name = path.replace('\\', '/') if ie.kind == 'dir': name = name + '/' if name in shape: shape.remove(name) else: extras.append(name) if shape: self.fail("expected paths not found in inventory: %r" % shape) if extras: self.fail("unexpected paths found in inventory: %r" % extras) def check_file_contents(self, filename, expect): self.log("check contents of file %s" % filename) contents = file(filename, 'r').read() if contents != expect: self.log("expected: %r" % expected) self.log("actually: %r" % contents) self.fail("contents of %s not as expected") class InTempDir(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.test_dir = os.path.join(self.TEST_ROOT, self.__class__.__name__) os.mkdir(self.test_dir) os.chdir(self.test_dir) def tearDown(self): import os os.chdir(self.TEST_ROOT) class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ def __init__(self, out): self.out = out TestResult.__init__(self) def startTest(self, test): # TODO: Maybe show test.shortDescription somewhere? print >>self.out, '%-60.60s' % test.id(), self.out.flush() TestResult.startTest(self, test) def stopTest(self, test): # print TestResult.stopTest(self, test) def addError(self, test, err): print >>self.out, 'ERROR' TestResult.addError(self, test, err) _show_test_failure('error', test, err, self.out) def addFailure(self, test, err): print >>self.out, 'FAILURE' TestResult.addFailure(self, test, err) _show_test_failure('failure', test, err, self.out) def addSuccess(self, test): print >>self.out, 'OK' TestResult.addSuccess(self, test) def selftest(): from unittest import TestLoader, TestSuite import bzrlib, bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, bzrlib.commands import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning import bzrlib.selftest.merge3 import bzrlib.merge_core from doctest import DocTestSuite import os import shutil import time import sys TestBase.BZRPATH = os.path.join(os.path.realpath(os.path.dirname(bzrlib.__path__[0])), 'bzr') print '%-30s %s' % ('bzr binary', TestBase.BZRPATH) _setup_test_log() _setup_test_dir() print suite = TestSuite() tl = TestLoader() # should also test bzrlib.merge_core, but they seem to be out of date with # the code. for m in bzrlib.selftest.whitebox, \ bzrlib.selftest.versioning, \ bzrlib.selftest.merge3: suite.addTest(tl.loadTestsFromModule(m)) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands, \ bzrlib.merge3: suite.addTest(DocTestSuite(m)) suite.addTest(bzrlib.selftest.blackbox.suite()) # save stdout & stderr so there's no leakage from code-under-test real_stdout = sys.stdout real_stderr = sys.stderr sys.stdout = sys.stderr = TestBase.TEST_LOG try: result = _MyResult(real_stdout) suite.run(result) finally: sys.stdout = real_stdout sys.stderr = real_stderr _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os log_filename = os.path.abspath('testbzr.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "bzr tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_ROOT = os.path.abspath("testbzr.tmp") print '%-30s %s' % ('running tests in', TestBase.TEST_ROOT) if os.path.exists(TestBase.TEST_ROOT): shutil.rmtree(TestBase.TEST_ROOT) os.mkdir(TestBase.TEST_ROOT) os.chdir(TestBase.TEST_ROOT) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_ROOT, '.bzr')) def _show_results(result): print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, exc_info, out): from traceback import print_exception print >>out, '-' * 60 print >>out, case desc = case.shortDescription() if desc: print >>out, ' (%s)' % desc print_exception(exc_info[0], exc_info[1], exc_info[2], None, out) if isinstance(case, TestBase): print >>out print >>out, 'log from this test:' print >>out, case._log_buf print >>out, '-' * 60 commit refs/heads/master mark :822 committer Martin Pool 1120547952 +1000 data 200 - Renamed merge3 test suite for easier access. - New merge approach based on finding triple-matching regions, and comparing the regions between them; add find_sync_regions() and some tests for it. from :821 R bzrlib/selftest/merge3.py bzrlib/selftest/testmerge3.py M 644 inline bzrlib/merge3.py data 5214 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def intersect(ra, rb): """Given two ranges return the range where they intersect or None. >>> intersect((0, 10), (0, 6)) (0, 6) >>> intersect((0, 10), (5, 15)) (5, 10) >>> intersect((0, 10), (10, 15)) >>> intersect((0, 9), (10, 15)) >>> intersect((0, 9), (7, 15)) (7, 9) """ assert ra[0] <= ra[1] assert rb[0] <= rb[1] sa = max(ra[0], rb[0]) sb = min(ra[1], rb[1]) if sa < sb: return sa, sb else: return None def threeway(baseline, aline, bline): if baseline == aline: return bline elif baseline == bline: return aline else: return [aline, bline] class Merge3(object): """3-way merge of texts. Given BASE, OTHER, THIS, tries to produce a combined text incorporating the changes from both BASE->OTHER and BASE->THIS. All three will typically be sequences of lines.""" def __init__(self, base, a, b): self.base = base self.a = a self.b = b from difflib import SequenceMatcher self.a_ops = SequenceMatcher(None, base, a).get_opcodes() self.b_ops = SequenceMatcher(None, base, b).get_opcodes() def merge(self): """Return sequences of matching and conflicting regions. Method is as follows: The two sequences align only on regions which match the base and both descendents. These are found by doing a two-way diff of each one against the base, and then finding the intersections between those regions. These "sync regions" are by definition unchanged in both and easily dealt with. The regions in between can be in any of three cases: conflicted, or changed on only one side. """ def find_sync_regions(self): """Return a list of sync regions, where both descendents match the base. Generates a list of ((base1, base2), (a1, a2), (b1, b2)). """ from difflib import SequenceMatcher aiter = iter(SequenceMatcher(None, self.base, self.a).get_matching_blocks()) biter = iter(SequenceMatcher(None, self.base, self.b).get_matching_blocks()) abase, amatch, alen = aiter.next() bbase, bmatch, blen = biter.next() while aiter and biter: # there is an unconflicted block at i; how long does it # extend? until whichever one ends earlier. i = intersect((abase, abase+alen), (bbase, bbase+blen)) if i: intbase = i[0] intend = i[1] intlen = intend - intbase # found a match of base[i[0], i[1]]; this may be less than # the region that matches in either one assert intlen <= alen assert intlen <= blen assert abase <= intbase assert bbase <= intbase asub = amatch + (intbase - abase) bsub = bmatch + (intbase - bbase) aend = asub + intlen bend = bsub + intlen assert self.base[intbase:intend] == self.a[asub:aend], \ (self.base[intbase:intend], self.a[asub:aend]) assert self.base[intbase:intend] == self.b[bsub:bend] yield ((intbase, intend), (asub, aend), (bsub, bend)) # advance whichever one ends first in the base text if (abase + alen) < (bbase + blen): abase, amatch, alen = aiter.next() else: bbase, bmatch, blen = biter.next() def find_unconflicted(self): """Return a list of ranges in base that are not conflicted.""" from difflib import SequenceMatcher am = SequenceMatcher(None, self.base, self.a).get_matching_blocks() bm = SequenceMatcher(None, self.base, self.b).get_matching_blocks() unc = [] while am and bm: # there is an unconflicted block at i; how long does it # extend? until whichever one ends earlier. a1 = am[0][0] a2 = a1 + am[0][2] b1 = bm[0][0] b2 = b1 + bm[0][2] i = intersect((a1, a2), (b1, b2)) if i: unc.append(i) if a2 < b2: del am[0] else: del bm[0] return unc M 644 inline bzrlib/selftest/__init__.py data 9244 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("testbzr: sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") raise class CommandFailed(Exception): pass class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr, so that we can run it # through a specified Python intepreter OVERRIDE_PYTHON = None # to run with alternative python 'python' BZRPATH = 'bzr' _log_buf = "" def formcmd(self, cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = self.BZRPATH if self.OVERRIDE_PYTHON: cmd.insert(0, self.OVERRIDE_PYTHON) self.log('$ %r' % cmd) return cmd def runcmd(self, cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = self.formcmd(cmd) self.log('$ ' + ' '.join(cmd)) actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(self, cmd, retcode=0): """Run a command and return its output""" cmd = self.formcmd(cmd) child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG) outd, errd = child.communicate() self.log(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def build_tree(self, shape): """Build a test tree according to a pattern. shape is a sequence of file specifications. If the final character is '/', a directory is created. This doesn't add anything to a branch. """ # XXX: It's OK to just create them using forward slashes on windows? import os for name in shape: assert isinstance(name, basestring) if name[-1] == '/': os.mkdir(name[:-1]) else: f = file(name, 'wt') print >>f, "contents of", name f.close() def log(self, msg): """Log a message to a progress file""" self._log_buf = self._log_buf + str(msg) + '\n' print >>self.TEST_LOG, msg def check_inventory_shape(self, inv, shape): """ Compare an inventory to a list of expected names. Fail if they are not precisely equal. """ extras = [] shape = list(shape) # copy for path, ie in inv.entries(): name = path.replace('\\', '/') if ie.kind == 'dir': name = name + '/' if name in shape: shape.remove(name) else: extras.append(name) if shape: self.fail("expected paths not found in inventory: %r" % shape) if extras: self.fail("unexpected paths found in inventory: %r" % extras) def check_file_contents(self, filename, expect): self.log("check contents of file %s" % filename) contents = file(filename, 'r').read() if contents != expect: self.log("expected: %r" % expected) self.log("actually: %r" % contents) self.fail("contents of %s not as expected") class InTempDir(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.test_dir = os.path.join(self.TEST_ROOT, self.__class__.__name__) os.mkdir(self.test_dir) os.chdir(self.test_dir) def tearDown(self): import os os.chdir(self.TEST_ROOT) class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ def __init__(self, out): self.out = out TestResult.__init__(self) def startTest(self, test): # TODO: Maybe show test.shortDescription somewhere? print >>self.out, '%-60.60s' % test.id(), self.out.flush() TestResult.startTest(self, test) def stopTest(self, test): # print TestResult.stopTest(self, test) def addError(self, test, err): print >>self.out, 'ERROR' TestResult.addError(self, test, err) _show_test_failure('error', test, err, self.out) def addFailure(self, test, err): print >>self.out, 'FAILURE' TestResult.addFailure(self, test, err) _show_test_failure('failure', test, err, self.out) def addSuccess(self, test): print >>self.out, 'OK' TestResult.addSuccess(self, test) def selftest(): from unittest import TestLoader, TestSuite import bzrlib, bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, bzrlib.commands import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning import bzrlib.selftest.testmerge3 import bzrlib.merge_core from doctest import DocTestSuite import os import shutil import time import sys TestBase.BZRPATH = os.path.join(os.path.realpath(os.path.dirname(bzrlib.__path__[0])), 'bzr') print '%-30s %s' % ('bzr binary', TestBase.BZRPATH) _setup_test_log() _setup_test_dir() print suite = TestSuite() tl = TestLoader() # should also test bzrlib.merge_core, but they seem to be out of date with # the code. for m in bzrlib.selftest.whitebox, \ bzrlib.selftest.versioning, \ bzrlib.selftest.testmerge3: suite.addTest(tl.loadTestsFromModule(m)) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands, \ bzrlib.merge3: suite.addTest(DocTestSuite(m)) suite.addTest(bzrlib.selftest.blackbox.suite()) # save stdout & stderr so there's no leakage from code-under-test real_stdout = sys.stdout real_stderr = sys.stderr sys.stdout = sys.stderr = TestBase.TEST_LOG try: result = _MyResult(real_stdout) suite.run(result) finally: sys.stdout = real_stdout sys.stderr = real_stderr _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os log_filename = os.path.abspath('testbzr.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "bzr tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_ROOT = os.path.abspath("testbzr.tmp") print '%-30s %s' % ('running tests in', TestBase.TEST_ROOT) if os.path.exists(TestBase.TEST_ROOT): shutil.rmtree(TestBase.TEST_ROOT) os.mkdir(TestBase.TEST_ROOT) os.chdir(TestBase.TEST_ROOT) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_ROOT, '.bzr')) def _show_results(result): print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, exc_info, out): from traceback import print_exception print >>out, '-' * 60 print >>out, case desc = case.shortDescription() if desc: print >>out, ' (%s)' % desc print_exception(exc_info[0], exc_info[1], exc_info[2], None, out) if isinstance(case, TestBase): print >>out print >>out, 'log from this test:' print >>out, case._log_buf print >>out, '-' * 60 M 644 inline bzrlib/selftest/testmerge3.py data 3264 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from bzrlib.selftest import InTempDir, TestBase from bzrlib.merge3 import Merge3 class NoChanges(TestBase): """No conflicts because nothing changed""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', 'bbb'], ['aaa', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 2)]) self.assertEquals(list(m3.find_sync_regions()), [((0, 2), (0, 2), (0, 2))]) class NoConflicts(TestBase): """No conflicts because only one side changed""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (1, 2)]) self.assertEquals(list(m3.find_sync_regions()), [((0, 1), (0, 1), (0, 1)), ((1, 2), (2, 3), (1, 2))]) class InsertClash(TestBase): """Both try to insert lines in the same place.""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', '222', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (1, 2)]) self.assertEquals(list(m3.find_sync_regions()), [((0, 1), (0, 1), (0, 1)), ((1, 2), (2, 3), (2, 3))]) class ReplaceClash(TestBase): """Both try to insert lines in the same place.""" def runTest(self): m3 = Merge3(['aaa', '000', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', '222', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (2, 3)]) self.assertEquals(list(m3.find_sync_regions()), [((0, 1), (0, 1), (0, 1)), ((2, 3), (2, 3), (2, 3))]) class ReplaceMulti(TestBase): """Replacement with regions of different size.""" def runTest(self): m3 = Merge3(['aaa', '000', '000', 'bbb'], ['aaa', '111', '111', '111', 'bbb'], ['aaa', '222', '222', '222', '222', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (3, 4)]) self.assertEquals(list(m3.find_sync_regions()), [((0, 1), (0, 1), (0, 1)), ((3, 4), (4, 5), (5, 6))]) commit refs/heads/master mark :823 committer Martin Pool 1120548256 +1000 data 5 quote from :822 M 644 inline bzrlib/merge3.py data 5303 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # mbp: "you know that thing where cvs gives you conflict markers?" # s: "i hate that." def intersect(ra, rb): """Given two ranges return the range where they intersect or None. >>> intersect((0, 10), (0, 6)) (0, 6) >>> intersect((0, 10), (5, 15)) (5, 10) >>> intersect((0, 10), (10, 15)) >>> intersect((0, 9), (10, 15)) >>> intersect((0, 9), (7, 15)) (7, 9) """ assert ra[0] <= ra[1] assert rb[0] <= rb[1] sa = max(ra[0], rb[0]) sb = min(ra[1], rb[1]) if sa < sb: return sa, sb else: return None def threeway(baseline, aline, bline): if baseline == aline: return bline elif baseline == bline: return aline else: return [aline, bline] class Merge3(object): """3-way merge of texts. Given BASE, OTHER, THIS, tries to produce a combined text incorporating the changes from both BASE->OTHER and BASE->THIS. All three will typically be sequences of lines.""" def __init__(self, base, a, b): self.base = base self.a = a self.b = b from difflib import SequenceMatcher self.a_ops = SequenceMatcher(None, base, a).get_opcodes() self.b_ops = SequenceMatcher(None, base, b).get_opcodes() def merge(self): """Return sequences of matching and conflicting regions. Method is as follows: The two sequences align only on regions which match the base and both descendents. These are found by doing a two-way diff of each one against the base, and then finding the intersections between those regions. These "sync regions" are by definition unchanged in both and easily dealt with. The regions in between can be in any of three cases: conflicted, or changed on only one side. """ def find_sync_regions(self): """Return a list of sync regions, where both descendents match the base. Generates a list of ((base1, base2), (a1, a2), (b1, b2)). """ from difflib import SequenceMatcher aiter = iter(SequenceMatcher(None, self.base, self.a).get_matching_blocks()) biter = iter(SequenceMatcher(None, self.base, self.b).get_matching_blocks()) abase, amatch, alen = aiter.next() bbase, bmatch, blen = biter.next() while aiter and biter: # there is an unconflicted block at i; how long does it # extend? until whichever one ends earlier. i = intersect((abase, abase+alen), (bbase, bbase+blen)) if i: intbase = i[0] intend = i[1] intlen = intend - intbase # found a match of base[i[0], i[1]]; this may be less than # the region that matches in either one assert intlen <= alen assert intlen <= blen assert abase <= intbase assert bbase <= intbase asub = amatch + (intbase - abase) bsub = bmatch + (intbase - bbase) aend = asub + intlen bend = bsub + intlen assert self.base[intbase:intend] == self.a[asub:aend], \ (self.base[intbase:intend], self.a[asub:aend]) assert self.base[intbase:intend] == self.b[bsub:bend] yield ((intbase, intend), (asub, aend), (bsub, bend)) # advance whichever one ends first in the base text if (abase + alen) < (bbase + blen): abase, amatch, alen = aiter.next() else: bbase, bmatch, blen = biter.next() def find_unconflicted(self): """Return a list of ranges in base that are not conflicted.""" from difflib import SequenceMatcher am = SequenceMatcher(None, self.base, self.a).get_matching_blocks() bm = SequenceMatcher(None, self.base, self.b).get_matching_blocks() unc = [] while am and bm: # there is an unconflicted block at i; how long does it # extend? until whichever one ends earlier. a1 = am[0][0] a2 = a1 + am[0][2] b1 = bm[0][0] b2 = b1 + bm[0][2] i = intersect((a1, a2), (b1, b2)) if i: unc.append(i) if a2 < b2: del am[0] else: del bm[0] return unc commit refs/heads/master mark :824 committer Martin Pool 1120548742 +1000 data 71 - Merge3.find_sync_regions yields just a 6-tuple, not a tuple of tuples from :823 M 644 inline bzrlib/merge3.py data 5300 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # mbp: "you know that thing where cvs gives you conflict markers?" # s: "i hate that." def intersect(ra, rb): """Given two ranges return the range where they intersect or None. >>> intersect((0, 10), (0, 6)) (0, 6) >>> intersect((0, 10), (5, 15)) (5, 10) >>> intersect((0, 10), (10, 15)) >>> intersect((0, 9), (10, 15)) >>> intersect((0, 9), (7, 15)) (7, 9) """ assert ra[0] <= ra[1] assert rb[0] <= rb[1] sa = max(ra[0], rb[0]) sb = min(ra[1], rb[1]) if sa < sb: return sa, sb else: return None def threeway(baseline, aline, bline): if baseline == aline: return bline elif baseline == bline: return aline else: return [aline, bline] class Merge3(object): """3-way merge of texts. Given BASE, OTHER, THIS, tries to produce a combined text incorporating the changes from both BASE->OTHER and BASE->THIS. All three will typically be sequences of lines.""" def __init__(self, base, a, b): self.base = base self.a = a self.b = b from difflib import SequenceMatcher self.a_ops = SequenceMatcher(None, base, a).get_opcodes() self.b_ops = SequenceMatcher(None, base, b).get_opcodes() def merge(self): """Return sequences of matching and conflicting regions. Method is as follows: The two sequences align only on regions which match the base and both descendents. These are found by doing a two-way diff of each one against the base, and then finding the intersections between those regions. These "sync regions" are by definition unchanged in both and easily dealt with. The regions in between can be in any of three cases: conflicted, or changed on only one side. """ def find_sync_regions(self): """Return a list of sync regions, where both descendents match the base. Generates a list of (base1, base2, a1, a2, b1, b2). """ from difflib import SequenceMatcher aiter = iter(SequenceMatcher(None, self.base, self.a).get_matching_blocks()) biter = iter(SequenceMatcher(None, self.base, self.b).get_matching_blocks()) abase, amatch, alen = aiter.next() bbase, bmatch, blen = biter.next() while aiter and biter: # there is an unconflicted block at i; how long does it # extend? until whichever one ends earlier. i = intersect((abase, abase+alen), (bbase, bbase+blen)) if i: intbase = i[0] intend = i[1] intlen = intend - intbase # found a match of base[i[0], i[1]]; this may be less than # the region that matches in either one assert intlen <= alen assert intlen <= blen assert abase <= intbase assert bbase <= intbase asub = amatch + (intbase - abase) bsub = bmatch + (intbase - bbase) aend = asub + intlen bend = bsub + intlen assert self.base[intbase:intend] == self.a[asub:aend], \ (self.base[intbase:intend], self.a[asub:aend]) assert self.base[intbase:intend] == self.b[bsub:bend] yield (intbase, intend, asub, aend, bsub, bend) # advance whichever one ends first in the base text if (abase + alen) < (bbase + blen): abase, amatch, alen = aiter.next() else: bbase, bmatch, blen = biter.next() def find_unconflicted(self): """Return a list of ranges in base that are not conflicted.""" from difflib import SequenceMatcher am = SequenceMatcher(None, self.base, self.a).get_matching_blocks() bm = SequenceMatcher(None, self.base, self.b).get_matching_blocks() unc = [] while am and bm: # there is an unconflicted block at i; how long does it # extend? until whichever one ends earlier. a1 = am[0][0] a2 = a1 + am[0][2] b1 = bm[0][0] b2 = b1 + bm[0][2] i = intersect((a1, a2), (b1, b2)) if i: unc.append(i) if a2 < b2: del am[0] else: del bm[0] return unc M 644 inline bzrlib/selftest/testmerge3.py data 3267 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from bzrlib.selftest import InTempDir, TestBase from bzrlib.merge3 import Merge3 class NoChanges(TestBase): """No conflicts because nothing changed""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', 'bbb'], ['aaa', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0, 2, 0, 2, 0, 2)]) class NoConflicts(TestBase): """No conflicts because only one side changed""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (1, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0, 1, 0, 1, 0, 1), (1, 2, 2, 3, 1, 2)]) class InsertClash(TestBase): """Both try to insert lines in the same place.""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', '222', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (1, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0, 1, 0, 1, 0, 1), (1, 2, 2, 3, 2, 3)]) class ReplaceClash(TestBase): """Both try to insert lines in the same place.""" def runTest(self): m3 = Merge3(['aaa', '000', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', '222', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (2, 3)]) self.assertEquals(list(m3.find_sync_regions()), [(0, 1, 0, 1, 0, 1), (2, 3, 2, 3, 2, 3)]) class ReplaceMulti(TestBase): """Replacement with regions of different size.""" def runTest(self): m3 = Merge3(['aaa', '000', '000', 'bbb'], ['aaa', '111', '111', '111', 'bbb'], ['aaa', '222', '222', '222', '222', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (3, 4)]) self.assertEquals(list(m3.find_sync_regions()), [(0, 1, 0, 1, 0, 1), (3, 4, 4, 5, 5, 6)]) commit refs/heads/master mark :825 committer Martin Pool 1120550970 +1000 data 128 - Merge3.find_sync_regions always returns a zero-length sentinal at the end to ease matching. - Merge3.merge partially done. from :824 M 644 inline bzrlib/merge3.py data 6723 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # mbp: "you know that thing where cvs gives you conflict markers?" # s: "i hate that." def intersect(ra, rb): """Given two ranges return the range where they intersect or None. >>> intersect((0, 10), (0, 6)) (0, 6) >>> intersect((0, 10), (5, 15)) (5, 10) >>> intersect((0, 10), (10, 15)) >>> intersect((0, 9), (10, 15)) >>> intersect((0, 9), (7, 15)) (7, 9) """ assert ra[0] <= ra[1] assert rb[0] <= rb[1] sa = max(ra[0], rb[0]) sb = min(ra[1], rb[1]) if sa < sb: return sa, sb else: return None def threeway(baseline, aline, bline): if baseline == aline: return bline elif baseline == bline: return aline else: return [aline, bline] class Merge3(object): """3-way merge of texts. Given BASE, OTHER, THIS, tries to produce a combined text incorporating the changes from both BASE->OTHER and BASE->THIS. All three will typically be sequences of lines.""" def __init__(self, base, a, b): self.base = base self.a = a self.b = b from difflib import SequenceMatcher self.a_ops = SequenceMatcher(None, base, a).get_opcodes() self.b_ops = SequenceMatcher(None, base, b).get_opcodes() def merge_regions(self): """Return sequences of matching and conflicting regions. This returns tuples, where the first value says what kind we have: 'unchanged', start, end Take a region of base[start:end] 'a', start, end Non-clashing insertion from a[start:end] Method is as follows: The two sequences align only on regions which match the base and both descendents. These are found by doing a two-way diff of each one against the base, and then finding the intersections between those regions. These "sync regions" are by definition unchanged in both and easily dealt with. The regions in between can be in any of three cases: conflicted, or changed on only one side. """ # section a[0:ia] has been disposed of, etc iz = ia = ib = 0 for zmatch, zend, amatch, aend, bmatch, bend in self.find_sync_regions(): matchlen = zend - zmatch assert matchlen >= 0 assert matchlen == (aend - amatch) assert matchlen == (bend - bmatch) if amatch > ia: # or bmatch > ib: # got an unmatched region; work out if either # alternative is the same as the base # kludge: return the whole thing as inserted into A yield 'a', ia, amatch ia = amatch if matchlen > 0: assert ia == amatch assert ib == bmatch assert iz == zmatch yield 'unchanged', zmatch, zend iz = zend ia = aend ib = bend def find_sync_regions(self): """Return a list of sync regions, where both descendents match the base. Generates a list of (base1, base2, a1, a2, b1, b2). There is always a zero-length sync region at the end of all the files. """ from difflib import SequenceMatcher aiter = iter(SequenceMatcher(None, self.base, self.a).get_matching_blocks()) biter = iter(SequenceMatcher(None, self.base, self.b).get_matching_blocks()) abase, amatch, alen = aiter.next() bbase, bmatch, blen = biter.next() while aiter and biter: # there is an unconflicted block at i; how long does it # extend? until whichever one ends earlier. i = intersect((abase, abase+alen), (bbase, bbase+blen)) if i: intbase = i[0] intend = i[1] intlen = intend - intbase # found a match of base[i[0], i[1]]; this may be less than # the region that matches in either one assert intlen <= alen assert intlen <= blen assert abase <= intbase assert bbase <= intbase asub = amatch + (intbase - abase) bsub = bmatch + (intbase - bbase) aend = asub + intlen bend = bsub + intlen assert self.base[intbase:intend] == self.a[asub:aend], \ (self.base[intbase:intend], self.a[asub:aend]) assert self.base[intbase:intend] == self.b[bsub:bend] yield (intbase, intend, asub, aend, bsub, bend) # advance whichever one ends first in the base text if (abase + alen) < (bbase + blen): abase, amatch, alen = aiter.next() else: bbase, bmatch, blen = biter.next() intbase = len(self.base) abase = len(self.a) bbase = len(self.b) yield (intbase, intbase, abase, abase, bbase, bbase) def find_unconflicted(self): """Return a list of ranges in base that are not conflicted.""" from difflib import SequenceMatcher am = SequenceMatcher(None, self.base, self.a).get_matching_blocks() bm = SequenceMatcher(None, self.base, self.b).get_matching_blocks() unc = [] while am and bm: # there is an unconflicted block at i; how long does it # extend? until whichever one ends earlier. a1 = am[0][0] a2 = a1 + am[0][2] b1 = bm[0][0] b2 = b1 + bm[0][2] i = intersect((a1, a2), (b1, b2)) if i: unc.append(i) if a2 < b2: del am[0] else: del bm[0] return unc M 644 inline bzrlib/selftest/testmerge3.py data 4570 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from bzrlib.selftest import InTempDir, TestBase from bzrlib.merge3 import Merge3 class NoChanges(TestBase): """No conflicts because nothing changed""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', 'bbb'], ['aaa', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0, 2, 0, 2, 0, 2), (2,2, 2,2, 2,2)]) self.assertEquals(list(m3.merge_regions()), [('unchanged', 0, 2)]) class FrontInsert(TestBase): def runTest(self): m3 = Merge3(['zz'], ['aaa', 'bbb', 'zz'], ['zz']) # todo: should use a sentinal at end as from get_matching_blocks # to match without zz self.assertEquals(list(m3.find_sync_regions()), [(0,1, 2,3, 0,1), (1,1, 3,3, 1,1),]) self.assertEquals(list(m3.merge_regions()), [('a', 0, 2), ('unchanged', 0, 1)]) class NullInsert(TestBase): def runTest(self): m3 = Merge3([], ['aaa', 'bbb'], []) # todo: should use a sentinal at end as from get_matching_blocks # to match without zz self.assertEquals(list(m3.find_sync_regions()), [(0,0, 2,2, 0,0)]) self.assertEquals(list(m3.merge_regions()), [('a', 0, 2)]) class NoConflicts(TestBase): """No conflicts because only one side changed""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (1, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (1,2, 2,3, 1,2), (2,2, 3,3, 2,2),]) class InsertClash(TestBase): """Both try to insert lines in the same place.""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', '222', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (1, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (1,2, 2,3, 2,3), (2,2, 3,3, 3,3),]) class ReplaceClash(TestBase): """Both try to insert lines in the same place.""" def runTest(self): m3 = Merge3(['aaa', '000', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', '222', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (2, 3)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (2,3, 2,3, 2,3), (3,3, 3,3, 3,3),]) class ReplaceMulti(TestBase): """Replacement with regions of different size.""" def runTest(self): m3 = Merge3(['aaa', '000', '000', 'bbb'], ['aaa', '111', '111', '111', 'bbb'], ['aaa', '222', '222', '222', '222', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (3, 4)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (3,4, 4,5, 5,6), (4,4, 5,5, 6,6),]) commit refs/heads/master mark :826 committer Martin Pool 1120551641 +1000 data 48 - Actually merge unsynchronized regions. Woot! from :825 M 644 inline bzrlib/merge3.py data 7478 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # mbp: "you know that thing where cvs gives you conflict markers?" # s: "i hate that." def intersect(ra, rb): """Given two ranges return the range where they intersect or None. >>> intersect((0, 10), (0, 6)) (0, 6) >>> intersect((0, 10), (5, 15)) (5, 10) >>> intersect((0, 10), (10, 15)) >>> intersect((0, 9), (10, 15)) >>> intersect((0, 9), (7, 15)) (7, 9) """ assert ra[0] <= ra[1] assert rb[0] <= rb[1] sa = max(ra[0], rb[0]) sb = min(ra[1], rb[1]) if sa < sb: return sa, sb else: return None def threeway(baseline, aline, bline): if baseline == aline: return bline elif baseline == bline: return aline else: return [aline, bline] class Merge3(object): """3-way merge of texts. Given BASE, OTHER, THIS, tries to produce a combined text incorporating the changes from both BASE->OTHER and BASE->THIS. All three will typically be sequences of lines.""" def __init__(self, base, a, b): self.base = base self.a = a self.b = b from difflib import SequenceMatcher self.a_ops = SequenceMatcher(None, base, a).get_opcodes() self.b_ops = SequenceMatcher(None, base, b).get_opcodes() def merge_regions(self): """Return sequences of matching and conflicting regions. This returns tuples, where the first value says what kind we have: 'unchanged', start, end Take a region of base[start:end] 'a', start, end Non-clashing insertion from a[start:end] Method is as follows: The two sequences align only on regions which match the base and both descendents. These are found by doing a two-way diff of each one against the base, and then finding the intersections between those regions. These "sync regions" are by definition unchanged in both and easily dealt with. The regions in between can be in any of three cases: conflicted, or changed on only one side. """ # section a[0:ia] has been disposed of, etc iz = ia = ib = 0 for zmatch, zend, amatch, aend, bmatch, bend in self.find_sync_regions(): matchlen = zend - zmatch assert matchlen >= 0 assert matchlen == (aend - amatch) assert matchlen == (bend - bmatch) len_a = amatch - ia len_b = bmatch - ib len_base = zmatch - iz assert len_a >= 0 assert len_b >= 0 assert len_base >= 0 if len_a or len_b: lines_base = self.base[iz:zmatch] lines_a = self.a[ia:amatch] lines_b = self.b[ib:bmatch] # TODO: check the len just as a shortcut equal_a = (lines_a == lines_base) equal_b = (lines_b == lines_base) if equal_a and not equal_b: yield 'b', ib, bmatch elif equal_b and not equal_a: yield 'a', ia, amatch elif not equal_a and not equal_b: yield 'conflict', ia, amatch, ib, bmatch else: assert 0 ia = amatch ib = bmatch iz = zmatch # if the same part of the base was deleted on both sides # that's OK, we can just skip it. if matchlen > 0: assert ia == amatch assert ib == bmatch assert iz == zmatch yield 'unchanged', zmatch, zend iz = zend ia = aend ib = bend def find_sync_regions(self): """Return a list of sync regions, where both descendents match the base. Generates a list of (base1, base2, a1, a2, b1, b2). There is always a zero-length sync region at the end of all the files. """ from difflib import SequenceMatcher aiter = iter(SequenceMatcher(None, self.base, self.a).get_matching_blocks()) biter = iter(SequenceMatcher(None, self.base, self.b).get_matching_blocks()) abase, amatch, alen = aiter.next() bbase, bmatch, blen = biter.next() while aiter and biter: # there is an unconflicted block at i; how long does it # extend? until whichever one ends earlier. i = intersect((abase, abase+alen), (bbase, bbase+blen)) if i: intbase = i[0] intend = i[1] intlen = intend - intbase # found a match of base[i[0], i[1]]; this may be less than # the region that matches in either one assert intlen <= alen assert intlen <= blen assert abase <= intbase assert bbase <= intbase asub = amatch + (intbase - abase) bsub = bmatch + (intbase - bbase) aend = asub + intlen bend = bsub + intlen assert self.base[intbase:intend] == self.a[asub:aend], \ (self.base[intbase:intend], self.a[asub:aend]) assert self.base[intbase:intend] == self.b[bsub:bend] yield (intbase, intend, asub, aend, bsub, bend) # advance whichever one ends first in the base text if (abase + alen) < (bbase + blen): abase, amatch, alen = aiter.next() else: bbase, bmatch, blen = biter.next() intbase = len(self.base) abase = len(self.a) bbase = len(self.b) yield (intbase, intbase, abase, abase, bbase, bbase) def find_unconflicted(self): """Return a list of ranges in base that are not conflicted.""" from difflib import SequenceMatcher am = SequenceMatcher(None, self.base, self.a).get_matching_blocks() bm = SequenceMatcher(None, self.base, self.b).get_matching_blocks() unc = [] while am and bm: # there is an unconflicted block at i; how long does it # extend? until whichever one ends earlier. a1 = am[0][0] a2 = a1 + am[0][2] b1 = bm[0][0] b2 = b1 + bm[0][2] i = intersect((a1, a2), (b1, b2)) if i: unc.append(i) if a2 < b2: del am[0] else: del bm[0] return unc M 644 inline bzrlib/selftest/testmerge3.py data 4960 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from bzrlib.selftest import InTempDir, TestBase from bzrlib.merge3 import Merge3 class NoChanges(TestBase): """No conflicts because nothing changed""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', 'bbb'], ['aaa', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0, 2, 0, 2, 0, 2), (2,2, 2,2, 2,2)]) self.assertEquals(list(m3.merge_regions()), [('unchanged', 0, 2)]) class FrontInsert(TestBase): def runTest(self): m3 = Merge3(['zz'], ['aaa', 'bbb', 'zz'], ['zz']) # todo: should use a sentinal at end as from get_matching_blocks # to match without zz self.assertEquals(list(m3.find_sync_regions()), [(0,1, 2,3, 0,1), (1,1, 3,3, 1,1),]) self.assertEquals(list(m3.merge_regions()), [('a', 0, 2), ('unchanged', 0, 1)]) class NullInsert(TestBase): def runTest(self): m3 = Merge3([], ['aaa', 'bbb'], []) # todo: should use a sentinal at end as from get_matching_blocks # to match without zz self.assertEquals(list(m3.find_sync_regions()), [(0,0, 2,2, 0,0)]) self.assertEquals(list(m3.merge_regions()), [('a', 0, 2)]) class NoConflicts(TestBase): """No conflicts because only one side changed""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (1, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (1,2, 2,3, 1,2), (2,2, 3,3, 2,2),]) self.assertEquals(list(m3.merge_regions()), [('unchanged', 0, 1), ('a', 1, 2), ('unchanged', 1, 2),]) class InsertClash(TestBase): """Both try to insert lines in the same place.""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', '222', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (1, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (1,2, 2,3, 2,3), (2,2, 3,3, 3,3),]) self.assertEquals(list(m3.merge_regions()), [('unchanged', 0,1), ('conflict', 1,2, 1,2), ('unchanged', 1,2)]) class ReplaceClash(TestBase): """Both try to insert lines in the same place.""" def runTest(self): m3 = Merge3(['aaa', '000', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', '222', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (2, 3)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (2,3, 2,3, 2,3), (3,3, 3,3, 3,3),]) class ReplaceMulti(TestBase): """Replacement with regions of different size.""" def runTest(self): m3 = Merge3(['aaa', '000', '000', 'bbb'], ['aaa', '111', '111', '111', 'bbb'], ['aaa', '222', '222', '222', '222', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (3, 4)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (3,4, 4,5, 5,6), (4,4, 5,5, 6,6),]) commit refs/heads/master mark :827 committer Martin Pool 1120552208 +1000 data 54 - new Merge3.merge_groups feeds back the merged lines from :826 M 644 inline bzrlib/merge3.py data 8443 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # mbp: "you know that thing where cvs gives you conflict markers?" # s: "i hate that." def intersect(ra, rb): """Given two ranges return the range where they intersect or None. >>> intersect((0, 10), (0, 6)) (0, 6) >>> intersect((0, 10), (5, 15)) (5, 10) >>> intersect((0, 10), (10, 15)) >>> intersect((0, 9), (10, 15)) >>> intersect((0, 9), (7, 15)) (7, 9) """ assert ra[0] <= ra[1] assert rb[0] <= rb[1] sa = max(ra[0], rb[0]) sb = min(ra[1], rb[1]) if sa < sb: return sa, sb else: return None def threeway(baseline, aline, bline): if baseline == aline: return bline elif baseline == bline: return aline else: return [aline, bline] class Merge3(object): """3-way merge of texts. Given BASE, OTHER, THIS, tries to produce a combined text incorporating the changes from both BASE->OTHER and BASE->THIS. All three will typically be sequences of lines.""" def __init__(self, base, a, b): self.base = base self.a = a self.b = b from difflib import SequenceMatcher self.a_ops = SequenceMatcher(None, base, a).get_opcodes() self.b_ops = SequenceMatcher(None, base, b).get_opcodes() def merge_groups(self): """Yield sequence of line groups. Each one is a tuple: 'unchanged', lines Lines unchanged from base 'a', lines Lines taken from a 'b', lines Lines taken from b 'conflict', base_lines, a_lines, b_lines Lines from base were changed to either a or b and conflict. """ for t in self.merge_regions(): what = t[0] if what == 'unchanged': yield what, self.base[t[1]:t[2]] elif what == 'a': yield what, self.a[t[1]:t[2]] elif what == 'b': yield what, self.b[t[1]:t[2]] elif what == 'conflict': yield (what, self.base[t[1]:t[2]], self.a[t[3]:t[4]], self.b[t[5]:t[6]]) else: raise ValueError(what) def merge_regions(self): """Return sequences of matching and conflicting regions. This returns tuples, where the first value says what kind we have: 'unchanged', start, end Take a region of base[start:end] 'a', start, end Non-clashing insertion from a[start:end] Method is as follows: The two sequences align only on regions which match the base and both descendents. These are found by doing a two-way diff of each one against the base, and then finding the intersections between those regions. These "sync regions" are by definition unchanged in both and easily dealt with. The regions in between can be in any of three cases: conflicted, or changed on only one side. """ # section a[0:ia] has been disposed of, etc iz = ia = ib = 0 for zmatch, zend, amatch, aend, bmatch, bend in self.find_sync_regions(): matchlen = zend - zmatch assert matchlen >= 0 assert matchlen == (aend - amatch) assert matchlen == (bend - bmatch) len_a = amatch - ia len_b = bmatch - ib len_base = zmatch - iz assert len_a >= 0 assert len_b >= 0 assert len_base >= 0 if len_a or len_b: lines_base = self.base[iz:zmatch] lines_a = self.a[ia:amatch] lines_b = self.b[ib:bmatch] # TODO: check the len just as a shortcut equal_a = (lines_a == lines_base) equal_b = (lines_b == lines_base) if equal_a and not equal_b: yield 'b', ib, bmatch elif equal_b and not equal_a: yield 'a', ia, amatch elif not equal_a and not equal_b: yield 'conflict', iz, zmatch, ia, amatch, ib, bmatch else: assert 0 ia = amatch ib = bmatch iz = zmatch # if the same part of the base was deleted on both sides # that's OK, we can just skip it. if matchlen > 0: assert ia == amatch assert ib == bmatch assert iz == zmatch yield 'unchanged', zmatch, zend iz = zend ia = aend ib = bend def find_sync_regions(self): """Return a list of sync regions, where both descendents match the base. Generates a list of (base1, base2, a1, a2, b1, b2). There is always a zero-length sync region at the end of all the files. """ from difflib import SequenceMatcher aiter = iter(SequenceMatcher(None, self.base, self.a).get_matching_blocks()) biter = iter(SequenceMatcher(None, self.base, self.b).get_matching_blocks()) abase, amatch, alen = aiter.next() bbase, bmatch, blen = biter.next() while aiter and biter: # there is an unconflicted block at i; how long does it # extend? until whichever one ends earlier. i = intersect((abase, abase+alen), (bbase, bbase+blen)) if i: intbase = i[0] intend = i[1] intlen = intend - intbase # found a match of base[i[0], i[1]]; this may be less than # the region that matches in either one assert intlen <= alen assert intlen <= blen assert abase <= intbase assert bbase <= intbase asub = amatch + (intbase - abase) bsub = bmatch + (intbase - bbase) aend = asub + intlen bend = bsub + intlen assert self.base[intbase:intend] == self.a[asub:aend], \ (self.base[intbase:intend], self.a[asub:aend]) assert self.base[intbase:intend] == self.b[bsub:bend] yield (intbase, intend, asub, aend, bsub, bend) # advance whichever one ends first in the base text if (abase + alen) < (bbase + blen): abase, amatch, alen = aiter.next() else: bbase, bmatch, blen = biter.next() intbase = len(self.base) abase = len(self.a) bbase = len(self.b) yield (intbase, intbase, abase, abase, bbase, bbase) def find_unconflicted(self): """Return a list of ranges in base that are not conflicted.""" from difflib import SequenceMatcher am = SequenceMatcher(None, self.base, self.a).get_matching_blocks() bm = SequenceMatcher(None, self.base, self.b).get_matching_blocks() unc = [] while am and bm: # there is an unconflicted block at i; how long does it # extend? until whichever one ends earlier. a1 = am[0][0] a2 = a1 + am[0][2] b1 = bm[0][0] b2 = b1 + bm[0][2] i = intersect((a1, a2), (b1, b2)) if i: unc.append(i) if a2 < b2: del am[0] else: del bm[0] return unc M 644 inline bzrlib/selftest/testmerge3.py data 5476 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from bzrlib.selftest import InTempDir, TestBase from bzrlib.merge3 import Merge3 class NoChanges(TestBase): """No conflicts because nothing changed""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', 'bbb'], ['aaa', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0, 2, 0, 2, 0, 2), (2,2, 2,2, 2,2)]) self.assertEquals(list(m3.merge_regions()), [('unchanged', 0, 2)]) self.assertEquals(list(m3.merge_groups()), [('unchanged', ['aaa', 'bbb'])]) class FrontInsert(TestBase): def runTest(self): m3 = Merge3(['zz'], ['aaa', 'bbb', 'zz'], ['zz']) # todo: should use a sentinal at end as from get_matching_blocks # to match without zz self.assertEquals(list(m3.find_sync_regions()), [(0,1, 2,3, 0,1), (1,1, 3,3, 1,1),]) self.assertEquals(list(m3.merge_regions()), [('a', 0, 2), ('unchanged', 0, 1)]) self.assertEquals(list(m3.merge_groups()), [('a', ['aaa', 'bbb']), ('unchanged', ['zz'])]) class NullInsert(TestBase): def runTest(self): m3 = Merge3([], ['aaa', 'bbb'], []) # todo: should use a sentinal at end as from get_matching_blocks # to match without zz self.assertEquals(list(m3.find_sync_regions()), [(0,0, 2,2, 0,0)]) self.assertEquals(list(m3.merge_regions()), [('a', 0, 2)]) class NoConflicts(TestBase): """No conflicts because only one side changed""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (1, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (1,2, 2,3, 1,2), (2,2, 3,3, 2,2),]) self.assertEquals(list(m3.merge_regions()), [('unchanged', 0, 1), ('a', 1, 2), ('unchanged', 1, 2),]) class InsertClash(TestBase): """Both try to insert lines in the same place.""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', '222', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (1, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (1,2, 2,3, 2,3), (2,2, 3,3, 3,3),]) self.assertEquals(list(m3.merge_regions()), [('unchanged', 0,1), ('conflict', 1,1, 1,2, 1,2), ('unchanged', 1,2)]) self.assertEquals(list(m3.merge_groups()), [('unchanged', ['aaa']), ('conflict', [], ['111'], ['222']), ('unchanged', ['bbb']), ]) class ReplaceClash(TestBase): """Both try to insert lines in the same place.""" def runTest(self): m3 = Merge3(['aaa', '000', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', '222', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (2, 3)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (2,3, 2,3, 2,3), (3,3, 3,3, 3,3),]) class ReplaceMulti(TestBase): """Replacement with regions of different size.""" def runTest(self): m3 = Merge3(['aaa', '000', '000', 'bbb'], ['aaa', '111', '111', '111', 'bbb'], ['aaa', '222', '222', '222', '222', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (3, 4)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (3,4, 4,5, 5,6), (4,4, 5,5, 6,6),]) commit refs/heads/master mark :828 committer Martin Pool 1120552629 +1000 data 56 - code to represent merges in regular text conflict form from :827 M 644 inline bzrlib/merge3.py data 9439 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # mbp: "you know that thing where cvs gives you conflict markers?" # s: "i hate that." def intersect(ra, rb): """Given two ranges return the range where they intersect or None. >>> intersect((0, 10), (0, 6)) (0, 6) >>> intersect((0, 10), (5, 15)) (5, 10) >>> intersect((0, 10), (10, 15)) >>> intersect((0, 9), (10, 15)) >>> intersect((0, 9), (7, 15)) (7, 9) """ assert ra[0] <= ra[1] assert rb[0] <= rb[1] sa = max(ra[0], rb[0]) sb = min(ra[1], rb[1]) if sa < sb: return sa, sb else: return None def threeway(baseline, aline, bline): if baseline == aline: return bline elif baseline == bline: return aline else: return [aline, bline] class Merge3(object): """3-way merge of texts. Given BASE, OTHER, THIS, tries to produce a combined text incorporating the changes from both BASE->OTHER and BASE->THIS. All three will typically be sequences of lines.""" def __init__(self, base, a, b): self.base = base self.a = a self.b = b from difflib import SequenceMatcher self.a_ops = SequenceMatcher(None, base, a).get_opcodes() self.b_ops = SequenceMatcher(None, base, b).get_opcodes() def merge_lines(self, start_marker='<<<<<<<<\n', mid_marker='========\n', end_marker='>>>>>>>>\n'): """Return merge in cvs-like form. """ for t in self.merge_regions(): what = t[0] if what == 'unchanged': for i in range(t[1], t[2]): yield self.base[i] elif what == 'a': for i in range(t[1], t[2]): yield self.a[i] elif what == 'b': for i in range(t[1], t[2]): yield self.b[i] elif what == 'conflict': yield start_marker for i in range(t[3], t[4]): yield self.a[i] yield mid_marker for i in range(t[5], t[6]): yield self.b[i] yield end_marker else: raise ValueError(what) def merge_groups(self): """Yield sequence of line groups. Each one is a tuple: 'unchanged', lines Lines unchanged from base 'a', lines Lines taken from a 'b', lines Lines taken from b 'conflict', base_lines, a_lines, b_lines Lines from base were changed to either a or b and conflict. """ for t in self.merge_regions(): what = t[0] if what == 'unchanged': yield what, self.base[t[1]:t[2]] elif what == 'a': yield what, self.a[t[1]:t[2]] elif what == 'b': yield what, self.b[t[1]:t[2]] elif what == 'conflict': yield (what, self.base[t[1]:t[2]], self.a[t[3]:t[4]], self.b[t[5]:t[6]]) else: raise ValueError(what) def merge_regions(self): """Return sequences of matching and conflicting regions. This returns tuples, where the first value says what kind we have: 'unchanged', start, end Take a region of base[start:end] 'a', start, end Non-clashing insertion from a[start:end] Method is as follows: The two sequences align only on regions which match the base and both descendents. These are found by doing a two-way diff of each one against the base, and then finding the intersections between those regions. These "sync regions" are by definition unchanged in both and easily dealt with. The regions in between can be in any of three cases: conflicted, or changed on only one side. """ # section a[0:ia] has been disposed of, etc iz = ia = ib = 0 for zmatch, zend, amatch, aend, bmatch, bend in self.find_sync_regions(): matchlen = zend - zmatch assert matchlen >= 0 assert matchlen == (aend - amatch) assert matchlen == (bend - bmatch) len_a = amatch - ia len_b = bmatch - ib len_base = zmatch - iz assert len_a >= 0 assert len_b >= 0 assert len_base >= 0 if len_a or len_b: lines_base = self.base[iz:zmatch] lines_a = self.a[ia:amatch] lines_b = self.b[ib:bmatch] # TODO: check the len just as a shortcut equal_a = (lines_a == lines_base) equal_b = (lines_b == lines_base) if equal_a and not equal_b: yield 'b', ib, bmatch elif equal_b and not equal_a: yield 'a', ia, amatch elif not equal_a and not equal_b: yield 'conflict', iz, zmatch, ia, amatch, ib, bmatch else: assert 0 ia = amatch ib = bmatch iz = zmatch # if the same part of the base was deleted on both sides # that's OK, we can just skip it. if matchlen > 0: assert ia == amatch assert ib == bmatch assert iz == zmatch yield 'unchanged', zmatch, zend iz = zend ia = aend ib = bend def find_sync_regions(self): """Return a list of sync regions, where both descendents match the base. Generates a list of (base1, base2, a1, a2, b1, b2). There is always a zero-length sync region at the end of all the files. """ from difflib import SequenceMatcher aiter = iter(SequenceMatcher(None, self.base, self.a).get_matching_blocks()) biter = iter(SequenceMatcher(None, self.base, self.b).get_matching_blocks()) abase, amatch, alen = aiter.next() bbase, bmatch, blen = biter.next() while aiter and biter: # there is an unconflicted block at i; how long does it # extend? until whichever one ends earlier. i = intersect((abase, abase+alen), (bbase, bbase+blen)) if i: intbase = i[0] intend = i[1] intlen = intend - intbase # found a match of base[i[0], i[1]]; this may be less than # the region that matches in either one assert intlen <= alen assert intlen <= blen assert abase <= intbase assert bbase <= intbase asub = amatch + (intbase - abase) bsub = bmatch + (intbase - bbase) aend = asub + intlen bend = bsub + intlen assert self.base[intbase:intend] == self.a[asub:aend], \ (self.base[intbase:intend], self.a[asub:aend]) assert self.base[intbase:intend] == self.b[bsub:bend] yield (intbase, intend, asub, aend, bsub, bend) # advance whichever one ends first in the base text if (abase + alen) < (bbase + blen): abase, amatch, alen = aiter.next() else: bbase, bmatch, blen = biter.next() intbase = len(self.base) abase = len(self.a) bbase = len(self.b) yield (intbase, intbase, abase, abase, bbase, bbase) def find_unconflicted(self): """Return a list of ranges in base that are not conflicted.""" from difflib import SequenceMatcher am = SequenceMatcher(None, self.base, self.a).get_matching_blocks() bm = SequenceMatcher(None, self.base, self.b).get_matching_blocks() unc = [] while am and bm: # there is an unconflicted block at i; how long does it # extend? until whichever one ends earlier. a1 = am[0][0] a2 = a1 + am[0][2] b1 = bm[0][0] b2 = b1 + bm[0][2] i = intersect((a1, a2), (b1, b2)) if i: unc.append(i) if a2 < b2: del am[0] else: del bm[0] return unc M 644 inline bzrlib/selftest/testmerge3.py data 5872 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from bzrlib.selftest import InTempDir, TestBase from bzrlib.merge3 import Merge3 class NoChanges(TestBase): """No conflicts because nothing changed""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', 'bbb'], ['aaa', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0, 2, 0, 2, 0, 2), (2,2, 2,2, 2,2)]) self.assertEquals(list(m3.merge_regions()), [('unchanged', 0, 2)]) self.assertEquals(list(m3.merge_groups()), [('unchanged', ['aaa', 'bbb'])]) class FrontInsert(TestBase): def runTest(self): m3 = Merge3(['zz'], ['aaa', 'bbb', 'zz'], ['zz']) # todo: should use a sentinal at end as from get_matching_blocks # to match without zz self.assertEquals(list(m3.find_sync_regions()), [(0,1, 2,3, 0,1), (1,1, 3,3, 1,1),]) self.assertEquals(list(m3.merge_regions()), [('a', 0, 2), ('unchanged', 0, 1)]) self.assertEquals(list(m3.merge_groups()), [('a', ['aaa', 'bbb']), ('unchanged', ['zz'])]) class NullInsert(TestBase): def runTest(self): m3 = Merge3([], ['aaa', 'bbb'], []) # todo: should use a sentinal at end as from get_matching_blocks # to match without zz self.assertEquals(list(m3.find_sync_regions()), [(0,0, 2,2, 0,0)]) self.assertEquals(list(m3.merge_regions()), [('a', 0, 2)]) self.assertEquals(list(m3.merge_lines()), ['aaa', 'bbb']) class NoConflicts(TestBase): """No conflicts because only one side changed""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (1, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (1,2, 2,3, 1,2), (2,2, 3,3, 2,2),]) self.assertEquals(list(m3.merge_regions()), [('unchanged', 0, 1), ('a', 1, 2), ('unchanged', 1, 2),]) class InsertClash(TestBase): """Both try to insert lines in the same place.""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', '222', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (1, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (1,2, 2,3, 2,3), (2,2, 3,3, 3,3),]) self.assertEquals(list(m3.merge_regions()), [('unchanged', 0,1), ('conflict', 1,1, 1,2, 1,2), ('unchanged', 1,2)]) self.assertEquals(list(m3.merge_groups()), [('unchanged', ['aaa']), ('conflict', [], ['111'], ['222']), ('unchanged', ['bbb']), ]) self.assertEquals(list(m3.merge_lines('<<', '--', '>>')), ['aaa', '<<', '111', '--', '222', '>>', 'bbb']) class ReplaceClash(TestBase): """Both try to insert lines in the same place.""" def runTest(self): m3 = Merge3(['aaa', '000', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', '222', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (2, 3)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (2,3, 2,3, 2,3), (3,3, 3,3, 3,3),]) class ReplaceMulti(TestBase): """Replacement with regions of different size.""" def runTest(self): m3 = Merge3(['aaa', '000', '000', 'bbb'], ['aaa', '111', '111', '111', 'bbb'], ['aaa', '222', '222', '222', '222', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (3, 4)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (3,4, 4,5, 5,6), (4,4, 5,5, 6,6),]) commit refs/heads/master mark :829 committer Martin Pool 1120553282 +1000 data 29 - More merge3 cvs-form stuff from :828 M 644 inline bzrlib/merge3.py data 10018 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # mbp: "you know that thing where cvs gives you conflict markers?" # s: "i hate that." def intersect(ra, rb): """Given two ranges return the range where they intersect or None. >>> intersect((0, 10), (0, 6)) (0, 6) >>> intersect((0, 10), (5, 15)) (5, 10) >>> intersect((0, 10), (10, 15)) >>> intersect((0, 9), (10, 15)) >>> intersect((0, 9), (7, 15)) (7, 9) """ assert ra[0] <= ra[1] assert rb[0] <= rb[1] sa = max(ra[0], rb[0]) sb = min(ra[1], rb[1]) if sa < sb: return sa, sb else: return None def threeway(baseline, aline, bline): if baseline == aline: return bline elif baseline == bline: return aline else: return [aline, bline] class Merge3(object): """3-way merge of texts. Given BASE, OTHER, THIS, tries to produce a combined text incorporating the changes from both BASE->OTHER and BASE->THIS. All three will typically be sequences of lines.""" def __init__(self, base, a, b): self.base = base self.a = a self.b = b from difflib import SequenceMatcher self.a_ops = SequenceMatcher(None, base, a).get_opcodes() self.b_ops = SequenceMatcher(None, base, b).get_opcodes() def merge_lines(self, name_a=None, name_b=None, start_marker='<<<<<<<<', mid_marker='========', end_marker='>>>>>>>>', show_base=False): """Return merge in cvs-like form. """ if name_a: start_marker = start_marker + ' ' + name_a if name_b: end_marker = end_marker + ' ' + name_b for t in self.merge_regions(): what = t[0] if what == 'unchanged': for i in range(t[1], t[2]): yield self.base[i] elif what == 'a': for i in range(t[1], t[2]): yield self.a[i] elif what == 'b': for i in range(t[1], t[2]): yield self.b[i] elif what == 'conflict': yield start_marker + '\n' for i in range(t[3], t[4]): yield self.a[i] yield mid_marker + '\n' for i in range(t[5], t[6]): yield self.b[i] yield end_marker + '\n' else: raise ValueError(what) def merge_groups(self): """Yield sequence of line groups. Each one is a tuple: 'unchanged', lines Lines unchanged from base 'a', lines Lines taken from a 'b', lines Lines taken from b 'conflict', base_lines, a_lines, b_lines Lines from base were changed to either a or b and conflict. """ for t in self.merge_regions(): what = t[0] if what == 'unchanged': yield what, self.base[t[1]:t[2]] elif what == 'a': yield what, self.a[t[1]:t[2]] elif what == 'b': yield what, self.b[t[1]:t[2]] elif what == 'conflict': yield (what, self.base[t[1]:t[2]], self.a[t[3]:t[4]], self.b[t[5]:t[6]]) else: raise ValueError(what) def merge_regions(self): """Return sequences of matching and conflicting regions. This returns tuples, where the first value says what kind we have: 'unchanged', start, end Take a region of base[start:end] 'a', start, end Non-clashing insertion from a[start:end] Method is as follows: The two sequences align only on regions which match the base and both descendents. These are found by doing a two-way diff of each one against the base, and then finding the intersections between those regions. These "sync regions" are by definition unchanged in both and easily dealt with. The regions in between can be in any of three cases: conflicted, or changed on only one side. """ # section a[0:ia] has been disposed of, etc iz = ia = ib = 0 for zmatch, zend, amatch, aend, bmatch, bend in self.find_sync_regions(): matchlen = zend - zmatch assert matchlen >= 0 assert matchlen == (aend - amatch) assert matchlen == (bend - bmatch) len_a = amatch - ia len_b = bmatch - ib len_base = zmatch - iz assert len_a >= 0 assert len_b >= 0 assert len_base >= 0 if len_a or len_b: lines_base = self.base[iz:zmatch] lines_a = self.a[ia:amatch] lines_b = self.b[ib:bmatch] # TODO: check the len just as a shortcut equal_a = (lines_a == lines_base) equal_b = (lines_b == lines_base) if equal_a and not equal_b: yield 'b', ib, bmatch elif equal_b and not equal_a: yield 'a', ia, amatch elif not equal_a and not equal_b: yield 'conflict', iz, zmatch, ia, amatch, ib, bmatch else: assert 0 ia = amatch ib = bmatch iz = zmatch # if the same part of the base was deleted on both sides # that's OK, we can just skip it. if matchlen > 0: assert ia == amatch assert ib == bmatch assert iz == zmatch yield 'unchanged', zmatch, zend iz = zend ia = aend ib = bend def find_sync_regions(self): """Return a list of sync regions, where both descendents match the base. Generates a list of (base1, base2, a1, a2, b1, b2). There is always a zero-length sync region at the end of all the files. """ from difflib import SequenceMatcher aiter = iter(SequenceMatcher(None, self.base, self.a).get_matching_blocks()) biter = iter(SequenceMatcher(None, self.base, self.b).get_matching_blocks()) abase, amatch, alen = aiter.next() bbase, bmatch, blen = biter.next() while aiter and biter: # there is an unconflicted block at i; how long does it # extend? until whichever one ends earlier. i = intersect((abase, abase+alen), (bbase, bbase+blen)) if i: intbase = i[0] intend = i[1] intlen = intend - intbase # found a match of base[i[0], i[1]]; this may be less than # the region that matches in either one assert intlen <= alen assert intlen <= blen assert abase <= intbase assert bbase <= intbase asub = amatch + (intbase - abase) bsub = bmatch + (intbase - bbase) aend = asub + intlen bend = bsub + intlen assert self.base[intbase:intend] == self.a[asub:aend], \ (self.base[intbase:intend], self.a[asub:aend]) assert self.base[intbase:intend] == self.b[bsub:bend] yield (intbase, intend, asub, aend, bsub, bend) # advance whichever one ends first in the base text if (abase + alen) < (bbase + blen): abase, amatch, alen = aiter.next() else: bbase, bmatch, blen = biter.next() intbase = len(self.base) abase = len(self.a) bbase = len(self.b) yield (intbase, intbase, abase, abase, bbase, bbase) def find_unconflicted(self): """Return a list of ranges in base that are not conflicted.""" from difflib import SequenceMatcher am = SequenceMatcher(None, self.base, self.a).get_matching_blocks() bm = SequenceMatcher(None, self.base, self.b).get_matching_blocks() unc = [] while am and bm: # there is an unconflicted block at i; how long does it # extend? until whichever one ends earlier. a1 = am[0][0] a2 = a1 + am[0][2] b1 = bm[0][0] b2 = b1 + bm[0][2] i = intersect((a1, a2), (b1, b2)) if i: unc.append(i) if a2 < b2: del am[0] else: del bm[0] return unc def main(argv): base = file(argv[1], 'rt').readlines() a = file(argv[2], 'rt').readlines() b = file(argv[3], 'rt').readlines() m3 = Merge3(base, a, b) sys.stdout.writelines(m3.merge_lines(argv[2], argv[3])) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) M 644 inline bzrlib/selftest/testmerge3.py data 5887 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from bzrlib.selftest import InTempDir, TestBase from bzrlib.merge3 import Merge3 class NoChanges(TestBase): """No conflicts because nothing changed""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', 'bbb'], ['aaa', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0, 2, 0, 2, 0, 2), (2,2, 2,2, 2,2)]) self.assertEquals(list(m3.merge_regions()), [('unchanged', 0, 2)]) self.assertEquals(list(m3.merge_groups()), [('unchanged', ['aaa', 'bbb'])]) class FrontInsert(TestBase): def runTest(self): m3 = Merge3(['zz'], ['aaa', 'bbb', 'zz'], ['zz']) # todo: should use a sentinal at end as from get_matching_blocks # to match without zz self.assertEquals(list(m3.find_sync_regions()), [(0,1, 2,3, 0,1), (1,1, 3,3, 1,1),]) self.assertEquals(list(m3.merge_regions()), [('a', 0, 2), ('unchanged', 0, 1)]) self.assertEquals(list(m3.merge_groups()), [('a', ['aaa', 'bbb']), ('unchanged', ['zz'])]) class NullInsert(TestBase): def runTest(self): m3 = Merge3([], ['aaa', 'bbb'], []) # todo: should use a sentinal at end as from get_matching_blocks # to match without zz self.assertEquals(list(m3.find_sync_regions()), [(0,0, 2,2, 0,0)]) self.assertEquals(list(m3.merge_regions()), [('a', 0, 2)]) self.assertEquals(list(m3.merge_lines()), ['aaa', 'bbb']) class NoConflicts(TestBase): """No conflicts because only one side changed""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (1, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (1,2, 2,3, 1,2), (2,2, 3,3, 2,2),]) self.assertEquals(list(m3.merge_regions()), [('unchanged', 0, 1), ('a', 1, 2), ('unchanged', 1, 2),]) class InsertClash(TestBase): """Both try to insert lines in the same place.""" def runTest(self): m3 = Merge3(['aaa\n', 'bbb\n'], ['aaa\n', '111\n', 'bbb\n'], ['aaa\n', '222\n', 'bbb\n']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (1, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (1,2, 2,3, 2,3), (2,2, 3,3, 3,3),]) self.assertEquals(list(m3.merge_regions()), [('unchanged', 0,1), ('conflict', 1,1, 1,2, 1,2), ('unchanged', 1,2)]) self.assertEquals(list(m3.merge_groups()), [('unchanged', ['aaa\n']), ('conflict', [], ['111\n'], ['222\n']), ('unchanged', ['bbb\n']), ]) ml = m3.merge_lines(name_a='a', name_b='b', start_marker='<<', mid_marker='--', end_marker='>>') self.assertEquals(''.join(ml), '''aaa << a 111 -- 222 >> b bbb ''') class ReplaceClash(TestBase): """Both try to insert lines in the same place.""" def runTest(self): m3 = Merge3(['aaa', '000', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', '222', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (2, 3)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (2,3, 2,3, 2,3), (3,3, 3,3, 3,3),]) class ReplaceMulti(TestBase): """Replacement with regions of different size.""" def runTest(self): m3 = Merge3(['aaa', '000', '000', 'bbb'], ['aaa', '111', '111', '111', 'bbb'], ['aaa', '222', '222', '222', '222', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (3, 4)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (3,4, 4,5, 5,6), (4,4, 5,5, 6,6),]) commit refs/heads/master mark :830 committer Martin Pool 1120554264 +1000 data 52 - handle chunks which differ from the base but agree from :829 M 644 inline bzrlib/merge3.py data 10414 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # mbp: "you know that thing where cvs gives you conflict markers?" # s: "i hate that." def intersect(ra, rb): """Given two ranges return the range where they intersect or None. >>> intersect((0, 10), (0, 6)) (0, 6) >>> intersect((0, 10), (5, 15)) (5, 10) >>> intersect((0, 10), (10, 15)) >>> intersect((0, 9), (10, 15)) >>> intersect((0, 9), (7, 15)) (7, 9) """ assert ra[0] <= ra[1] assert rb[0] <= rb[1] sa = max(ra[0], rb[0]) sb = min(ra[1], rb[1]) if sa < sb: return sa, sb else: return None def threeway(baseline, aline, bline): if baseline == aline: return bline elif baseline == bline: return aline else: return [aline, bline] class Merge3(object): """3-way merge of texts. Given BASE, OTHER, THIS, tries to produce a combined text incorporating the changes from both BASE->OTHER and BASE->THIS. All three will typically be sequences of lines.""" def __init__(self, base, a, b): self.base = base self.a = a self.b = b from difflib import SequenceMatcher self.a_ops = SequenceMatcher(None, base, a).get_opcodes() self.b_ops = SequenceMatcher(None, base, b).get_opcodes() def merge_lines(self, name_a=None, name_b=None, start_marker='<<<<<<<<', mid_marker='========', end_marker='>>>>>>>>', show_base=False): """Return merge in cvs-like form. """ if name_a: start_marker = start_marker + ' ' + name_a if name_b: end_marker = end_marker + ' ' + name_b for t in self.merge_regions(): what = t[0] if what == 'unchanged': for i in range(t[1], t[2]): yield self.base[i] elif what == 'a' or what == 'same': for i in range(t[1], t[2]): yield self.a[i] elif what == 'b': for i in range(t[1], t[2]): yield self.b[i] elif what == 'conflict': yield start_marker + '\n' for i in range(t[3], t[4]): yield self.a[i] yield mid_marker + '\n' for i in range(t[5], t[6]): yield self.b[i] yield end_marker + '\n' else: raise ValueError(what) def merge_groups(self): """Yield sequence of line groups. Each one is a tuple: 'unchanged', lines Lines unchanged from base 'a', lines Lines taken from a 'same', lines Lines taken from a (and equal to b) 'b', lines Lines taken from b 'conflict', base_lines, a_lines, b_lines Lines from base were changed to either a or b and conflict. """ for t in self.merge_regions(): what = t[0] if what == 'unchanged': yield what, self.base[t[1]:t[2]] elif what == 'a' or what == 'same': yield what, self.a[t[1]:t[2]] elif what == 'b': yield what, self.b[t[1]:t[2]] elif what == 'conflict': yield (what, self.base[t[1]:t[2]], self.a[t[3]:t[4]], self.b[t[5]:t[6]]) else: raise ValueError(what) def merge_regions(self): """Return sequences of matching and conflicting regions. This returns tuples, where the first value says what kind we have: 'unchanged', start, end Take a region of base[start:end] 'same', astart, aend b and a are different from base but give the same result 'a', start, end Non-clashing insertion from a[start:end] Method is as follows: The two sequences align only on regions which match the base and both descendents. These are found by doing a two-way diff of each one against the base, and then finding the intersections between those regions. These "sync regions" are by definition unchanged in both and easily dealt with. The regions in between can be in any of three cases: conflicted, or changed on only one side. """ # section a[0:ia] has been disposed of, etc iz = ia = ib = 0 for zmatch, zend, amatch, aend, bmatch, bend in self.find_sync_regions(): matchlen = zend - zmatch assert matchlen >= 0 assert matchlen == (aend - amatch) assert matchlen == (bend - bmatch) len_a = amatch - ia len_b = bmatch - ib len_base = zmatch - iz assert len_a >= 0 assert len_b >= 0 assert len_base >= 0 if len_a or len_b: lines_base = self.base[iz:zmatch] lines_a = self.a[ia:amatch] lines_b = self.b[ib:bmatch] # TODO: check the len just as a shortcut equal_a = (lines_a == lines_base) equal_b = (lines_b == lines_base) same = lines_a == lines_b if same: yield 'same', ia, amatch elif equal_a and not equal_b: yield 'b', ib, bmatch elif equal_b and not equal_a: yield 'a', ia, amatch elif not equal_a and not equal_b: yield 'conflict', iz, zmatch, ia, amatch, ib, bmatch else: assert 0 ia = amatch ib = bmatch iz = zmatch # if the same part of the base was deleted on both sides # that's OK, we can just skip it. if matchlen > 0: assert ia == amatch assert ib == bmatch assert iz == zmatch yield 'unchanged', zmatch, zend iz = zend ia = aend ib = bend def find_sync_regions(self): """Return a list of sync regions, where both descendents match the base. Generates a list of (base1, base2, a1, a2, b1, b2). There is always a zero-length sync region at the end of all the files. """ from difflib import SequenceMatcher aiter = iter(SequenceMatcher(None, self.base, self.a).get_matching_blocks()) biter = iter(SequenceMatcher(None, self.base, self.b).get_matching_blocks()) abase, amatch, alen = aiter.next() bbase, bmatch, blen = biter.next() while aiter and biter: # there is an unconflicted block at i; how long does it # extend? until whichever one ends earlier. i = intersect((abase, abase+alen), (bbase, bbase+blen)) if i: intbase = i[0] intend = i[1] intlen = intend - intbase # found a match of base[i[0], i[1]]; this may be less than # the region that matches in either one assert intlen <= alen assert intlen <= blen assert abase <= intbase assert bbase <= intbase asub = amatch + (intbase - abase) bsub = bmatch + (intbase - bbase) aend = asub + intlen bend = bsub + intlen assert self.base[intbase:intend] == self.a[asub:aend], \ (self.base[intbase:intend], self.a[asub:aend]) assert self.base[intbase:intend] == self.b[bsub:bend] yield (intbase, intend, asub, aend, bsub, bend) # advance whichever one ends first in the base text if (abase + alen) < (bbase + blen): abase, amatch, alen = aiter.next() else: bbase, bmatch, blen = biter.next() intbase = len(self.base) abase = len(self.a) bbase = len(self.b) yield (intbase, intbase, abase, abase, bbase, bbase) def find_unconflicted(self): """Return a list of ranges in base that are not conflicted.""" from difflib import SequenceMatcher am = SequenceMatcher(None, self.base, self.a).get_matching_blocks() bm = SequenceMatcher(None, self.base, self.b).get_matching_blocks() unc = [] while am and bm: # there is an unconflicted block at i; how long does it # extend? until whichever one ends earlier. a1 = am[0][0] a2 = a1 + am[0][2] b1 = bm[0][0] b2 = b1 + bm[0][2] i = intersect((a1, a2), (b1, b2)) if i: unc.append(i) if a2 < b2: del am[0] else: del bm[0] return unc def main(argv): # as for diff3 and meld the syntax is "MINE BASE OTHER" a = file(argv[1], 'rt').readlines() base = file(argv[2], 'rt').readlines() b = file(argv[3], 'rt').readlines() m3 = Merge3(base, a, b) sys.stdout.writelines(m3.merge_lines(name_a=argv[1], name_b=argv[3])) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) M 644 inline bzrlib/selftest/testmerge3.py data 6182 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from bzrlib.selftest import InTempDir, TestBase from bzrlib.merge3 import Merge3 class NoChanges(TestBase): """No conflicts because nothing changed""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', 'bbb'], ['aaa', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0, 2, 0, 2, 0, 2), (2,2, 2,2, 2,2)]) self.assertEquals(list(m3.merge_regions()), [('unchanged', 0, 2)]) self.assertEquals(list(m3.merge_groups()), [('unchanged', ['aaa', 'bbb'])]) class FrontInsert(TestBase): def runTest(self): m3 = Merge3(['zz'], ['aaa', 'bbb', 'zz'], ['zz']) # todo: should use a sentinal at end as from get_matching_blocks # to match without zz self.assertEquals(list(m3.find_sync_regions()), [(0,1, 2,3, 0,1), (1,1, 3,3, 1,1),]) self.assertEquals(list(m3.merge_regions()), [('a', 0, 2), ('unchanged', 0, 1)]) self.assertEquals(list(m3.merge_groups()), [('a', ['aaa', 'bbb']), ('unchanged', ['zz'])]) class NullInsert(TestBase): def runTest(self): m3 = Merge3([], ['aaa', 'bbb'], []) # todo: should use a sentinal at end as from get_matching_blocks # to match without zz self.assertEquals(list(m3.find_sync_regions()), [(0,0, 2,2, 0,0)]) self.assertEquals(list(m3.merge_regions()), [('a', 0, 2)]) self.assertEquals(list(m3.merge_lines()), ['aaa', 'bbb']) class NoConflicts(TestBase): """No conflicts because only one side changed""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (1, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (1,2, 2,3, 1,2), (2,2, 3,3, 2,2),]) self.assertEquals(list(m3.merge_regions()), [('unchanged', 0, 1), ('a', 1, 2), ('unchanged', 1, 2),]) class InsertAgreement(TestBase): def runTest(self): m3 = Merge3(['aaa\n', 'bbb\n'], ['aaa\n', '222\n', 'bbb\n'], ['aaa\n', '222\n', 'bbb\n']) self.assertEquals(''.join(m3.merge_lines()), 'aaa\n222\nbbb\n') class InsertClash(TestBase): """Both try to insert lines in the same place.""" def runTest(self): m3 = Merge3(['aaa\n', 'bbb\n'], ['aaa\n', '111\n', 'bbb\n'], ['aaa\n', '222\n', 'bbb\n']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (1, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (1,2, 2,3, 2,3), (2,2, 3,3, 3,3),]) self.assertEquals(list(m3.merge_regions()), [('unchanged', 0,1), ('conflict', 1,1, 1,2, 1,2), ('unchanged', 1,2)]) self.assertEquals(list(m3.merge_groups()), [('unchanged', ['aaa\n']), ('conflict', [], ['111\n'], ['222\n']), ('unchanged', ['bbb\n']), ]) ml = m3.merge_lines(name_a='a', name_b='b', start_marker='<<', mid_marker='--', end_marker='>>') self.assertEquals(''.join(ml), '''aaa << a 111 -- 222 >> b bbb ''') class ReplaceClash(TestBase): """Both try to insert lines in the same place.""" def runTest(self): m3 = Merge3(['aaa', '000', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', '222', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (2, 3)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (2,3, 2,3, 2,3), (3,3, 3,3, 3,3),]) class ReplaceMulti(TestBase): """Replacement with regions of different size.""" def runTest(self): m3 = Merge3(['aaa', '000', '000', 'bbb'], ['aaa', '111', '111', '111', 'bbb'], ['aaa', '222', '222', '222', '222', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (3, 4)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (3,4, 4,5, 5,6), (4,4, 5,5, 6,6),]) commit refs/heads/master mark :831 committer Martin Pool 1120554789 +1000 data 36 - add merge3 test case from Lao-Tzu from :830 M 644 inline bzrlib/selftest/testmerge3.py data 8696 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from bzrlib.selftest import InTempDir, TestBase from bzrlib.merge3 import Merge3 class NoChanges(TestBase): """No conflicts because nothing changed""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', 'bbb'], ['aaa', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0, 2, 0, 2, 0, 2), (2,2, 2,2, 2,2)]) self.assertEquals(list(m3.merge_regions()), [('unchanged', 0, 2)]) self.assertEquals(list(m3.merge_groups()), [('unchanged', ['aaa', 'bbb'])]) class FrontInsert(TestBase): def runTest(self): m3 = Merge3(['zz'], ['aaa', 'bbb', 'zz'], ['zz']) # todo: should use a sentinal at end as from get_matching_blocks # to match without zz self.assertEquals(list(m3.find_sync_regions()), [(0,1, 2,3, 0,1), (1,1, 3,3, 1,1),]) self.assertEquals(list(m3.merge_regions()), [('a', 0, 2), ('unchanged', 0, 1)]) self.assertEquals(list(m3.merge_groups()), [('a', ['aaa', 'bbb']), ('unchanged', ['zz'])]) class NullInsert(TestBase): def runTest(self): m3 = Merge3([], ['aaa', 'bbb'], []) # todo: should use a sentinal at end as from get_matching_blocks # to match without zz self.assertEquals(list(m3.find_sync_regions()), [(0,0, 2,2, 0,0)]) self.assertEquals(list(m3.merge_regions()), [('a', 0, 2)]) self.assertEquals(list(m3.merge_lines()), ['aaa', 'bbb']) class NoConflicts(TestBase): """No conflicts because only one side changed""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (1, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (1,2, 2,3, 1,2), (2,2, 3,3, 2,2),]) self.assertEquals(list(m3.merge_regions()), [('unchanged', 0, 1), ('a', 1, 2), ('unchanged', 1, 2),]) class InsertAgreement(TestBase): def runTest(self): m3 = Merge3(['aaa\n', 'bbb\n'], ['aaa\n', '222\n', 'bbb\n'], ['aaa\n', '222\n', 'bbb\n']) self.assertEquals(''.join(m3.merge_lines()), 'aaa\n222\nbbb\n') class InsertClash(TestBase): """Both try to insert lines in the same place.""" def runTest(self): m3 = Merge3(['aaa\n', 'bbb\n'], ['aaa\n', '111\n', 'bbb\n'], ['aaa\n', '222\n', 'bbb\n']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (1, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (1,2, 2,3, 2,3), (2,2, 3,3, 3,3),]) self.assertEquals(list(m3.merge_regions()), [('unchanged', 0,1), ('conflict', 1,1, 1,2, 1,2), ('unchanged', 1,2)]) self.assertEquals(list(m3.merge_groups()), [('unchanged', ['aaa\n']), ('conflict', [], ['111\n'], ['222\n']), ('unchanged', ['bbb\n']), ]) ml = m3.merge_lines(name_a='a', name_b='b', start_marker='<<', mid_marker='--', end_marker='>>') self.assertEquals(''.join(ml), '''aaa << a 111 -- 222 >> b bbb ''') class ReplaceClash(TestBase): """Both try to insert lines in the same place.""" def runTest(self): m3 = Merge3(['aaa', '000', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', '222', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (2, 3)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (2,3, 2,3, 2,3), (3,3, 3,3, 3,3),]) class ReplaceMulti(TestBase): """Replacement with regions of different size.""" def runTest(self): m3 = Merge3(['aaa', '000', '000', 'bbb'], ['aaa', '111', '111', '111', 'bbb'], ['aaa', '222', '222', '222', '222', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (3, 4)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (3,4, 4,5, 5,6), (4,4, 5,5, 6,6),]) def split_lines(t): from cStringIO import StringIO return StringIO(t).readlines() # common base TZU = split_lines(""" The Nameless is the origin of Heaven and Earth; The named is the mother of all things. Therefore let there always be non-being, so we may see their subtlety, And let there always be being, so we may see their outcome. The two are the same, But after they are produced, they have different names. They both may be called deep and profound. Deeper and more profound, The door of all subtleties! """) LAO = split_lines(""" The Way that can be told of is not the eternal Way; The name that can be named is not the eternal name. The Nameless is the origin of Heaven and Earth; The Named is the mother of all things. Therefore let there always be non-being, so we may see their subtlety, And let there always be being, so we may see their outcome. The two are the same, But after they are produced, they have different names. """) TAO = split_lines(""" The Way that can be told of is not the eternal Way; The name that can be named is not the eternal name. The Nameless is the origin of Heaven and Earth; The named is the mother of all things. Therefore let there always be non-being, so we may see their subtlety, And let there always be being, so we may see their result. The two are the same, But after they are produced, they have different names. -- The Way of Lao-Tzu, tr. Wing-tsit Chan """) MERGED_RESULT = split_lines(""" The Way that can be told of is not the eternal Way; The name that can be named is not the eternal name. The Nameless is the origin of Heaven and Earth; The Named is the mother of all things. Therefore let there always be non-being, so we may see their subtlety, And let there always be being, so we may see their result. The two are the same, But after they are produced, they have different names. <<<<<<<< LAO ======== -- The Way of Lao-Tzu, tr. Wing-tsit Chan >>>>>>>> TAO """) class MergePoem(TestBase): """Test case from diff3 manual""" def runTest(self): m3 = Merge3(TZU, LAO, TAO) ml = list(m3.merge_lines('LAO', 'TAO')) self.log('merge result:') self.log(''.join(ml)) self.assertEquals(ml, MERGED_RESULT) commit refs/heads/master mark :832 committer Martin Pool 1120557335 +1000 data 72 - New Merge3.merge_annotated method for debugging. - Remove dead code. from :831 M 644 inline bzrlib/merge3.py data 11289 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # mbp: "you know that thing where cvs gives you conflict markers?" # s: "i hate that." def intersect(ra, rb): """Given two ranges return the range where they intersect or None. >>> intersect((0, 10), (0, 6)) (0, 6) >>> intersect((0, 10), (5, 15)) (5, 10) >>> intersect((0, 10), (10, 15)) >>> intersect((0, 9), (10, 15)) >>> intersect((0, 9), (7, 15)) (7, 9) """ assert ra[0] <= ra[1] assert rb[0] <= rb[1] sa = max(ra[0], rb[0]) sb = min(ra[1], rb[1]) if sa < sb: return sa, sb else: return None class Merge3(object): """3-way merge of texts. Given BASE, OTHER, THIS, tries to produce a combined text incorporating the changes from both BASE->OTHER and BASE->THIS. All three will typically be sequences of lines.""" def __init__(self, base, a, b): self.base = base self.a = a self.b = b from difflib import SequenceMatcher self.a_ops = SequenceMatcher(None, base, a).get_opcodes() self.b_ops = SequenceMatcher(None, base, b).get_opcodes() def merge_lines(self, name_a=None, name_b=None, start_marker='<<<<<<<<', mid_marker='========', end_marker='>>>>>>>>', show_base=False): """Return merge in cvs-like form. """ if name_a: start_marker = start_marker + ' ' + name_a if name_b: end_marker = end_marker + ' ' + name_b for t in self.merge_regions(): what = t[0] if what == 'unchanged': for i in range(t[1], t[2]): yield self.base[i] elif what == 'a' or what == 'same': for i in range(t[1], t[2]): yield self.a[i] elif what == 'b': for i in range(t[1], t[2]): yield self.b[i] elif what == 'conflict': yield start_marker + '\n' for i in range(t[3], t[4]): yield self.a[i] yield mid_marker + '\n' for i in range(t[5], t[6]): yield self.b[i] yield end_marker + '\n' else: raise ValueError(what) def merge_annotated(self): """Return merge with conflicts, showing origin of lines. Most useful for debugging merge. """ for t in self.merge_regions(): what = t[0] if what == 'unchanged': for i in range(t[1], t[2]): yield 'u | ' + self.base[i] elif what == 'a' or what == 'same': for i in range(t[1], t[2]): yield what[0] + ' | ' + self.a[i] elif what == 'b': for i in range(t[1], t[2]): yield 'b | ' + self.b[i] elif what == 'conflict': yield '<<<<\n' for i in range(t[3], t[4]): yield 'A | ' + self.a[i] yield '----\n' for i in range(t[5], t[6]): yield 'B | ' + self.b[i] yield '>>>>\n' else: raise ValueError(what) def merge_groups(self): """Yield sequence of line groups. Each one is a tuple: 'unchanged', lines Lines unchanged from base 'a', lines Lines taken from a 'same', lines Lines taken from a (and equal to b) 'b', lines Lines taken from b 'conflict', base_lines, a_lines, b_lines Lines from base were changed to either a or b and conflict. """ for t in self.merge_regions(): what = t[0] if what == 'unchanged': yield what, self.base[t[1]:t[2]] elif what == 'a' or what == 'same': yield what, self.a[t[1]:t[2]] elif what == 'b': yield what, self.b[t[1]:t[2]] elif what == 'conflict': yield (what, self.base[t[1]:t[2]], self.a[t[3]:t[4]], self.b[t[5]:t[6]]) else: raise ValueError(what) def merge_regions(self): """Return sequences of matching and conflicting regions. This returns tuples, where the first value says what kind we have: 'unchanged', start, end Take a region of base[start:end] 'same', astart, aend b and a are different from base but give the same result 'a', start, end Non-clashing insertion from a[start:end] Method is as follows: The two sequences align only on regions which match the base and both descendents. These are found by doing a two-way diff of each one against the base, and then finding the intersections between those regions. These "sync regions" are by definition unchanged in both and easily dealt with. The regions in between can be in any of three cases: conflicted, or changed on only one side. """ # section a[0:ia] has been disposed of, etc iz = ia = ib = 0 for zmatch, zend, amatch, aend, bmatch, bend in self.find_sync_regions(): matchlen = zend - zmatch assert matchlen >= 0 assert matchlen == (aend - amatch) assert matchlen == (bend - bmatch) len_a = amatch - ia len_b = bmatch - ib len_base = zmatch - iz assert len_a >= 0 assert len_b >= 0 assert len_base >= 0 if len_a or len_b: lines_base = self.base[iz:zmatch] lines_a = self.a[ia:amatch] lines_b = self.b[ib:bmatch] # TODO: check the len just as a shortcut equal_a = (lines_a == lines_base) equal_b = (lines_b == lines_base) same = lines_a == lines_b if same: yield 'same', ia, amatch elif equal_a and not equal_b: yield 'b', ib, bmatch elif equal_b and not equal_a: yield 'a', ia, amatch elif not equal_a and not equal_b: yield 'conflict', iz, zmatch, ia, amatch, ib, bmatch else: assert 0 ia = amatch ib = bmatch iz = zmatch # if the same part of the base was deleted on both sides # that's OK, we can just skip it. if matchlen > 0: assert ia == amatch assert ib == bmatch assert iz == zmatch yield 'unchanged', zmatch, zend iz = zend ia = aend ib = bend def find_sync_regions(self): """Return a list of sync regions, where both descendents match the base. Generates a list of (base1, base2, a1, a2, b1, b2). There is always a zero-length sync region at the end of all the files. """ from difflib import SequenceMatcher aiter = iter(SequenceMatcher(None, self.base, self.a).get_matching_blocks()) biter = iter(SequenceMatcher(None, self.base, self.b).get_matching_blocks()) abase, amatch, alen = aiter.next() bbase, bmatch, blen = biter.next() while aiter and biter: # there is an unconflicted block at i; how long does it # extend? until whichever one ends earlier. i = intersect((abase, abase+alen), (bbase, bbase+blen)) if i: intbase = i[0] intend = i[1] intlen = intend - intbase # found a match of base[i[0], i[1]]; this may be less than # the region that matches in either one assert intlen <= alen assert intlen <= blen assert abase <= intbase assert bbase <= intbase asub = amatch + (intbase - abase) bsub = bmatch + (intbase - bbase) aend = asub + intlen bend = bsub + intlen assert self.base[intbase:intend] == self.a[asub:aend], \ (self.base[intbase:intend], self.a[asub:aend]) assert self.base[intbase:intend] == self.b[bsub:bend] yield (intbase, intend, asub, aend, bsub, bend) # advance whichever one ends first in the base text if (abase + alen) < (bbase + blen): abase, amatch, alen = aiter.next() else: bbase, bmatch, blen = biter.next() intbase = len(self.base) abase = len(self.a) bbase = len(self.b) yield (intbase, intbase, abase, abase, bbase, bbase) def find_unconflicted(self): """Return a list of ranges in base that are not conflicted.""" from difflib import SequenceMatcher am = SequenceMatcher(None, self.base, self.a).get_matching_blocks() bm = SequenceMatcher(None, self.base, self.b).get_matching_blocks() unc = [] while am and bm: # there is an unconflicted block at i; how long does it # extend? until whichever one ends earlier. a1 = am[0][0] a2 = a1 + am[0][2] b1 = bm[0][0] b2 = b1 + bm[0][2] i = intersect((a1, a2), (b1, b2)) if i: unc.append(i) if a2 < b2: del am[0] else: del bm[0] return unc def main(argv): # as for diff3 and meld the syntax is "MINE BASE OTHER" a = file(argv[1], 'rt').readlines() base = file(argv[2], 'rt').readlines() b = file(argv[3], 'rt').readlines() m3 = Merge3(base, a, b) # sys.stdout.writelines(m3.merge_lines(name_a=argv[1], name_b=argv[3])) sys.stdout.writelines(m3.merge_annotated()) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) commit refs/heads/master mark :833 committer Martin Pool 1120558477 +1000 data 43 - don't sync up on blank or hash-only lines from :832 M 644 inline bzrlib/merge3.py data 11444 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # mbp: "you know that thing where cvs gives you conflict markers?" # s: "i hate that." def intersect(ra, rb): """Given two ranges return the range where they intersect or None. >>> intersect((0, 10), (0, 6)) (0, 6) >>> intersect((0, 10), (5, 15)) (5, 10) >>> intersect((0, 10), (10, 15)) >>> intersect((0, 9), (10, 15)) >>> intersect((0, 9), (7, 15)) (7, 9) """ assert ra[0] <= ra[1] assert rb[0] <= rb[1] sa = max(ra[0], rb[0]) sb = min(ra[1], rb[1]) if sa < sb: return sa, sb else: return None class Merge3(object): """3-way merge of texts. Given BASE, OTHER, THIS, tries to produce a combined text incorporating the changes from both BASE->OTHER and BASE->THIS. All three will typically be sequences of lines.""" def __init__(self, base, a, b): self.base = base self.a = a self.b = b from difflib import SequenceMatcher self.a_ops = SequenceMatcher(None, base, a).get_opcodes() self.b_ops = SequenceMatcher(None, base, b).get_opcodes() def merge_lines(self, name_a=None, name_b=None, start_marker='<<<<<<<<', mid_marker='========', end_marker='>>>>>>>>', show_base=False): """Return merge in cvs-like form. """ if name_a: start_marker = start_marker + ' ' + name_a if name_b: end_marker = end_marker + ' ' + name_b for t in self.merge_regions(): what = t[0] if what == 'unchanged': for i in range(t[1], t[2]): yield self.base[i] elif what == 'a' or what == 'same': for i in range(t[1], t[2]): yield self.a[i] elif what == 'b': for i in range(t[1], t[2]): yield self.b[i] elif what == 'conflict': yield start_marker + '\n' for i in range(t[3], t[4]): yield self.a[i] yield mid_marker + '\n' for i in range(t[5], t[6]): yield self.b[i] yield end_marker + '\n' else: raise ValueError(what) def merge_annotated(self): """Return merge with conflicts, showing origin of lines. Most useful for debugging merge. """ for t in self.merge_regions(): what = t[0] if what == 'unchanged': for i in range(t[1], t[2]): yield 'u | ' + self.base[i] elif what == 'a' or what == 'same': for i in range(t[1], t[2]): yield what[0] + ' | ' + self.a[i] elif what == 'b': for i in range(t[1], t[2]): yield 'b | ' + self.b[i] elif what == 'conflict': yield '<<<<\n' for i in range(t[3], t[4]): yield 'A | ' + self.a[i] yield '----\n' for i in range(t[5], t[6]): yield 'B | ' + self.b[i] yield '>>>>\n' else: raise ValueError(what) def merge_groups(self): """Yield sequence of line groups. Each one is a tuple: 'unchanged', lines Lines unchanged from base 'a', lines Lines taken from a 'same', lines Lines taken from a (and equal to b) 'b', lines Lines taken from b 'conflict', base_lines, a_lines, b_lines Lines from base were changed to either a or b and conflict. """ for t in self.merge_regions(): what = t[0] if what == 'unchanged': yield what, self.base[t[1]:t[2]] elif what == 'a' or what == 'same': yield what, self.a[t[1]:t[2]] elif what == 'b': yield what, self.b[t[1]:t[2]] elif what == 'conflict': yield (what, self.base[t[1]:t[2]], self.a[t[3]:t[4]], self.b[t[5]:t[6]]) else: raise ValueError(what) def merge_regions(self): """Return sequences of matching and conflicting regions. This returns tuples, where the first value says what kind we have: 'unchanged', start, end Take a region of base[start:end] 'same', astart, aend b and a are different from base but give the same result 'a', start, end Non-clashing insertion from a[start:end] Method is as follows: The two sequences align only on regions which match the base and both descendents. These are found by doing a two-way diff of each one against the base, and then finding the intersections between those regions. These "sync regions" are by definition unchanged in both and easily dealt with. The regions in between can be in any of three cases: conflicted, or changed on only one side. """ # section a[0:ia] has been disposed of, etc iz = ia = ib = 0 for zmatch, zend, amatch, aend, bmatch, bend in self.find_sync_regions(): matchlen = zend - zmatch assert matchlen >= 0 assert matchlen == (aend - amatch) assert matchlen == (bend - bmatch) len_a = amatch - ia len_b = bmatch - ib len_base = zmatch - iz assert len_a >= 0 assert len_b >= 0 assert len_base >= 0 if len_a or len_b: lines_base = self.base[iz:zmatch] lines_a = self.a[ia:amatch] lines_b = self.b[ib:bmatch] # TODO: check the len just as a shortcut equal_a = (lines_a == lines_base) equal_b = (lines_b == lines_base) same = lines_a == lines_b if same: yield 'same', ia, amatch elif equal_a and not equal_b: yield 'b', ib, bmatch elif equal_b and not equal_a: yield 'a', ia, amatch elif not equal_a and not equal_b: yield 'conflict', iz, zmatch, ia, amatch, ib, bmatch else: assert 0 ia = amatch ib = bmatch iz = zmatch # if the same part of the base was deleted on both sides # that's OK, we can just skip it. if matchlen > 0: assert ia == amatch assert ib == bmatch assert iz == zmatch yield 'unchanged', zmatch, zend iz = zend ia = aend ib = bend def find_sync_regions(self): """Return a list of sync regions, where both descendents match the base. Generates a list of (base1, base2, a1, a2, b1, b2). There is always a zero-length sync region at the end of all the files. """ from difflib import SequenceMatcher aiter = iter(SequenceMatcher(None, self.base, self.a).get_matching_blocks()) biter = iter(SequenceMatcher(None, self.base, self.b).get_matching_blocks()) abase, amatch, alen = aiter.next() bbase, bmatch, blen = biter.next() while aiter and biter: # there is an unconflicted block at i; how long does it # extend? until whichever one ends earlier. i = intersect((abase, abase+alen), (bbase, bbase+blen)) if i: intbase = i[0] intend = i[1] intlen = intend - intbase # found a match of base[i[0], i[1]]; this may be less than # the region that matches in either one assert intlen <= alen assert intlen <= blen assert abase <= intbase assert bbase <= intbase asub = amatch + (intbase - abase) bsub = bmatch + (intbase - bbase) aend = asub + intlen bend = bsub + intlen assert self.base[intbase:intend] == self.a[asub:aend], \ (self.base[intbase:intend], self.a[asub:aend]) assert self.base[intbase:intend] == self.b[bsub:bend] yield (intbase, intend, asub, aend, bsub, bend) # advance whichever one ends first in the base text if (abase + alen) < (bbase + blen): abase, amatch, alen = aiter.next() else: bbase, bmatch, blen = biter.next() intbase = len(self.base) abase = len(self.a) bbase = len(self.b) yield (intbase, intbase, abase, abase, bbase, bbase) def find_unconflicted(self): """Return a list of ranges in base that are not conflicted.""" from difflib import SequenceMatcher import re # don't sync-up on lines containing only blanks or pounds junk_re = re.compile(r'^[ \t#]*$') am = SequenceMatcher(junk_re.match, self.base, self.a).get_matching_blocks() bm = SequenceMatcher(junk_re.match, self.base, self.b).get_matching_blocks() unc = [] while am and bm: # there is an unconflicted block at i; how long does it # extend? until whichever one ends earlier. a1 = am[0][0] a2 = a1 + am[0][2] b1 = bm[0][0] b2 = b1 + bm[0][2] i = intersect((a1, a2), (b1, b2)) if i: unc.append(i) if a2 < b2: del am[0] else: del bm[0] return unc def main(argv): # as for diff3 and meld the syntax is "MINE BASE OTHER" a = file(argv[1], 'rt').readlines() base = file(argv[2], 'rt').readlines() b = file(argv[3], 'rt').readlines() m3 = Merge3(base, a, b) # sys.stdout.writelines(m3.merge_lines(name_a=argv[1], name_b=argv[3])) sys.stdout.writelines(m3.merge_annotated()) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) commit refs/heads/master mark :834 committer Martin Pool 1120567954 +1000 data 44 - Small performance optimization for merge3 from :833 M 644 inline bzrlib/merge3.py data 11584 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # mbp: "you know that thing where cvs gives you conflict markers?" # s: "i hate that." def intersect(ra, rb): """Given two ranges return the range where they intersect or None. >>> intersect((0, 10), (0, 6)) (0, 6) >>> intersect((0, 10), (5, 15)) (5, 10) >>> intersect((0, 10), (10, 15)) >>> intersect((0, 9), (10, 15)) >>> intersect((0, 9), (7, 15)) (7, 9) """ assert ra[0] <= ra[1] assert rb[0] <= rb[1] sa = max(ra[0], rb[0]) sb = min(ra[1], rb[1]) if sa < sb: return sa, sb else: return None class Merge3(object): """3-way merge of texts. Given BASE, OTHER, THIS, tries to produce a combined text incorporating the changes from both BASE->OTHER and BASE->THIS. All three will typically be sequences of lines.""" def __init__(self, base, a, b): self.base = base self.a = a self.b = b from difflib import SequenceMatcher self.a_ops = SequenceMatcher(None, base, a).get_opcodes() self.b_ops = SequenceMatcher(None, base, b).get_opcodes() def merge_lines(self, name_a=None, name_b=None, start_marker='<<<<<<<<', mid_marker='========', end_marker='>>>>>>>>', show_base=False): """Return merge in cvs-like form. """ if name_a: start_marker = start_marker + ' ' + name_a if name_b: end_marker = end_marker + ' ' + name_b for t in self.merge_regions(): what = t[0] if what == 'unchanged': for i in range(t[1], t[2]): yield self.base[i] elif what == 'a' or what == 'same': for i in range(t[1], t[2]): yield self.a[i] elif what == 'b': for i in range(t[1], t[2]): yield self.b[i] elif what == 'conflict': yield start_marker + '\n' for i in range(t[3], t[4]): yield self.a[i] yield mid_marker + '\n' for i in range(t[5], t[6]): yield self.b[i] yield end_marker + '\n' else: raise ValueError(what) def merge_annotated(self): """Return merge with conflicts, showing origin of lines. Most useful for debugging merge. """ for t in self.merge_regions(): what = t[0] if what == 'unchanged': for i in range(t[1], t[2]): yield 'u | ' + self.base[i] elif what == 'a' or what == 'same': for i in range(t[1], t[2]): yield what[0] + ' | ' + self.a[i] elif what == 'b': for i in range(t[1], t[2]): yield 'b | ' + self.b[i] elif what == 'conflict': yield '<<<<\n' for i in range(t[3], t[4]): yield 'A | ' + self.a[i] yield '----\n' for i in range(t[5], t[6]): yield 'B | ' + self.b[i] yield '>>>>\n' else: raise ValueError(what) def merge_groups(self): """Yield sequence of line groups. Each one is a tuple: 'unchanged', lines Lines unchanged from base 'a', lines Lines taken from a 'same', lines Lines taken from a (and equal to b) 'b', lines Lines taken from b 'conflict', base_lines, a_lines, b_lines Lines from base were changed to either a or b and conflict. """ for t in self.merge_regions(): what = t[0] if what == 'unchanged': yield what, self.base[t[1]:t[2]] elif what == 'a' or what == 'same': yield what, self.a[t[1]:t[2]] elif what == 'b': yield what, self.b[t[1]:t[2]] elif what == 'conflict': yield (what, self.base[t[1]:t[2]], self.a[t[3]:t[4]], self.b[t[5]:t[6]]) else: raise ValueError(what) def merge_regions(self): """Return sequences of matching and conflicting regions. This returns tuples, where the first value says what kind we have: 'unchanged', start, end Take a region of base[start:end] 'same', astart, aend b and a are different from base but give the same result 'a', start, end Non-clashing insertion from a[start:end] Method is as follows: The two sequences align only on regions which match the base and both descendents. These are found by doing a two-way diff of each one against the base, and then finding the intersections between those regions. These "sync regions" are by definition unchanged in both and easily dealt with. The regions in between can be in any of three cases: conflicted, or changed on only one side. """ # section a[0:ia] has been disposed of, etc iz = ia = ib = 0 for zmatch, zend, amatch, aend, bmatch, bend in self.find_sync_regions(): matchlen = zend - zmatch assert matchlen >= 0 assert matchlen == (aend - amatch) assert matchlen == (bend - bmatch) len_a = amatch - ia len_b = bmatch - ib len_base = zmatch - iz assert len_a >= 0 assert len_b >= 0 assert len_base >= 0 if len_a or len_b: lines_base = self.base[iz:zmatch] lines_a = self.a[ia:amatch] lines_b = self.b[ib:bmatch] # we check the len just as a shortcut equal_a = (len_a == len_base and lines_a == lines_base) equal_b = (len_b == len_base and lines_b == lines_base) same = (len_a == len_b and lines_a == lines_b) if same: yield 'same', ia, amatch elif equal_a and not equal_b: yield 'b', ib, bmatch elif equal_b and not equal_a: yield 'a', ia, amatch elif not equal_a and not equal_b: yield 'conflict', iz, zmatch, ia, amatch, ib, bmatch else: assert 0 ia = amatch ib = bmatch iz = zmatch # if the same part of the base was deleted on both sides # that's OK, we can just skip it. if matchlen > 0: assert ia == amatch assert ib == bmatch assert iz == zmatch yield 'unchanged', zmatch, zend iz = zend ia = aend ib = bend def find_sync_regions(self): """Return a list of sync regions, where both descendents match the base. Generates a list of (base1, base2, a1, a2, b1, b2). There is always a zero-length sync region at the end of all the files. """ from difflib import SequenceMatcher aiter = iter(SequenceMatcher(None, self.base, self.a).get_matching_blocks()) biter = iter(SequenceMatcher(None, self.base, self.b).get_matching_blocks()) abase, amatch, alen = aiter.next() bbase, bmatch, blen = biter.next() while aiter and biter: # there is an unconflicted block at i; how long does it # extend? until whichever one ends earlier. i = intersect((abase, abase+alen), (bbase, bbase+blen)) if i: intbase = i[0] intend = i[1] intlen = intend - intbase # found a match of base[i[0], i[1]]; this may be less than # the region that matches in either one assert intlen <= alen assert intlen <= blen assert abase <= intbase assert bbase <= intbase asub = amatch + (intbase - abase) bsub = bmatch + (intbase - bbase) aend = asub + intlen bend = bsub + intlen assert self.base[intbase:intend] == self.a[asub:aend], \ (self.base[intbase:intend], self.a[asub:aend]) assert self.base[intbase:intend] == self.b[bsub:bend] yield (intbase, intend, asub, aend, bsub, bend) # advance whichever one ends first in the base text if (abase + alen) < (bbase + blen): abase, amatch, alen = aiter.next() else: bbase, bmatch, blen = biter.next() intbase = len(self.base) abase = len(self.a) bbase = len(self.b) yield (intbase, intbase, abase, abase, bbase, bbase) def find_unconflicted(self): """Return a list of ranges in base that are not conflicted.""" from difflib import SequenceMatcher import re # don't sync-up on lines containing only blanks or pounds junk_re = re.compile(r'^[ \t#]*$') am = SequenceMatcher(junk_re.match, self.base, self.a).get_matching_blocks() bm = SequenceMatcher(junk_re.match, self.base, self.b).get_matching_blocks() unc = [] while am and bm: # there is an unconflicted block at i; how long does it # extend? until whichever one ends earlier. a1 = am[0][0] a2 = a1 + am[0][2] b1 = bm[0][0] b2 = b1 + bm[0][2] i = intersect((a1, a2), (b1, b2)) if i: unc.append(i) if a2 < b2: del am[0] else: del bm[0] return unc def main(argv): # as for diff3 and meld the syntax is "MINE BASE OTHER" a = file(argv[1], 'rt').readlines() base = file(argv[2], 'rt').readlines() b = file(argv[3], 'rt').readlines() m3 = Merge3(base, a, b) # sys.stdout.writelines(m3.merge_lines(name_a=argv[1], name_b=argv[3])) sys.stdout.writelines(m3.merge_annotated()) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) commit refs/heads/master mark :835 committer Martin Pool 1120568558 +1000 data 3 doc from :834 M 644 inline bzrlib/selftest/testmerge3.py data 8801 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from bzrlib.selftest import InTempDir, TestBase from bzrlib.merge3 import Merge3 class NoChanges(TestBase): """No conflicts because nothing changed""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', 'bbb'], ['aaa', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0, 2, 0, 2, 0, 2), (2,2, 2,2, 2,2)]) self.assertEquals(list(m3.merge_regions()), [('unchanged', 0, 2)]) self.assertEquals(list(m3.merge_groups()), [('unchanged', ['aaa', 'bbb'])]) class FrontInsert(TestBase): def runTest(self): m3 = Merge3(['zz'], ['aaa', 'bbb', 'zz'], ['zz']) # todo: should use a sentinal at end as from get_matching_blocks # to match without zz self.assertEquals(list(m3.find_sync_regions()), [(0,1, 2,3, 0,1), (1,1, 3,3, 1,1),]) self.assertEquals(list(m3.merge_regions()), [('a', 0, 2), ('unchanged', 0, 1)]) self.assertEquals(list(m3.merge_groups()), [('a', ['aaa', 'bbb']), ('unchanged', ['zz'])]) class NullInsert(TestBase): def runTest(self): m3 = Merge3([], ['aaa', 'bbb'], []) # todo: should use a sentinal at end as from get_matching_blocks # to match without zz self.assertEquals(list(m3.find_sync_regions()), [(0,0, 2,2, 0,0)]) self.assertEquals(list(m3.merge_regions()), [('a', 0, 2)]) self.assertEquals(list(m3.merge_lines()), ['aaa', 'bbb']) class NoConflicts(TestBase): """No conflicts because only one side changed""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (1, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (1,2, 2,3, 1,2), (2,2, 3,3, 2,2),]) self.assertEquals(list(m3.merge_regions()), [('unchanged', 0, 1), ('a', 1, 2), ('unchanged', 1, 2),]) class InsertAgreement(TestBase): def runTest(self): m3 = Merge3(['aaa\n', 'bbb\n'], ['aaa\n', '222\n', 'bbb\n'], ['aaa\n', '222\n', 'bbb\n']) self.assertEquals(''.join(m3.merge_lines()), 'aaa\n222\nbbb\n') class InsertClash(TestBase): """Both try to insert lines in the same place.""" def runTest(self): m3 = Merge3(['aaa\n', 'bbb\n'], ['aaa\n', '111\n', 'bbb\n'], ['aaa\n', '222\n', 'bbb\n']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (1, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (1,2, 2,3, 2,3), (2,2, 3,3, 3,3),]) self.assertEquals(list(m3.merge_regions()), [('unchanged', 0,1), ('conflict', 1,1, 1,2, 1,2), ('unchanged', 1,2)]) self.assertEquals(list(m3.merge_groups()), [('unchanged', ['aaa\n']), ('conflict', [], ['111\n'], ['222\n']), ('unchanged', ['bbb\n']), ]) ml = m3.merge_lines(name_a='a', name_b='b', start_marker='<<', mid_marker='--', end_marker='>>') self.assertEquals(''.join(ml), '''aaa << a 111 -- 222 >> b bbb ''') class ReplaceClash(TestBase): """Both try to insert lines in the same place.""" def runTest(self): m3 = Merge3(['aaa', '000', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', '222', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (2, 3)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (2,3, 2,3, 2,3), (3,3, 3,3, 3,3),]) class ReplaceMulti(TestBase): """Replacement with regions of different size.""" def runTest(self): m3 = Merge3(['aaa', '000', '000', 'bbb'], ['aaa', '111', '111', '111', 'bbb'], ['aaa', '222', '222', '222', '222', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (3, 4)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (3,4, 4,5, 5,6), (4,4, 5,5, 6,6),]) def split_lines(t): from cStringIO import StringIO return StringIO(t).readlines() ############################################################ # test case from the gnu diffutils manual # common base TZU = split_lines(""" The Nameless is the origin of Heaven and Earth; The named is the mother of all things. Therefore let there always be non-being, so we may see their subtlety, And let there always be being, so we may see their outcome. The two are the same, But after they are produced, they have different names. They both may be called deep and profound. Deeper and more profound, The door of all subtleties! """) LAO = split_lines(""" The Way that can be told of is not the eternal Way; The name that can be named is not the eternal name. The Nameless is the origin of Heaven and Earth; The Named is the mother of all things. Therefore let there always be non-being, so we may see their subtlety, And let there always be being, so we may see their outcome. The two are the same, But after they are produced, they have different names. """) TAO = split_lines(""" The Way that can be told of is not the eternal Way; The name that can be named is not the eternal name. The Nameless is the origin of Heaven and Earth; The named is the mother of all things. Therefore let there always be non-being, so we may see their subtlety, And let there always be being, so we may see their result. The two are the same, But after they are produced, they have different names. -- The Way of Lao-Tzu, tr. Wing-tsit Chan """) MERGED_RESULT = split_lines(""" The Way that can be told of is not the eternal Way; The name that can be named is not the eternal name. The Nameless is the origin of Heaven and Earth; The Named is the mother of all things. Therefore let there always be non-being, so we may see their subtlety, And let there always be being, so we may see their result. The two are the same, But after they are produced, they have different names. <<<<<<<< LAO ======== -- The Way of Lao-Tzu, tr. Wing-tsit Chan >>>>>>>> TAO """) class MergePoem(TestBase): """Test case from diff3 manual""" def runTest(self): m3 = Merge3(TZU, LAO, TAO) ml = list(m3.merge_lines('LAO', 'TAO')) self.log('merge result:') self.log(''.join(ml)) self.assertEquals(ml, MERGED_RESULT) commit refs/heads/master mark :836 committer Martin Pool 1120610223 +1000 data 28 - more merge tests from john from :835 M 644 inline bzrlib/selftest/testmerge3.py data 10355 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from bzrlib.selftest import InTempDir, TestBase from bzrlib.merge3 import Merge3 class NoChanges(TestBase): """No conflicts because nothing changed""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', 'bbb'], ['aaa', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0, 2, 0, 2, 0, 2), (2,2, 2,2, 2,2)]) self.assertEquals(list(m3.merge_regions()), [('unchanged', 0, 2)]) self.assertEquals(list(m3.merge_groups()), [('unchanged', ['aaa', 'bbb'])]) class FrontInsert(TestBase): def runTest(self): m3 = Merge3(['zz'], ['aaa', 'bbb', 'zz'], ['zz']) # todo: should use a sentinal at end as from get_matching_blocks # to match without zz self.assertEquals(list(m3.find_sync_regions()), [(0,1, 2,3, 0,1), (1,1, 3,3, 1,1),]) self.assertEquals(list(m3.merge_regions()), [('a', 0, 2), ('unchanged', 0, 1)]) self.assertEquals(list(m3.merge_groups()), [('a', ['aaa', 'bbb']), ('unchanged', ['zz'])]) class NullInsert(TestBase): def runTest(self): m3 = Merge3([], ['aaa', 'bbb'], []) # todo: should use a sentinal at end as from get_matching_blocks # to match without zz self.assertEquals(list(m3.find_sync_regions()), [(0,0, 2,2, 0,0)]) self.assertEquals(list(m3.merge_regions()), [('a', 0, 2)]) self.assertEquals(list(m3.merge_lines()), ['aaa', 'bbb']) class NoConflicts(TestBase): """No conflicts because only one side changed""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (1, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (1,2, 2,3, 1,2), (2,2, 3,3, 2,2),]) self.assertEquals(list(m3.merge_regions()), [('unchanged', 0, 1), ('a', 1, 2), ('unchanged', 1, 2),]) class AppendA(TestBase): def runTest(self): m3 = Merge3(['aaa\n', 'bbb\n'], ['aaa\n', 'bbb\n', '222\n'], ['aaa\n', 'bbb\n']) self.assertEquals(''.join(m3.merge_lines()), 'aaa\nbbb\n222\n') class AppendB(TestBase): def runTest(self): m3 = Merge3(['aaa\n', 'bbb\n'], ['aaa\n', 'bbb\n'], ['aaa\n', 'bbb\n', '222\n']) self.assertEquals(''.join(m3.merge_lines()), 'aaa\nbbb\n222\n') class AppendAgreement(TestBase): def runTest(self): m3 = Merge3(['aaa\n', 'bbb\n'], ['aaa\n', 'bbb\n', '222\n'], ['aaa\n', 'bbb\n', '222\n']) self.assertEquals(''.join(m3.merge_lines()), 'aaa\nbbb\n222\n') class AppendClash(TestBase): def runTest(self): m3 = Merge3(['aaa\n', 'bbb\n'], ['aaa\n', 'bbb\n', '222\n'], ['aaa\n', 'bbb\n', '333\n']) ml = m3.merge_lines(name_a='a', name_b='b', start_marker='<<', mid_marker='--', end_marker='>>') self.assertEquals(''.join(ml), '''\ aaa bbb << a 222 -- 333 >> b ''') class InsertAgreement(TestBase): def runTest(self): m3 = Merge3(['aaa\n', 'bbb\n'], ['aaa\n', '222\n', 'bbb\n'], ['aaa\n', '222\n', 'bbb\n']) ml = m3.merge_lines(name_a='a', name_b='b', start_marker='<<', mid_marker='--', end_marker='>>') self.assertEquals(''.join(m3.merge_lines()), 'aaa\n222\nbbb\n') class InsertClash(TestBase): """Both try to insert lines in the same place.""" def runTest(self): m3 = Merge3(['aaa\n', 'bbb\n'], ['aaa\n', '111\n', 'bbb\n'], ['aaa\n', '222\n', 'bbb\n']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (1, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (1,2, 2,3, 2,3), (2,2, 3,3, 3,3),]) self.assertEquals(list(m3.merge_regions()), [('unchanged', 0,1), ('conflict', 1,1, 1,2, 1,2), ('unchanged', 1,2)]) self.assertEquals(list(m3.merge_groups()), [('unchanged', ['aaa\n']), ('conflict', [], ['111\n'], ['222\n']), ('unchanged', ['bbb\n']), ]) ml = m3.merge_lines(name_a='a', name_b='b', start_marker='<<', mid_marker='--', end_marker='>>') self.assertEquals(''.join(ml), '''aaa << a 111 -- 222 >> b bbb ''') class ReplaceClash(TestBase): """Both try to insert lines in the same place.""" def runTest(self): m3 = Merge3(['aaa', '000', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', '222', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (2, 3)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (2,3, 2,3, 2,3), (3,3, 3,3, 3,3),]) class ReplaceMulti(TestBase): """Replacement with regions of different size.""" def runTest(self): m3 = Merge3(['aaa', '000', '000', 'bbb'], ['aaa', '111', '111', '111', 'bbb'], ['aaa', '222', '222', '222', '222', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (3, 4)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (3,4, 4,5, 5,6), (4,4, 5,5, 6,6),]) def split_lines(t): from cStringIO import StringIO return StringIO(t).readlines() ############################################################ # test case from the gnu diffutils manual # common base TZU = split_lines(""" The Nameless is the origin of Heaven and Earth; The named is the mother of all things. Therefore let there always be non-being, so we may see their subtlety, And let there always be being, so we may see their outcome. The two are the same, But after they are produced, they have different names. They both may be called deep and profound. Deeper and more profound, The door of all subtleties! """) LAO = split_lines(""" The Way that can be told of is not the eternal Way; The name that can be named is not the eternal name. The Nameless is the origin of Heaven and Earth; The Named is the mother of all things. Therefore let there always be non-being, so we may see their subtlety, And let there always be being, so we may see their outcome. The two are the same, But after they are produced, they have different names. """) TAO = split_lines(""" The Way that can be told of is not the eternal Way; The name that can be named is not the eternal name. The Nameless is the origin of Heaven and Earth; The named is the mother of all things. Therefore let there always be non-being, so we may see their subtlety, And let there always be being, so we may see their result. The two are the same, But after they are produced, they have different names. -- The Way of Lao-Tzu, tr. Wing-tsit Chan """) MERGED_RESULT = split_lines(""" The Way that can be told of is not the eternal Way; The name that can be named is not the eternal name. The Nameless is the origin of Heaven and Earth; The Named is the mother of all things. Therefore let there always be non-being, so we may see their subtlety, And let there always be being, so we may see their result. The two are the same, But after they are produced, they have different names. <<<<<<<< LAO ======== -- The Way of Lao-Tzu, tr. Wing-tsit Chan >>>>>>>> TAO """) class MergePoem(TestBase): """Test case from diff3 manual""" def runTest(self): m3 = Merge3(TZU, LAO, TAO) ml = list(m3.merge_lines('LAO', 'TAO')) self.log('merge result:') self.log(''.join(ml)) self.assertEquals(ml, MERGED_RESULT) commit refs/heads/master mark :837 committer Martin Pool 1120610293 +1000 data 56 - better error when failing to run selftest on python2.3 from :836 M 644 inline bzrlib/selftest/__init__.py data 9259 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: import sys sys.stderr.write("testbzr: sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") raise class CommandFailed(Exception): pass class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr, so that we can run it # through a specified Python intepreter OVERRIDE_PYTHON = None # to run with alternative python 'python' BZRPATH = 'bzr' _log_buf = "" def formcmd(self, cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = self.BZRPATH if self.OVERRIDE_PYTHON: cmd.insert(0, self.OVERRIDE_PYTHON) self.log('$ %r' % cmd) return cmd def runcmd(self, cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = self.formcmd(cmd) self.log('$ ' + ' '.join(cmd)) actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(self, cmd, retcode=0): """Run a command and return its output""" cmd = self.formcmd(cmd) child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG) outd, errd = child.communicate() self.log(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def build_tree(self, shape): """Build a test tree according to a pattern. shape is a sequence of file specifications. If the final character is '/', a directory is created. This doesn't add anything to a branch. """ # XXX: It's OK to just create them using forward slashes on windows? import os for name in shape: assert isinstance(name, basestring) if name[-1] == '/': os.mkdir(name[:-1]) else: f = file(name, 'wt') print >>f, "contents of", name f.close() def log(self, msg): """Log a message to a progress file""" self._log_buf = self._log_buf + str(msg) + '\n' print >>self.TEST_LOG, msg def check_inventory_shape(self, inv, shape): """ Compare an inventory to a list of expected names. Fail if they are not precisely equal. """ extras = [] shape = list(shape) # copy for path, ie in inv.entries(): name = path.replace('\\', '/') if ie.kind == 'dir': name = name + '/' if name in shape: shape.remove(name) else: extras.append(name) if shape: self.fail("expected paths not found in inventory: %r" % shape) if extras: self.fail("unexpected paths found in inventory: %r" % extras) def check_file_contents(self, filename, expect): self.log("check contents of file %s" % filename) contents = file(filename, 'r').read() if contents != expect: self.log("expected: %r" % expected) self.log("actually: %r" % contents) self.fail("contents of %s not as expected") class InTempDir(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.test_dir = os.path.join(self.TEST_ROOT, self.__class__.__name__) os.mkdir(self.test_dir) os.chdir(self.test_dir) def tearDown(self): import os os.chdir(self.TEST_ROOT) class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ def __init__(self, out): self.out = out TestResult.__init__(self) def startTest(self, test): # TODO: Maybe show test.shortDescription somewhere? print >>self.out, '%-60.60s' % test.id(), self.out.flush() TestResult.startTest(self, test) def stopTest(self, test): # print TestResult.stopTest(self, test) def addError(self, test, err): print >>self.out, 'ERROR' TestResult.addError(self, test, err) _show_test_failure('error', test, err, self.out) def addFailure(self, test, err): print >>self.out, 'FAILURE' TestResult.addFailure(self, test, err) _show_test_failure('failure', test, err, self.out) def addSuccess(self, test): print >>self.out, 'OK' TestResult.addSuccess(self, test) def selftest(): from unittest import TestLoader, TestSuite import bzrlib, bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, bzrlib.commands import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning import bzrlib.selftest.testmerge3 import bzrlib.merge_core from doctest import DocTestSuite import os import shutil import time import sys TestBase.BZRPATH = os.path.join(os.path.realpath(os.path.dirname(bzrlib.__path__[0])), 'bzr') print '%-30s %s' % ('bzr binary', TestBase.BZRPATH) _setup_test_log() _setup_test_dir() print suite = TestSuite() tl = TestLoader() # should also test bzrlib.merge_core, but they seem to be out of date with # the code. for m in bzrlib.selftest.whitebox, \ bzrlib.selftest.versioning, \ bzrlib.selftest.testmerge3: suite.addTest(tl.loadTestsFromModule(m)) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands, \ bzrlib.merge3: suite.addTest(DocTestSuite(m)) suite.addTest(bzrlib.selftest.blackbox.suite()) # save stdout & stderr so there's no leakage from code-under-test real_stdout = sys.stdout real_stderr = sys.stderr sys.stdout = sys.stderr = TestBase.TEST_LOG try: result = _MyResult(real_stdout) suite.run(result) finally: sys.stdout = real_stdout sys.stderr = real_stderr _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os log_filename = os.path.abspath('testbzr.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "bzr tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_ROOT = os.path.abspath("testbzr.tmp") print '%-30s %s' % ('running tests in', TestBase.TEST_ROOT) if os.path.exists(TestBase.TEST_ROOT): shutil.rmtree(TestBase.TEST_ROOT) os.mkdir(TestBase.TEST_ROOT) os.chdir(TestBase.TEST_ROOT) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_ROOT, '.bzr')) def _show_results(result): print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, exc_info, out): from traceback import print_exception print >>out, '-' * 60 print >>out, case desc = case.shortDescription() if desc: print >>out, ' (%s)' % desc print_exception(exc_info[0], exc_info[1], exc_info[2], None, out) if isinstance(case, TestBase): print >>out print >>out, 'log from this test:' print >>out, case._log_buf print >>out, '-' * 60 commit refs/heads/master mark :838 committer Martin Pool 1120611848 +1000 data 232 - Merge3.find_sync_regions() - avoid problems with iters on python2.3 by just stepping through arrays; also make this return a list rather than being a generator. Thanks very much to John for the report and help debugging. from :837 M 644 inline bzrlib/merge3.py data 11861 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # mbp: "you know that thing where cvs gives you conflict markers?" # s: "i hate that." def intersect(ra, rb): """Given two ranges return the range where they intersect or None. >>> intersect((0, 10), (0, 6)) (0, 6) >>> intersect((0, 10), (5, 15)) (5, 10) >>> intersect((0, 10), (10, 15)) >>> intersect((0, 9), (10, 15)) >>> intersect((0, 9), (7, 15)) (7, 9) """ assert ra[0] <= ra[1] assert rb[0] <= rb[1] sa = max(ra[0], rb[0]) sb = min(ra[1], rb[1]) if sa < sb: return sa, sb else: return None class Merge3(object): """3-way merge of texts. Given BASE, OTHER, THIS, tries to produce a combined text incorporating the changes from both BASE->OTHER and BASE->THIS. All three will typically be sequences of lines.""" def __init__(self, base, a, b): self.base = base self.a = a self.b = b from difflib import SequenceMatcher self.a_ops = SequenceMatcher(None, base, a).get_opcodes() self.b_ops = SequenceMatcher(None, base, b).get_opcodes() def merge_lines(self, name_a=None, name_b=None, start_marker='<<<<<<<<', mid_marker='========', end_marker='>>>>>>>>', show_base=False): """Return merge in cvs-like form. """ if name_a: start_marker = start_marker + ' ' + name_a if name_b: end_marker = end_marker + ' ' + name_b for t in self.merge_regions(): what = t[0] if what == 'unchanged': for i in range(t[1], t[2]): yield self.base[i] elif what == 'a' or what == 'same': for i in range(t[1], t[2]): yield self.a[i] elif what == 'b': for i in range(t[1], t[2]): yield self.b[i] elif what == 'conflict': yield start_marker + '\n' for i in range(t[3], t[4]): yield self.a[i] yield mid_marker + '\n' for i in range(t[5], t[6]): yield self.b[i] yield end_marker + '\n' else: raise ValueError(what) def merge_annotated(self): """Return merge with conflicts, showing origin of lines. Most useful for debugging merge. """ for t in self.merge_regions(): what = t[0] if what == 'unchanged': for i in range(t[1], t[2]): yield 'u | ' + self.base[i] elif what == 'a' or what == 'same': for i in range(t[1], t[2]): yield what[0] + ' | ' + self.a[i] elif what == 'b': for i in range(t[1], t[2]): yield 'b | ' + self.b[i] elif what == 'conflict': yield '<<<<\n' for i in range(t[3], t[4]): yield 'A | ' + self.a[i] yield '----\n' for i in range(t[5], t[6]): yield 'B | ' + self.b[i] yield '>>>>\n' else: raise ValueError(what) def merge_groups(self): """Yield sequence of line groups. Each one is a tuple: 'unchanged', lines Lines unchanged from base 'a', lines Lines taken from a 'same', lines Lines taken from a (and equal to b) 'b', lines Lines taken from b 'conflict', base_lines, a_lines, b_lines Lines from base were changed to either a or b and conflict. """ for t in self.merge_regions(): what = t[0] if what == 'unchanged': yield what, self.base[t[1]:t[2]] elif what == 'a' or what == 'same': yield what, self.a[t[1]:t[2]] elif what == 'b': yield what, self.b[t[1]:t[2]] elif what == 'conflict': yield (what, self.base[t[1]:t[2]], self.a[t[3]:t[4]], self.b[t[5]:t[6]]) else: raise ValueError(what) def merge_regions(self): """Return sequences of matching and conflicting regions. This returns tuples, where the first value says what kind we have: 'unchanged', start, end Take a region of base[start:end] 'same', astart, aend b and a are different from base but give the same result 'a', start, end Non-clashing insertion from a[start:end] Method is as follows: The two sequences align only on regions which match the base and both descendents. These are found by doing a two-way diff of each one against the base, and then finding the intersections between those regions. These "sync regions" are by definition unchanged in both and easily dealt with. The regions in between can be in any of three cases: conflicted, or changed on only one side. """ # section a[0:ia] has been disposed of, etc iz = ia = ib = 0 for zmatch, zend, amatch, aend, bmatch, bend in self.find_sync_regions(): #print 'match base [%d:%d]' % (zmatch, zend) matchlen = zend - zmatch assert matchlen >= 0 assert matchlen == (aend - amatch) assert matchlen == (bend - bmatch) len_a = amatch - ia len_b = bmatch - ib len_base = zmatch - iz assert len_a >= 0 assert len_b >= 0 assert len_base >= 0 #print 'unmatched a=%d, b=%d' % (len_a, len_b) if len_a or len_b: lines_base = self.base[iz:zmatch] lines_a = self.a[ia:amatch] lines_b = self.b[ib:bmatch] # we check the len just as a shortcut equal_a = (len_a == len_base and lines_a == lines_base) equal_b = (len_b == len_base and lines_b == lines_base) same = (len_a == len_b and lines_a == lines_b) if same: yield 'same', ia, amatch elif equal_a and not equal_b: yield 'b', ib, bmatch elif equal_b and not equal_a: yield 'a', ia, amatch elif not equal_a and not equal_b: yield 'conflict', iz, zmatch, ia, amatch, ib, bmatch else: assert 0 ia = amatch ib = bmatch iz = zmatch # if the same part of the base was deleted on both sides # that's OK, we can just skip it. if matchlen > 0: assert ia == amatch assert ib == bmatch assert iz == zmatch yield 'unchanged', zmatch, zend iz = zend ia = aend ib = bend def find_sync_regions(self): """Return a list of sync regions, where both descendents match the base. Generates a list of (base1, base2, a1, a2, b1, b2). There is always a zero-length sync region at the end of all the files. """ from difflib import SequenceMatcher ia = ib = 0 amatches = SequenceMatcher(None, self.base, self.a).get_matching_blocks() bmatches = SequenceMatcher(None, self.base, self.b).get_matching_blocks() len_a = len(amatches) len_b = len(bmatches) sl = [] while ia < len_a and ib < len_b: abase, amatch, alen = amatches[ia] bbase, bmatch, blen = bmatches[ib] # there is an unconflicted block at i; how long does it # extend? until whichever one ends earlier. i = intersect((abase, abase+alen), (bbase, bbase+blen)) if i: intbase = i[0] intend = i[1] intlen = intend - intbase # found a match of base[i[0], i[1]]; this may be less than # the region that matches in either one assert intlen <= alen assert intlen <= blen assert abase <= intbase assert bbase <= intbase asub = amatch + (intbase - abase) bsub = bmatch + (intbase - bbase) aend = asub + intlen bend = bsub + intlen assert self.base[intbase:intend] == self.a[asub:aend], \ (self.base[intbase:intend], self.a[asub:aend]) assert self.base[intbase:intend] == self.b[bsub:bend] sl.append((intbase, intend, asub, aend, bsub, bend)) # advance whichever one ends first in the base text if (abase + alen) < (bbase + blen): ia += 1 else: ib += 1 intbase = len(self.base) abase = len(self.a) bbase = len(self.b) sl.append((intbase, intbase, abase, abase, bbase, bbase)) return sl def find_unconflicted(self): """Return a list of ranges in base that are not conflicted.""" from difflib import SequenceMatcher import re # don't sync-up on lines containing only blanks or pounds junk_re = re.compile(r'^[ \t#]*$') am = SequenceMatcher(junk_re.match, self.base, self.a).get_matching_blocks() bm = SequenceMatcher(junk_re.match, self.base, self.b).get_matching_blocks() unc = [] while am and bm: # there is an unconflicted block at i; how long does it # extend? until whichever one ends earlier. a1 = am[0][0] a2 = a1 + am[0][2] b1 = bm[0][0] b2 = b1 + bm[0][2] i = intersect((a1, a2), (b1, b2)) if i: unc.append(i) if a2 < b2: del am[0] else: del bm[0] return unc def main(argv): # as for diff3 and meld the syntax is "MINE BASE OTHER" a = file(argv[1], 'rt').readlines() base = file(argv[2], 'rt').readlines() b = file(argv[3], 'rt').readlines() m3 = Merge3(base, a, b) #for sr in m3.find_sync_regions(): # print sr # sys.stdout.writelines(m3.merge_lines(name_a=argv[1], name_b=argv[3])) sys.stdout.writelines(m3.merge_annotated()) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) commit refs/heads/master mark :839 committer Martin Pool 1120622942 +1000 data 60 - avoid copying string lists when handling unmatched regions from :838 M 644 inline bzrlib/merge3.py data 12199 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # mbp: "you know that thing where cvs gives you conflict markers?" # s: "i hate that." def intersect(ra, rb): """Given two ranges return the range where they intersect or None. >>> intersect((0, 10), (0, 6)) (0, 6) >>> intersect((0, 10), (5, 15)) (5, 10) >>> intersect((0, 10), (10, 15)) >>> intersect((0, 9), (10, 15)) >>> intersect((0, 9), (7, 15)) (7, 9) """ assert ra[0] <= ra[1] assert rb[0] <= rb[1] sa = max(ra[0], rb[0]) sb = min(ra[1], rb[1]) if sa < sb: return sa, sb else: return None def compare_range(a, astart, aend, b, bstart, bend): """Compare a[astart:aend] == b[bstart:bend], without slicing. """ if (aend-astart) != (bend-bstart): return False for ia, ib in zip(xrange(astart, aend), xrange(bstart, bend)): if a[ia] != b[ib]: return False else: return True class Merge3(object): """3-way merge of texts. Given BASE, OTHER, THIS, tries to produce a combined text incorporating the changes from both BASE->OTHER and BASE->THIS. All three will typically be sequences of lines.""" def __init__(self, base, a, b): self.base = base self.a = a self.b = b from difflib import SequenceMatcher self.a_ops = SequenceMatcher(None, base, a).get_opcodes() self.b_ops = SequenceMatcher(None, base, b).get_opcodes() def merge_lines(self, name_a=None, name_b=None, start_marker='<<<<<<<<', mid_marker='========', end_marker='>>>>>>>>', show_base=False): """Return merge in cvs-like form. """ if name_a: start_marker = start_marker + ' ' + name_a if name_b: end_marker = end_marker + ' ' + name_b for t in self.merge_regions(): what = t[0] if what == 'unchanged': for i in range(t[1], t[2]): yield self.base[i] elif what == 'a' or what == 'same': for i in range(t[1], t[2]): yield self.a[i] elif what == 'b': for i in range(t[1], t[2]): yield self.b[i] elif what == 'conflict': yield start_marker + '\n' for i in range(t[3], t[4]): yield self.a[i] yield mid_marker + '\n' for i in range(t[5], t[6]): yield self.b[i] yield end_marker + '\n' else: raise ValueError(what) def merge_annotated(self): """Return merge with conflicts, showing origin of lines. Most useful for debugging merge. """ for t in self.merge_regions(): what = t[0] if what == 'unchanged': for i in range(t[1], t[2]): yield 'u | ' + self.base[i] elif what == 'a' or what == 'same': for i in range(t[1], t[2]): yield what[0] + ' | ' + self.a[i] elif what == 'b': for i in range(t[1], t[2]): yield 'b | ' + self.b[i] elif what == 'conflict': yield '<<<<\n' for i in range(t[3], t[4]): yield 'A | ' + self.a[i] yield '----\n' for i in range(t[5], t[6]): yield 'B | ' + self.b[i] yield '>>>>\n' else: raise ValueError(what) def merge_groups(self): """Yield sequence of line groups. Each one is a tuple: 'unchanged', lines Lines unchanged from base 'a', lines Lines taken from a 'same', lines Lines taken from a (and equal to b) 'b', lines Lines taken from b 'conflict', base_lines, a_lines, b_lines Lines from base were changed to either a or b and conflict. """ for t in self.merge_regions(): what = t[0] if what == 'unchanged': yield what, self.base[t[1]:t[2]] elif what == 'a' or what == 'same': yield what, self.a[t[1]:t[2]] elif what == 'b': yield what, self.b[t[1]:t[2]] elif what == 'conflict': yield (what, self.base[t[1]:t[2]], self.a[t[3]:t[4]], self.b[t[5]:t[6]]) else: raise ValueError(what) def merge_regions(self): """Return sequences of matching and conflicting regions. This returns tuples, where the first value says what kind we have: 'unchanged', start, end Take a region of base[start:end] 'same', astart, aend b and a are different from base but give the same result 'a', start, end Non-clashing insertion from a[start:end] Method is as follows: The two sequences align only on regions which match the base and both descendents. These are found by doing a two-way diff of each one against the base, and then finding the intersections between those regions. These "sync regions" are by definition unchanged in both and easily dealt with. The regions in between can be in any of three cases: conflicted, or changed on only one side. """ # section a[0:ia] has been disposed of, etc iz = ia = ib = 0 for zmatch, zend, amatch, aend, bmatch, bend in self.find_sync_regions(): #print 'match base [%d:%d]' % (zmatch, zend) matchlen = zend - zmatch assert matchlen >= 0 assert matchlen == (aend - amatch) assert matchlen == (bend - bmatch) len_a = amatch - ia len_b = bmatch - ib len_base = zmatch - iz assert len_a >= 0 assert len_b >= 0 assert len_base >= 0 #print 'unmatched a=%d, b=%d' % (len_a, len_b) if len_a or len_b: # try to avoid actually slicing the lists equal_a = compare_range(self.a, ia, amatch, self.base, iz, zmatch) equal_b = compare_range(self.b, ib, bmatch, self.base, iz, zmatch) same = compare_range(self.a, ia, amatch, self.b, ib, bmatch) if same: yield 'same', ia, amatch elif equal_a and not equal_b: yield 'b', ib, bmatch elif equal_b and not equal_a: yield 'a', ia, amatch elif not equal_a and not equal_b: yield 'conflict', iz, zmatch, ia, amatch, ib, bmatch else: raise AssertionError("can't handle a=b=base but unmatched") ia = amatch ib = bmatch iz = zmatch # if the same part of the base was deleted on both sides # that's OK, we can just skip it. if matchlen > 0: assert ia == amatch assert ib == bmatch assert iz == zmatch yield 'unchanged', zmatch, zend iz = zend ia = aend ib = bend def find_sync_regions(self): """Return a list of sync regions, where both descendents match the base. Generates a list of (base1, base2, a1, a2, b1, b2). There is always a zero-length sync region at the end of all the files. """ from difflib import SequenceMatcher ia = ib = 0 amatches = SequenceMatcher(None, self.base, self.a).get_matching_blocks() bmatches = SequenceMatcher(None, self.base, self.b).get_matching_blocks() len_a = len(amatches) len_b = len(bmatches) sl = [] while ia < len_a and ib < len_b: abase, amatch, alen = amatches[ia] bbase, bmatch, blen = bmatches[ib] # there is an unconflicted block at i; how long does it # extend? until whichever one ends earlier. i = intersect((abase, abase+alen), (bbase, bbase+blen)) if i: intbase = i[0] intend = i[1] intlen = intend - intbase # found a match of base[i[0], i[1]]; this may be less than # the region that matches in either one assert intlen <= alen assert intlen <= blen assert abase <= intbase assert bbase <= intbase asub = amatch + (intbase - abase) bsub = bmatch + (intbase - bbase) aend = asub + intlen bend = bsub + intlen assert self.base[intbase:intend] == self.a[asub:aend], \ (self.base[intbase:intend], self.a[asub:aend]) assert self.base[intbase:intend] == self.b[bsub:bend] sl.append((intbase, intend, asub, aend, bsub, bend)) # advance whichever one ends first in the base text if (abase + alen) < (bbase + blen): ia += 1 else: ib += 1 intbase = len(self.base) abase = len(self.a) bbase = len(self.b) sl.append((intbase, intbase, abase, abase, bbase, bbase)) return sl def find_unconflicted(self): """Return a list of ranges in base that are not conflicted.""" from difflib import SequenceMatcher import re # don't sync-up on lines containing only blanks or pounds junk_re = re.compile(r'^[ \t#]*$') am = SequenceMatcher(junk_re.match, self.base, self.a).get_matching_blocks() bm = SequenceMatcher(junk_re.match, self.base, self.b).get_matching_blocks() unc = [] while am and bm: # there is an unconflicted block at i; how long does it # extend? until whichever one ends earlier. a1 = am[0][0] a2 = a1 + am[0][2] b1 = bm[0][0] b2 = b1 + bm[0][2] i = intersect((a1, a2), (b1, b2)) if i: unc.append(i) if a2 < b2: del am[0] else: del bm[0] return unc def main(argv): # as for diff3 and meld the syntax is "MINE BASE OTHER" a = file(argv[1], 'rt').readlines() base = file(argv[2], 'rt').readlines() b = file(argv[3], 'rt').readlines() m3 = Merge3(base, a, b) #for sr in m3.find_sync_regions(): # print sr # sys.stdout.writelines(m3.merge_lines(name_a=argv[1], name_b=argv[3])) sys.stdout.writelines(m3.merge_annotated()) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) commit refs/heads/master mark :840 committer Martin Pool 1120623614 +1000 data 55 - delay import of subprocess module until it's needed. from :839 M 644 inline bzrlib/selftest/__init__.py data 9664 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase class CommandFailed(Exception): pass class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr, so that we can run it # through a specified Python intepreter OVERRIDE_PYTHON = None # to run with alternative python 'python' BZRPATH = 'bzr' _log_buf = "" def formcmd(self, cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = self.BZRPATH if self.OVERRIDE_PYTHON: cmd.insert(0, self.OVERRIDE_PYTHON) self.log('$ %r' % cmd) return cmd def runcmd(self, cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" try: import shutil from subprocess import call except ImportError, e: import sys sys.stderr.write("testbzr: sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") raise cmd = self.formcmd(cmd) self.log('$ ' + ' '.join(cmd)) actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(self, cmd, retcode=0): """Run a command and return its output""" try: import shutil from subprocess import Popen, PIPE except ImportError, e: import sys sys.stderr.write("testbzr: sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") raise cmd = self.formcmd(cmd) child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG) outd, errd = child.communicate() self.log(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def build_tree(self, shape): """Build a test tree according to a pattern. shape is a sequence of file specifications. If the final character is '/', a directory is created. This doesn't add anything to a branch. """ # XXX: It's OK to just create them using forward slashes on windows? import os for name in shape: assert isinstance(name, basestring) if name[-1] == '/': os.mkdir(name[:-1]) else: f = file(name, 'wt') print >>f, "contents of", name f.close() def log(self, msg): """Log a message to a progress file""" self._log_buf = self._log_buf + str(msg) + '\n' print >>self.TEST_LOG, msg def check_inventory_shape(self, inv, shape): """ Compare an inventory to a list of expected names. Fail if they are not precisely equal. """ extras = [] shape = list(shape) # copy for path, ie in inv.entries(): name = path.replace('\\', '/') if ie.kind == 'dir': name = name + '/' if name in shape: shape.remove(name) else: extras.append(name) if shape: self.fail("expected paths not found in inventory: %r" % shape) if extras: self.fail("unexpected paths found in inventory: %r" % extras) def check_file_contents(self, filename, expect): self.log("check contents of file %s" % filename) contents = file(filename, 'r').read() if contents != expect: self.log("expected: %r" % expected) self.log("actually: %r" % contents) self.fail("contents of %s not as expected") class InTempDir(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.test_dir = os.path.join(self.TEST_ROOT, self.__class__.__name__) os.mkdir(self.test_dir) os.chdir(self.test_dir) def tearDown(self): import os os.chdir(self.TEST_ROOT) class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ def __init__(self, out): self.out = out TestResult.__init__(self) def startTest(self, test): # TODO: Maybe show test.shortDescription somewhere? print >>self.out, '%-60.60s' % test.id(), self.out.flush() TestResult.startTest(self, test) def stopTest(self, test): # print TestResult.stopTest(self, test) def addError(self, test, err): print >>self.out, 'ERROR' TestResult.addError(self, test, err) _show_test_failure('error', test, err, self.out) def addFailure(self, test, err): print >>self.out, 'FAILURE' TestResult.addFailure(self, test, err) _show_test_failure('failure', test, err, self.out) def addSuccess(self, test): print >>self.out, 'OK' TestResult.addSuccess(self, test) def selftest(): from unittest import TestLoader, TestSuite import bzrlib, bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, bzrlib.commands import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning import bzrlib.selftest.testmerge3 import bzrlib.merge_core from doctest import DocTestSuite import os import shutil import time import sys TestBase.BZRPATH = os.path.join(os.path.realpath(os.path.dirname(bzrlib.__path__[0])), 'bzr') print '%-30s %s' % ('bzr binary', TestBase.BZRPATH) _setup_test_log() _setup_test_dir() print suite = TestSuite() tl = TestLoader() # should also test bzrlib.merge_core, but they seem to be out of date with # the code. for m in bzrlib.selftest.whitebox, \ bzrlib.selftest.versioning, \ bzrlib.selftest.testmerge3: suite.addTest(tl.loadTestsFromModule(m)) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands, \ bzrlib.merge3: suite.addTest(DocTestSuite(m)) suite.addTest(bzrlib.selftest.blackbox.suite()) # save stdout & stderr so there's no leakage from code-under-test real_stdout = sys.stdout real_stderr = sys.stderr sys.stdout = sys.stderr = TestBase.TEST_LOG try: result = _MyResult(real_stdout) suite.run(result) finally: sys.stdout = real_stdout sys.stderr = real_stderr _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os log_filename = os.path.abspath('testbzr.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "bzr tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_ROOT = os.path.abspath("testbzr.tmp") print '%-30s %s' % ('running tests in', TestBase.TEST_ROOT) if os.path.exists(TestBase.TEST_ROOT): shutil.rmtree(TestBase.TEST_ROOT) os.mkdir(TestBase.TEST_ROOT) os.chdir(TestBase.TEST_ROOT) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_ROOT, '.bzr')) def _show_results(result): print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, exc_info, out): from traceback import print_exception print >>out, '-' * 60 print >>out, case desc = case.shortDescription() if desc: print >>out, ' (%s)' % desc print_exception(exc_info[0], exc_info[1], exc_info[2], None, out) if isinstance(case, TestBase): print >>out print >>out, 'log from this test:' print >>out, case._log_buf print >>out, '-' * 60 commit refs/heads/master mark :841 committer Martin Pool 1120624852 +1000 data 82 - Start splitting bzr-independent parts of the test framework into testsweet.py from :840 M 644 inline testsweet.py data 9392 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Enhanced layer on unittest. This does several things: * nicer reporting as tests run * test code can log messages into a buffer that is recorded to disk and displayed if the test fails * tests can be run in a separate directory, which is useful for code that wants to create files * utilities to run external commands and check their return code and/or output Test cases should normally subclass TestBase. The test runner should call runsuite(). This is meant to become independent of bzr, though that's not quite true yet. """ from unittest import TestResult, TestCase def _need_subprocess(): sys.stderr.write("sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") class CommandFailed(Exception): pass class TestSkipped(Exception): """Indicates that a test was intentionally skipped, rather than failing.""" # XXX: Not used yet class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr, so that we can run it # through a specified Python intepreter OVERRIDE_PYTHON = None # to run with alternative python 'python' BZRPATH = 'bzr' _log_buf = "" def setUp(self): super(TestBase, self).setUp() self.log("%s setup" % self.id()) def tearDown(self): super(TestBase, self).tearDown() self.log("%s teardown" % self.id()) self.log('') def formcmd(self, cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = self.BZRPATH if self.OVERRIDE_PYTHON: cmd.insert(0, self.OVERRIDE_PYTHON) self.log('$ %r' % cmd) return cmd def runcmd(self, cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" try: import shutil from subprocess import call except ImportError, e: _need_subprocess() raise cmd = self.formcmd(cmd) self.log('$ ' + ' '.join(cmd)) actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(self, cmd, retcode=0): """Run a command and return its output""" try: import shutil from subprocess import Popen, PIPE except ImportError, e: _need_subprocess() raise cmd = self.formcmd(cmd) child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG) outd, errd = child.communicate() self.log(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def build_tree(self, shape): """Build a test tree according to a pattern. shape is a sequence of file specifications. If the final character is '/', a directory is created. This doesn't add anything to a branch. """ # XXX: It's OK to just create them using forward slashes on windows? import os for name in shape: assert isinstance(name, basestring) if name[-1] == '/': os.mkdir(name[:-1]) else: f = file(name, 'wt') print >>f, "contents of", name f.close() def log(self, msg): """Log a message to a progress file""" self._log_buf = self._log_buf + str(msg) + '\n' print >>self.TEST_LOG, msg def check_inventory_shape(self, inv, shape): """ Compare an inventory to a list of expected names. Fail if they are not precisely equal. """ extras = [] shape = list(shape) # copy for path, ie in inv.entries(): name = path.replace('\\', '/') if ie.kind == 'dir': name = name + '/' if name in shape: shape.remove(name) else: extras.append(name) if shape: self.fail("expected paths not found in inventory: %r" % shape) if extras: self.fail("unexpected paths found in inventory: %r" % extras) def check_file_contents(self, filename, expect): self.log("check contents of file %s" % filename) contents = file(filename, 'r').read() if contents != expect: self.log("expected: %r" % expected) self.log("actually: %r" % contents) self.fail("contents of %s not as expected") class InTempDir(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.test_dir = os.path.join(self.TEST_ROOT, self.__class__.__name__) os.mkdir(self.test_dir) os.chdir(self.test_dir) def tearDown(self): import os os.chdir(self.TEST_ROOT) class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ def __init__(self, out): self.out = out TestResult.__init__(self) def startTest(self, test): # TODO: Maybe show test.shortDescription somewhere? print >>self.out, '%-60.60s' % test.id(), self.out.flush() TestResult.startTest(self, test) def stopTest(self, test): # print TestResult.stopTest(self, test) def addError(self, test, err): print >>self.out, 'ERROR' TestResult.addError(self, test, err) _show_test_failure('error', test, err, self.out) def addFailure(self, test, err): print >>self.out, 'FAILURE' TestResult.addFailure(self, test, err) _show_test_failure('failure', test, err, self.out) def addSuccess(self, test): print >>self.out, 'OK' TestResult.addSuccess(self, test) def run_suite(suite, name="test"): import os import shutil import time import sys _setup_test_log(name) _setup_test_dir(name) print # save stdout & stderr so there's no leakage from code-under-test real_stdout = sys.stdout real_stderr = sys.stderr sys.stdout = sys.stderr = TestBase.TEST_LOG try: result = _MyResult(real_stdout) suite.run(result) finally: sys.stdout = real_stdout sys.stderr = real_stderr _show_results(result) return result.wasSuccessful() def _setup_test_log(name): import time import os log_filename = os.path.abspath(name + '.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(name): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_ROOT = os.path.abspath(name + '.tmp') print '%-30s %s' % ('running tests in', TestBase.TEST_ROOT) if os.path.exists(TestBase.TEST_ROOT): shutil.rmtree(TestBase.TEST_ROOT) os.mkdir(TestBase.TEST_ROOT) os.chdir(TestBase.TEST_ROOT) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_ROOT, '.bzr')) def _show_results(result): print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, exc_info, out): from traceback import print_exception print >>out, '-' * 60 print >>out, case desc = case.shortDescription() if desc: print >>out, ' (%s)' % desc print_exception(exc_info[0], exc_info[1], exc_info[2], None, out) if isinstance(case, TestBase): print >>out print >>out, 'log from this test:' print >>out, case._log_buf print >>out, '-' * 60 M 644 inline bzrlib/selftest/__init__.py data 1976 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from testsweet import TestBase, run_suite, InTempDir def selftest(): from unittest import TestLoader, TestSuite import bzrlib, bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, bzrlib.commands import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning import bzrlib.selftest.testmerge3 import bzrlib.merge_core from doctest import DocTestSuite import os import shutil import time import sys TestBase.BZRPATH = os.path.join(os.path.realpath(os.path.dirname(bzrlib.__path__[0])), 'bzr') print '%-30s %s' % ('bzr binary', TestBase.BZRPATH) print suite = TestSuite() tl = TestLoader() # should also test bzrlib.merge_core, but they seem to be out of date with # the code. for m in bzrlib.selftest.whitebox, \ bzrlib.selftest.versioning, \ bzrlib.selftest.testmerge3: suite.addTest(tl.loadTestsFromModule(m)) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands, \ bzrlib.merge3: suite.addTest(DocTestSuite(m)) suite.addTest(bzrlib.selftest.blackbox.suite()) return run_suite(suite) commit refs/heads/master mark :842 committer Martin Pool 1120625121 +1000 data 59 - don't say runit when running tests under python2.3 dammit from :841 M 644 inline testsweet.py data 9551 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Enhanced layer on unittest. This does several things: * nicer reporting as tests run * test code can log messages into a buffer that is recorded to disk and displayed if the test fails * tests can be run in a separate directory, which is useful for code that wants to create files * utilities to run external commands and check their return code and/or output Test cases should normally subclass TestBase. The test runner should call runsuite(). This is meant to become independent of bzr, though that's not quite true yet. """ from unittest import TestResult, TestCase def _need_subprocess(): sys.stderr.write("sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") class CommandFailed(Exception): pass class TestSkipped(Exception): """Indicates that a test was intentionally skipped, rather than failing.""" # XXX: Not used yet class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr, so that we can run it # through a specified Python intepreter OVERRIDE_PYTHON = None # to run with alternative python 'python' BZRPATH = 'bzr' _log_buf = "" def setUp(self): super(TestBase, self).setUp() self.log("%s setup" % self.id()) def tearDown(self): super(TestBase, self).tearDown() self.log("%s teardown" % self.id()) self.log('') def formcmd(self, cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = self.BZRPATH if self.OVERRIDE_PYTHON: cmd.insert(0, self.OVERRIDE_PYTHON) self.log('$ %r' % cmd) return cmd def runcmd(self, cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" try: import shutil from subprocess import call except ImportError, e: _need_subprocess() raise cmd = self.formcmd(cmd) self.log('$ ' + ' '.join(cmd)) actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(self, cmd, retcode=0): """Run a command and return its output""" try: import shutil from subprocess import Popen, PIPE except ImportError, e: _need_subprocess() raise cmd = self.formcmd(cmd) child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG) outd, errd = child.communicate() self.log(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def build_tree(self, shape): """Build a test tree according to a pattern. shape is a sequence of file specifications. If the final character is '/', a directory is created. This doesn't add anything to a branch. """ # XXX: It's OK to just create them using forward slashes on windows? import os for name in shape: assert isinstance(name, basestring) if name[-1] == '/': os.mkdir(name[:-1]) else: f = file(name, 'wt') print >>f, "contents of", name f.close() def log(self, msg): """Log a message to a progress file""" self._log_buf = self._log_buf + str(msg) + '\n' print >>self.TEST_LOG, msg def check_inventory_shape(self, inv, shape): """ Compare an inventory to a list of expected names. Fail if they are not precisely equal. """ extras = [] shape = list(shape) # copy for path, ie in inv.entries(): name = path.replace('\\', '/') if ie.kind == 'dir': name = name + '/' if name in shape: shape.remove(name) else: extras.append(name) if shape: self.fail("expected paths not found in inventory: %r" % shape) if extras: self.fail("unexpected paths found in inventory: %r" % extras) def check_file_contents(self, filename, expect): self.log("check contents of file %s" % filename) contents = file(filename, 'r').read() if contents != expect: self.log("expected: %r" % expected) self.log("actually: %r" % contents) self.fail("contents of %s not as expected") class InTempDir(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.test_dir = os.path.join(self.TEST_ROOT, self.__class__.__name__) os.mkdir(self.test_dir) os.chdir(self.test_dir) def tearDown(self): import os os.chdir(self.TEST_ROOT) class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ def __init__(self, out): self.out = out TestResult.__init__(self) def startTest(self, test): # TODO: Maybe show test.shortDescription somewhere? what = test.id() # python2.3 has the bad habit of just "runit" for doctests if what == 'runit': what = test.shortDescription() print >>self.out, '%-60.60s' % what, self.out.flush() TestResult.startTest(self, test) def stopTest(self, test): # print TestResult.stopTest(self, test) def addError(self, test, err): print >>self.out, 'ERROR' TestResult.addError(self, test, err) _show_test_failure('error', test, err, self.out) def addFailure(self, test, err): print >>self.out, 'FAILURE' TestResult.addFailure(self, test, err) _show_test_failure('failure', test, err, self.out) def addSuccess(self, test): print >>self.out, 'OK' TestResult.addSuccess(self, test) def run_suite(suite, name="test"): import os import shutil import time import sys _setup_test_log(name) _setup_test_dir(name) print # save stdout & stderr so there's no leakage from code-under-test real_stdout = sys.stdout real_stderr = sys.stderr sys.stdout = sys.stderr = TestBase.TEST_LOG try: result = _MyResult(real_stdout) suite.run(result) finally: sys.stdout = real_stdout sys.stderr = real_stderr _show_results(result) return result.wasSuccessful() def _setup_test_log(name): import time import os log_filename = os.path.abspath(name + '.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(name): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_ROOT = os.path.abspath(name + '.tmp') print '%-30s %s' % ('running tests in', TestBase.TEST_ROOT) if os.path.exists(TestBase.TEST_ROOT): shutil.rmtree(TestBase.TEST_ROOT) os.mkdir(TestBase.TEST_ROOT) os.chdir(TestBase.TEST_ROOT) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_ROOT, '.bzr')) def _show_results(result): print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, exc_info, out): from traceback import print_exception print >>out, '-' * 60 print >>out, case desc = case.shortDescription() if desc: print >>out, ' (%s)' % desc print_exception(exc_info[0], exc_info[1], exc_info[2], None, out) if isinstance(case, TestBase): print >>out print >>out, 'log from this test:' print >>out, case._log_buf print >>out, '-' * 60 commit refs/heads/master mark :843 committer Martin Pool 1120627469 +1000 data 171 - workaround for flaky TestLoader in python2.3 Now we just list all the test classes manually -- a bit of a pain, but does mean we can run the simpler tests first. from :842 M 644 inline bzrlib/selftest/__init__.py data 2103 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from testsweet import TestBase, run_suite, InTempDir def selftest(): from unittest import TestLoader, TestSuite import bzrlib, bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, bzrlib.commands import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning import bzrlib.selftest.testmerge3 import bzrlib.merge_core from doctest import DocTestSuite import os import shutil import time import sys TestBase.BZRPATH = os.path.join(os.path.realpath(os.path.dirname(bzrlib.__path__[0])), 'bzr') print '%-30s %s' % ('bzr binary', TestBase.BZRPATH) print suite = TestSuite() # should also test bzrlib.merge_core, but they seem to be out of date with # the code. # python2.3's TestLoader() doesn't seem to work well; don't know why for m in (bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, bzrlib.commands, bzrlib.merge3): suite.addTest(DocTestSuite(m)) for cl in (bzrlib.selftest.whitebox.TEST_CLASSES + bzrlib.selftest.versioning.TEST_CLASSES + bzrlib.selftest.testmerge3.TEST_CLASSES + bzrlib.selftest.blackbox.TEST_CLASSES): suite.addTest(cl()) return run_suite(suite) M 644 inline bzrlib/selftest/blackbox.py data 11526 # Copyright (C) 2005 by Canonical Ltd # -*- coding: utf-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Black-box tests for bzr. These check that it behaves properly when it's invoked through the regular command-line interface. This always reinvokes bzr through a new Python interpreter, which is a bit inefficient but arguably tests in a way more representative of how it's normally invoked. """ # this code was previously in testbzr from unittest import TestCase from bzrlib.selftest import TestBase, InTempDir class TestVersion(TestBase): def runTest(self): # output is intentionally passed through to stdout so that we # can see the version being tested self.runcmd(['bzr', 'version']) class HelpCommands(TestBase): def runTest(self): self.runcmd('bzr --help') self.runcmd('bzr help') self.runcmd('bzr help commands') self.runcmd('bzr help help') self.runcmd('bzr commit -h') class InitBranch(InTempDir): def runTest(self): import os self.runcmd(['bzr', 'init']) class UserIdentity(InTempDir): def runTest(self): # this should always identify something, if only "john@localhost" self.runcmd("bzr whoami") self.runcmd("bzr whoami --email") self.assertEquals(self.backtick("bzr whoami --email").count('@'), 1) class InvalidCommands(InTempDir): def runTest(self): self.runcmd("bzr pants", retcode=1) self.runcmd("bzr --pants off", retcode=1) self.runcmd("bzr diff --message foo", retcode=1) class OldTests(InTempDir): # old tests moved from ./testbzr def runTest(self): from os import chdir, mkdir from os.path import exists import os runcmd = self.runcmd backtick = self.backtick progress = self.log progress("basic branch creation") runcmd(['mkdir', 'branch1']) chdir('branch1') runcmd('bzr init') self.assertEquals(backtick('bzr root').rstrip(), os.path.join(self.test_dir, 'branch1')) progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") self.assertEquals(out, 'test.txt\n') out = backtick("bzr status") assert out == 'unknown:\n test.txt\n' out = backtick("bzr status --all") assert out == "unknown:\n test.txt\n" out = backtick("bzr status test.txt --all") assert out == "unknown:\n test.txt\n" f = file('test2.txt', 'wt') f.write('goodbye cruel world...\n') f.close() out = backtick("bzr status test.txt") assert out == "unknown:\n test.txt\n" out = backtick("bzr status") assert out == ("unknown:\n" " test.txt\n" " test2.txt\n") os.unlink('test2.txt') progress("command aliases") out = backtick("bzr st --all") assert out == ("unknown:\n" " test.txt\n") out = backtick("bzr stat") assert out == ("unknown:\n" " test.txt\n") progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == ("added:\n" " test.txt\n") progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == os.path.join("sub2", "hello.txt\n") assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) chdir('sub1/sub2') self.assertEquals(backtick('bzr root')[:-1], os.path.join(self.test_dir, 'branch1')) runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') runcmd(['bzr', 'commit', '-m', 'move to parent directory']) chdir('..') assert backtick('bzr relpath sub2/hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') runcmd('bzr log') runcmd('bzr log -v') progress("file with spaces in name") mkdir('sub directory') file('sub directory/file with spaces ', 'wt').write('see how this works\n') runcmd('bzr add .') runcmd('bzr diff') runcmd('bzr commit -m add-spaces') runcmd('bzr check') runcmd('bzr log') runcmd('bzr log --forward') runcmd('bzr info') chdir('..') chdir('..') progress('branch') # Can't create a branch if it already exists runcmd('bzr branch branch1', retcode=1) # Can't create a branch if its parent doesn't exist runcmd('bzr branch /unlikely/to/exist', retcode=1) runcmd('bzr branch branch1 branch2') progress("pull") chdir('branch1') runcmd('bzr pull', retcode=1) runcmd('bzr pull ../branch2') chdir('.bzr') runcmd('bzr pull') runcmd('bzr commit -m empty') runcmd('bzr pull') chdir('../../branch2') runcmd('bzr pull') runcmd('bzr commit -m empty') chdir('../branch1') runcmd('bzr commit -m empty') runcmd('bzr pull', retcode=1) chdir ('..') progress('status after remove') mkdir('status-after-remove') # see mail from William Dodé, 2005-05-25 # $ bzr init; touch a; bzr add a; bzr commit -m "add a" # * looking for changes... # added a # * commited r1 # $ bzr remove a # $ bzr status # bzr: local variable 'kind' referenced before assignment # at /vrac/python/bazaar-ng/bzrlib/diff.py:286 in compare_trees() # see ~/.bzr.log for debug information chdir('status-after-remove') runcmd('bzr init') file('a', 'w').write('foo') runcmd('bzr add a') runcmd(['bzr', 'commit', '-m', 'add a']) runcmd('bzr remove a') runcmd('bzr status') chdir('..') progress('ignore patterns') mkdir('ignorebranch') chdir('ignorebranch') runcmd('bzr init') assert backtick('bzr unknowns') == '' file('foo.tmp', 'wt').write('tmp files are ignored') assert backtick('bzr unknowns') == '' file('foo.c', 'wt').write('int main() {}') assert backtick('bzr unknowns') == 'foo.c\n' runcmd('bzr add foo.c') assert backtick('bzr unknowns') == '' # 'ignore' works when creating the .bzignore file file('foo.blah', 'wt').write('blah') assert backtick('bzr unknowns') == 'foo.blah\n' runcmd('bzr ignore *.blah') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\n' # 'ignore' works when then .bzrignore file already exists file('garh', 'wt').write('garh') assert backtick('bzr unknowns') == 'garh\n' runcmd('bzr ignore garh') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\ngarh\n' chdir('..') progress("recursive and non-recursive add") mkdir('no-recurse') chdir('no-recurse') runcmd('bzr init') mkdir('foo') fp = os.path.join('foo', 'test.txt') f = file(fp, 'w') f.write('hello!\n') f.close() runcmd('bzr add --no-recurse foo') runcmd('bzr file-id foo') runcmd('bzr file-id ' + fp, 1) # not versioned yet runcmd('bzr commit -m add-dir-only') runcmd('bzr file-id ' + fp, 1) # still not versioned runcmd('bzr add foo') runcmd('bzr file-id ' + fp) runcmd('bzr commit -m add-sub-file') chdir('..') class RevertCommand(InTempDir): def runTest(self): self.runcmd('bzr init') file('hello', 'wt').write('foo') self.runcmd('bzr add hello') self.runcmd('bzr commit -m setup hello') file('hello', 'wt').write('bar') self.runcmd('bzr revert hello') self.check_file_contents('hello', 'foo') # lists all tests from this module in the best order to run them. we # do it this way rather than just discovering them all because it # allows us to test more basic functions first where failures will be # easiest to understand. TEST_CLASSES = [TestVersion, InitBranch, HelpCommands, UserIdentity, InvalidCommands, RevertCommand, OldTests, ] M 644 inline bzrlib/selftest/testmerge3.py data 10599 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from bzrlib.selftest import InTempDir, TestBase from bzrlib.merge3 import Merge3 class NoChanges(TestBase): """No conflicts because nothing changed""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', 'bbb'], ['aaa', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0, 2, 0, 2, 0, 2), (2,2, 2,2, 2,2)]) self.assertEquals(list(m3.merge_regions()), [('unchanged', 0, 2)]) self.assertEquals(list(m3.merge_groups()), [('unchanged', ['aaa', 'bbb'])]) class FrontInsert(TestBase): def runTest(self): m3 = Merge3(['zz'], ['aaa', 'bbb', 'zz'], ['zz']) # todo: should use a sentinal at end as from get_matching_blocks # to match without zz self.assertEquals(list(m3.find_sync_regions()), [(0,1, 2,3, 0,1), (1,1, 3,3, 1,1),]) self.assertEquals(list(m3.merge_regions()), [('a', 0, 2), ('unchanged', 0, 1)]) self.assertEquals(list(m3.merge_groups()), [('a', ['aaa', 'bbb']), ('unchanged', ['zz'])]) class NullInsert(TestBase): def runTest(self): m3 = Merge3([], ['aaa', 'bbb'], []) # todo: should use a sentinal at end as from get_matching_blocks # to match without zz self.assertEquals(list(m3.find_sync_regions()), [(0,0, 2,2, 0,0)]) self.assertEquals(list(m3.merge_regions()), [('a', 0, 2)]) self.assertEquals(list(m3.merge_lines()), ['aaa', 'bbb']) class NoConflicts(TestBase): """No conflicts because only one side changed""" def runTest(self): m3 = Merge3(['aaa', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (1, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (1,2, 2,3, 1,2), (2,2, 3,3, 2,2),]) self.assertEquals(list(m3.merge_regions()), [('unchanged', 0, 1), ('a', 1, 2), ('unchanged', 1, 2),]) class AppendA(TestBase): def runTest(self): m3 = Merge3(['aaa\n', 'bbb\n'], ['aaa\n', 'bbb\n', '222\n'], ['aaa\n', 'bbb\n']) self.assertEquals(''.join(m3.merge_lines()), 'aaa\nbbb\n222\n') class AppendB(TestBase): def runTest(self): m3 = Merge3(['aaa\n', 'bbb\n'], ['aaa\n', 'bbb\n'], ['aaa\n', 'bbb\n', '222\n']) self.assertEquals(''.join(m3.merge_lines()), 'aaa\nbbb\n222\n') class AppendAgreement(TestBase): def runTest(self): m3 = Merge3(['aaa\n', 'bbb\n'], ['aaa\n', 'bbb\n', '222\n'], ['aaa\n', 'bbb\n', '222\n']) self.assertEquals(''.join(m3.merge_lines()), 'aaa\nbbb\n222\n') class AppendClash(TestBase): def runTest(self): m3 = Merge3(['aaa\n', 'bbb\n'], ['aaa\n', 'bbb\n', '222\n'], ['aaa\n', 'bbb\n', '333\n']) ml = m3.merge_lines(name_a='a', name_b='b', start_marker='<<', mid_marker='--', end_marker='>>') self.assertEquals(''.join(ml), '''\ aaa bbb << a 222 -- 333 >> b ''') class InsertAgreement(TestBase): def runTest(self): m3 = Merge3(['aaa\n', 'bbb\n'], ['aaa\n', '222\n', 'bbb\n'], ['aaa\n', '222\n', 'bbb\n']) ml = m3.merge_lines(name_a='a', name_b='b', start_marker='<<', mid_marker='--', end_marker='>>') self.assertEquals(''.join(m3.merge_lines()), 'aaa\n222\nbbb\n') class InsertClash(TestBase): """Both try to insert lines in the same place.""" def runTest(self): m3 = Merge3(['aaa\n', 'bbb\n'], ['aaa\n', '111\n', 'bbb\n'], ['aaa\n', '222\n', 'bbb\n']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (1, 2)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (1,2, 2,3, 2,3), (2,2, 3,3, 3,3),]) self.assertEquals(list(m3.merge_regions()), [('unchanged', 0,1), ('conflict', 1,1, 1,2, 1,2), ('unchanged', 1,2)]) self.assertEquals(list(m3.merge_groups()), [('unchanged', ['aaa\n']), ('conflict', [], ['111\n'], ['222\n']), ('unchanged', ['bbb\n']), ]) ml = m3.merge_lines(name_a='a', name_b='b', start_marker='<<', mid_marker='--', end_marker='>>') self.assertEquals(''.join(ml), '''aaa << a 111 -- 222 >> b bbb ''') class ReplaceClash(TestBase): """Both try to insert lines in the same place.""" def runTest(self): m3 = Merge3(['aaa', '000', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', '222', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (2, 3)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (2,3, 2,3, 2,3), (3,3, 3,3, 3,3),]) class ReplaceMulti(TestBase): """Replacement with regions of different size.""" def runTest(self): m3 = Merge3(['aaa', '000', '000', 'bbb'], ['aaa', '111', '111', '111', 'bbb'], ['aaa', '222', '222', '222', '222', 'bbb']) self.assertEquals(m3.find_unconflicted(), [(0, 1), (3, 4)]) self.assertEquals(list(m3.find_sync_regions()), [(0,1, 0,1, 0,1), (3,4, 4,5, 5,6), (4,4, 5,5, 6,6),]) def split_lines(t): from cStringIO import StringIO return StringIO(t).readlines() ############################################################ # test case from the gnu diffutils manual # common base TZU = split_lines(""" The Nameless is the origin of Heaven and Earth; The named is the mother of all things. Therefore let there always be non-being, so we may see their subtlety, And let there always be being, so we may see their outcome. The two are the same, But after they are produced, they have different names. They both may be called deep and profound. Deeper and more profound, The door of all subtleties! """) LAO = split_lines(""" The Way that can be told of is not the eternal Way; The name that can be named is not the eternal name. The Nameless is the origin of Heaven and Earth; The Named is the mother of all things. Therefore let there always be non-being, so we may see their subtlety, And let there always be being, so we may see their outcome. The two are the same, But after they are produced, they have different names. """) TAO = split_lines(""" The Way that can be told of is not the eternal Way; The name that can be named is not the eternal name. The Nameless is the origin of Heaven and Earth; The named is the mother of all things. Therefore let there always be non-being, so we may see their subtlety, And let there always be being, so we may see their result. The two are the same, But after they are produced, they have different names. -- The Way of Lao-Tzu, tr. Wing-tsit Chan """) MERGED_RESULT = split_lines(""" The Way that can be told of is not the eternal Way; The name that can be named is not the eternal name. The Nameless is the origin of Heaven and Earth; The Named is the mother of all things. Therefore let there always be non-being, so we may see their subtlety, And let there always be being, so we may see their result. The two are the same, But after they are produced, they have different names. <<<<<<<< LAO ======== -- The Way of Lao-Tzu, tr. Wing-tsit Chan >>>>>>>> TAO """) class MergePoem(TestBase): """Test case from diff3 manual""" def runTest(self): m3 = Merge3(TZU, LAO, TAO) ml = list(m3.merge_lines('LAO', 'TAO')) self.log('merge result:') self.log(''.join(ml)) self.assertEquals(ml, MERGED_RESULT) TEST_CLASSES = [ NoChanges, FrontInsert, NullInsert, NoConflicts, AppendA, AppendB, AppendAgreement, AppendClash, InsertAgreement, InsertClash, ReplaceClash, ReplaceMulti, MergePoem, ] M 644 inline bzrlib/selftest/versioning.py data 2142 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tests of simple versioning operations""" from bzrlib.selftest import InTempDir class Mkdir(InTempDir): def runTest(self): """Basic 'bzr mkdir' operation""" from bzrlib.commands import run_bzr import os run_bzr(['bzr', 'init']) run_bzr(['bzr', 'mkdir', 'foo']) self.assert_(os.path.isdir('foo')) self.assertRaises(OSError, run_bzr, ['bzr', 'mkdir', 'foo']) from bzrlib.diff import compare_trees, TreeDelta from bzrlib.branch import Branch b = Branch('.') delta = compare_trees(b.basis_tree(), b.working_tree()) self.assertEquals(len(delta.added), 1) self.assertEquals(delta.added[0][0], 'foo') self.failIf(delta.modified) class AddInUnversioned(InTempDir): def runTest(self): """Try to add a file in an unversioned directory. smart_add may eventually add the parent as necessary, but simple branch add doesn't do that. """ from bzrlib.branch import Branch import os from bzrlib.errors import NotVersionedError b = Branch('.', init=True) self.build_tree(['foo/', 'foo/hello']) self.assertRaises(NotVersionedError, b.add, 'foo/hello') TEST_CLASSES = [ Mkdir, AddInUnversioned, ] M 644 inline bzrlib/selftest/whitebox.py data 6655 #! /usr/bin/python import os import unittest from bzrlib.selftest import InTempDir, TestBase from bzrlib.branch import ScratchBranch, Branch from bzrlib.errors import NotBranchError, NotVersionedError class Unknowns(InTempDir): def runTest(self): b = Branch('.', init=True) self.build_tree(['hello.txt', 'hello.txt~']) self.assertEquals(list(b.unknowns()), ['hello.txt']) class ValidateRevisionId(TestBase): def runTest(self): from bzrlib.revision import validate_revision_id validate_revision_id('mbp@sourcefrog.net-20050311061123-96a255005c7c9dbe') self.assertRaises(ValueError, validate_revision_id, ' asdkjas') self.assertRaises(ValueError, validate_revision_id, 'mbp@sourcefrog.net-20050311061123-96a255005c7c9dbe\n') self.assertRaises(ValueError, validate_revision_id, ' mbp@sourcefrog.net-20050311061123-96a255005c7c9dbe') self.assertRaises(ValueError, validate_revision_id, 'Martin Pool -20050311061123-96a255005c7c9dbe') class PendingMerges(InTempDir): """Tracking pending-merged revisions.""" def runTest(self): b = Branch('.', init=True) self.assertEquals(b.pending_merges(), []) b.add_pending_merge('foo@azkhazan-123123-abcabc') self.assertEquals(b.pending_merges(), ['foo@azkhazan-123123-abcabc']) b.add_pending_merge('foo@azkhazan-123123-abcabc') self.assertEquals(b.pending_merges(), ['foo@azkhazan-123123-abcabc']) b.add_pending_merge('wibble@fofof--20050401--1928390812') self.assertEquals(b.pending_merges(), ['foo@azkhazan-123123-abcabc', 'wibble@fofof--20050401--1928390812']) b.commit("commit from base with two merges") rev = b.get_revision(b.revision_history()[0]) self.assertEquals(len(rev.parents), 2) self.assertEquals(rev.parents[0].revision_id, 'foo@azkhazan-123123-abcabc') self.assertEquals(rev.parents[1].revision_id, 'wibble@fofof--20050401--1928390812') # list should be cleared when we do a commit self.assertEquals(b.pending_merges(), []) class Revert(InTempDir): """Test selected-file revert""" def runTest(self): b = Branch('.', init=True) self.build_tree(['hello.txt']) file('hello.txt', 'w').write('initial hello') self.assertRaises(NotVersionedError, b.revert, ['hello.txt']) b.add(['hello.txt']) b.commit('create initial hello.txt') self.check_file_contents('hello.txt', 'initial hello') file('hello.txt', 'w').write('new hello') self.check_file_contents('hello.txt', 'new hello') # revert file modified since last revision b.revert(['hello.txt']) self.check_file_contents('hello.txt', 'initial hello') self.check_file_contents('hello.txt~', 'new hello') # reverting again clobbers the backup b.revert(['hello.txt']) self.check_file_contents('hello.txt', 'initial hello') self.check_file_contents('hello.txt~', 'initial hello') class RenameDirs(InTempDir): """Test renaming directories and the files within them.""" def runTest(self): b = Branch('.', init=True) self.build_tree(['dir/', 'dir/sub/', 'dir/sub/file']) b.add(['dir', 'dir/sub', 'dir/sub/file']) b.commit('create initial state') # TODO: lift out to a test helper that checks the shape of # an inventory revid = b.revision_history()[0] self.log('first revision_id is {%s}' % revid) inv = b.get_revision_inventory(revid) self.log('contents of inventory: %r' % inv.entries()) self.check_inventory_shape(inv, ['dir', 'dir/sub', 'dir/sub/file']) b.rename_one('dir', 'newdir') self.check_inventory_shape(b.inventory, ['newdir', 'newdir/sub', 'newdir/sub/file']) b.rename_one('newdir/sub', 'newdir/newsub') self.check_inventory_shape(b.inventory, ['newdir', 'newdir/newsub', 'newdir/newsub/file']) class BranchPathTestCase(TestBase): """test for branch path lookups Branch.relpath and bzrlib.branch._relpath do a simple but subtle job: given a path (either relative to cwd or absolute), work out if it is inside a branch and return the path relative to the base. """ def runTest(self): from bzrlib.branch import _relpath import tempfile, shutil savedir = os.getcwdu() dtmp = tempfile.mkdtemp() def rp(p): return _relpath(dtmp, p) try: # check paths inside dtmp while standing outside it self.assertEqual(rp(os.path.join(dtmp, 'foo')), 'foo') # root = nothing self.assertEqual(rp(dtmp), '') self.assertRaises(NotBranchError, rp, '/etc') # now some near-miss operations -- note that # os.path.commonprefix gets these wrong! self.assertRaises(NotBranchError, rp, dtmp.rstrip('\\/') + '2') self.assertRaises(NotBranchError, rp, dtmp.rstrip('\\/') + '2/foo') # now operations based on relpath of files in current # directory, or nearby os.chdir(dtmp) self.assertEqual(rp('foo/bar/quux'), 'foo/bar/quux') self.assertEqual(rp('foo'), 'foo') self.assertEqual(rp('./foo'), 'foo') self.assertEqual(rp(os.path.abspath('foo')), 'foo') self.assertRaises(NotBranchError, rp, '../foo') finally: os.chdir(savedir) shutil.rmtree(dtmp) TEST_CLASSES = [Unknowns, ValidateRevisionId, PendingMerges, Revert, RenameDirs, BranchPathTestCase, ] commit refs/heads/master mark :844 committer Martin Pool 1120627952 +1000 data 32 - clean up imports of statcache from :843 M 644 inline bzrlib/commands.py data 53579 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import BzrError, BzrCheckError, BzrCommandError from bzrlib.branch import find_branch from bzrlib import BZRDIR plugin_cmds = {} def register_command(cmd): "Utility function to help register a command" global plugin_cmds k = cmd.__name__ if k.startswith("cmd_"): k_unsquished = _unsquish_command_name(k) else: k_unsquished = k if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = cmd else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r' % sys.modules[cmd.__module__]) def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _get_cmd_dict(plugins_override=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v # If we didn't load plugins, the plugin_cmds dict will be empty if plugins_override: d.update(plugin_cmds) else: d2 = plugin_cmds.copy() d2.update(d) d = d2 return d def get_all_cmds(plugins_override=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(plugins_override=plugins_override).iteritems(): yield k,v def get_cmd_class(cmd, plugins_override=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(plugins_override=plugins_override) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() for opt in self.takes_options: if not opt in OPTIONS: raise BzrError("Unknown option '%s' returned by external command %s" % (opt, path)) # TODO: Is there any way to check takes_args is valid here? self.takes_args = pipe.readline().split() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-usage'" % path) pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-help'" % path) def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: optname = name.replace('_','-') value = kargs[name] if OPTIONS.has_key(optname): # it's an option opts.append('--%s' % optname) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = find_branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): from bzrlib.xml import pack_xml pack_xml(find_branch('.').get_revision(revision_id), sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print find_branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): from bzrlib.add import smart_add smart_add(file_list, verbose, not no_recurse) class cmd_mkdir(Command): """Create a new versioned directory. This is equivalent to creating the directory and then adding it. """ takes_args = ['dir+'] def run(self, dir_list): b = None for d in dir_list: os.mkdir(d) if not b: b = find_branch(d) b.add([d], verbose=True) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print find_branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = find_branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = find_branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = find_branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import tempfile from shutil import rmtree import errno br_to = find_branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if e.errno != errno.ENOENT: raise if location is None: if stored_loc is None: raise BzrCommandError("No pull location known or specified.") else: print "Using last location: %s" % stored_loc location = stored_loc cache_root = tempfile.mkdtemp() from bzrlib.branch import DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: from branch import find_cached_branch, DivergedBranches br_from = find_cached_branch(location, cache_root) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged." " Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") finally: rmtree(cache_root) class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. To retrieve the branch as of a particular revision, supply the --revision parameter, as in "branch foo/bar -r 5". """ takes_args = ['from_location', 'to_location?'] takes_options = ['revision'] def run(self, from_location, to_location=None, revision=None): import errno from bzrlib.merge import merge from bzrlib.branch import DivergedBranches, NoSuchRevision, \ find_cached_branch, Branch from shutil import rmtree from meta_store import CachedStore import tempfile cache_root = tempfile.mkdtemp() try: try: br_from = find_cached_branch(from_location, cache_root) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not' ' exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location.rstrip("/\\")) try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already' ' exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) try: br_to.update_revisions(br_from, stop_revision=revision) except NoSuchRevision: rmtree(to_location) msg = "The branch %s has no revision %d." % (from_location, revision) raise BzrCommandError(msg) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False, ignore_zero=True) from_location = pull_loc(br_from) br_to.controlfile("x-pull", "wb").write(from_location + "\n") finally: rmtree(cache_root) def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = find_branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = find_branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = find_branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: raise BzrError("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = find_branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: raise BzrError("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in find_branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in find_branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): from bzrlib.branch import Branch Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = find_branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = find_branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): from bzrlib.statcache import update_cache, SC_SHA1 b = find_branch('.') inv = b.read_working_inventory() sc = update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = find_branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision','long'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None, long=False): from bzrlib.branch import find_branch from bzrlib.log import log_formatter, show_log import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') if long: log_format = 'long' else: log_format = 'short' lf = log_formatter(log_format, show_ids=show_ids, to_file=outf, show_timezone=timezone) show_log(b, lf, file_id, verbose=verbose, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = find_branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = find_branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): from bzrlib.osutils import quotefn for f in find_branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = find_branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = find_branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print find_branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, exports to a directory (equivalent to --format=dir).""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format'] def run(self, dest, revision=None, format='dir'): b = find_branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest, format) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = find_branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit from bzrlib.osutils import get_text_message ## Warning: shadows builtin file() if not message and not file: import cStringIO stdout = sys.stdout catcher = cStringIO.StringIO() sys.stdout = catcher cmd_status({"file_list":selected_list}, {}) info = catcher.getvalue() sys.stdout = stdout message = get_text_message(info) if message is None: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = find_branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.check import check check(find_branch(dir)) class cmd_upgrade(Command): """Upgrade branch storage to current format. This should normally be used only after the check command tells you to run it. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.upgrade import upgrade upgrade(find_branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest return int(not selftest()) class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Restore selected files from a previous revision. """ takes_args = ['file+'] def run(self, file_list): from bzrlib.branch import find_branch if not file_list: file_list = ['.'] b = find_branch(file_list[0]) b.revert([b.relpath(f) for f in file_list]) class cmd_merge_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): from bzrlib.statcache import update_cache b = find_branch('.') update_cache(b.base, b.read_working_inventory()) class cmd_plugins(Command): """List plugins""" hidden = True def run(self): import bzrlib.plugin from pprint import pprint pprint(bzrlib.plugin.all_plugins) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'update': None, 'long': None, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', 'l': 'long', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) >>> parse_args('log -r 500'.split()) (['log'], {'revision': 500}) >>> parse_args('log -r500:600'.split()) (['log'], {'revision': [500, 600]}) >>> parse_args('log -vr500:600'.split()) (['log'], {'verbose': True, 'revision': [500, 600]}) >>> parse_args('log -rv500:600'.split()) #the r takes an argument Traceback (most recent call last): ... ValueError: invalid literal for int(): v500 """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: raise BzrError('unknown long option %r' % a) else: shortopt = a[1:] if shortopt in SHORT_OPTIONS: # Multi-character options must have a space to delimit # their value optname = SHORT_OPTIONS[shortopt] else: # Single character short options, can be chained, # and have their value appended to their name shortopt = a[1:2] if shortopt not in SHORT_OPTIONS: # We didn't find the multi-character name, and we # didn't find the single char name raise BzrError('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if a[2:]: # There are extra things on this option # see if it is the value, or if it is another # short option optargfn = OPTIONS[optname] if optargfn is None: # This option does not take an argument, so the # next entry is another short option, pack it back # into the list argv.insert(0, '-' + a[2:]) else: # This option takes an argument, so pack it # into the array optarg = a[2:] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? raise BzrError('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: raise BzrError('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: raise BzrError('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def _parse_master_args(argv): """Parse the arguments that always go with the original command. These are things like bzr --no-plugins, etc. There are now 2 types of option flags. Ones that come *before* the command, and ones that come *after* the command. Ones coming *before* the command are applied against all possible commands. And are generally applied before plugins are loaded. The current list are: --builtin Allow plugins to load, but don't let them override builtin commands, they will still be allowed if they do not override a builtin. --no-plugins Don't load any plugins. This lets you get back to official source behavior. --profile Enable the hotspot profile before running the command. For backwards compatibility, this is also a non-master option. --version Spit out the version of bzr that is running and exit. This is also a non-master option. --help Run help and exit, also a non-master option (I think that should stay, though) >>> argv, opts = _parse_master_args(['bzr', '--test']) Traceback (most recent call last): ... BzrCommandError: Invalid master option: 'test' >>> argv, opts = _parse_master_args(['bzr', '--version', 'command']) >>> print argv ['command'] >>> print opts['version'] True >>> argv, opts = _parse_master_args(['bzr', '--profile', 'command', '--more-options']) >>> print argv ['command', '--more-options'] >>> print opts['profile'] True >>> argv, opts = _parse_master_args(['bzr', '--no-plugins', 'command']) >>> print argv ['command'] >>> print opts['no-plugins'] True >>> print opts['profile'] False >>> argv, opts = _parse_master_args(['bzr', 'command', '--profile']) >>> print argv ['command', '--profile'] >>> print opts['profile'] False """ master_opts = {'builtin':False, 'no-plugins':False, 'version':False, 'profile':False, 'help':False } # This is the point where we could hook into argv[0] to determine # what front-end is supposed to be run # For now, we are just ignoring it. cmd_name = argv.pop(0) for arg in argv[:]: if arg[:2] != '--': # at the first non-option, we return the rest break arg = arg[2:] # Remove '--' if arg not in master_opts: # We could say that this is not an error, that we should # just let it be handled by the main section instead raise BzrCommandError('Invalid master option: %r' % arg) argv.pop(0) # We are consuming this entry master_opts[arg] = True return argv, master_opts def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: # some options like --builtin and --no-plugins have special effects argv, master_opts = _parse_master_args(argv) if not master_opts['no-plugins']: from bzrlib.plugin import load_plugins load_plugins() args, opts = parse_args(argv) if master_opts['help']: from bzrlib.help import help if argv: help(argv[0]) else: help() return 0 if 'help' in opts: from bzrlib.help import help if args: help(args[0]) else: help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 plugins_override = not (master_opts['builtin']) canonical_cmd, cmd_class = get_cmd_class(cmd, plugins_override=plugins_override) profile = master_opts['profile'] # For backwards compatibility, I would rather stick with --profile being a # master/global option if 'profile' in opts: profile = True del opts['profile'] # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): bzrlib.trace.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: import errno quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/workingtree.py data 9080 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os import bzrlib.tree from errors import BzrCheckError from trace import mutter class WorkingTree(bzrlib.tree.Tree): """Working copy tree. The inventory is held in the `Branch` working-inventory, and the files are in a directory on disk. It is possible for a `WorkingTree` to have a filename which is not listed in the Inventory and vice versa. """ _statcache = None def __init__(self, basedir, inv): self._inventory = inv self.basedir = basedir self.path2id = inv.path2id self._update_statcache() def __iter__(self): """Iterate through file_ids for this tree. file_ids are in a WorkingTree if they are in the working inventory and the working file exists. """ inv = self._inventory for file_id in self._inventory: # TODO: This is slightly redundant; we should be able to just # check the statcache but it only includes regular files. # only include files which still exist on disk ie = inv[file_id] if ie.kind == 'file': if ((file_id in self._statcache) or (os.path.exists(self.abspath(inv.id2path(file_id))))): yield file_id def __repr__(self): return "<%s of %s>" % (self.__class__.__name__, self.basedir) def abspath(self, filename): return os.path.join(self.basedir, filename) def has_filename(self, filename): return os.path.exists(self.abspath(filename)) def get_file(self, file_id): return self.get_file_byname(self.id2path(file_id)) def get_file_byname(self, filename): return file(self.abspath(filename), 'rb') def _get_store_filename(self, file_id): ## XXX: badly named; this isn't in the store at all return self.abspath(self.id2path(file_id)) def has_id(self, file_id): # files that have been deleted are excluded if not self.inventory.has_id(file_id): return False if file_id in self._statcache: return True return os.path.exists(self.abspath(self.id2path(file_id))) __contains__ = has_id def _update_statcache(self): if not self._statcache: from bzrlib.statcache import update_cache self._statcache = update_cache(self.basedir, self.inventory) def get_file_size(self, file_id): import os, stat return os.stat(self._get_store_filename(file_id))[stat.ST_SIZE] def get_file_sha1(self, file_id): from bzrlib.statcache import SC_SHA1 return self._statcache[file_id][SC_SHA1] def file_class(self, filename): if self.path2id(filename): return 'V' elif self.is_ignored(filename): return 'I' else: return '?' def list_files(self): """Recursively list all files as (path, class, kind, id). Lists, but does not descend into unversioned directories. This does not include files that have been deleted in this tree. Skips the control directory. """ from osutils import appendpath, file_kind import os inv = self.inventory def descend(from_dir_relpath, from_dir_id, dp): ls = os.listdir(dp) ls.sort() for f in ls: ## TODO: If we find a subdirectory with its own .bzr ## directory, then that is a separate tree and we ## should exclude it. if bzrlib.BZRDIR == f: continue # path within tree fp = appendpath(from_dir_relpath, f) # absolute path fap = appendpath(dp, f) f_ie = inv.get_child(from_dir_id, f) if f_ie: c = 'V' elif self.is_ignored(fp): c = 'I' else: c = '?' fk = file_kind(fap) if f_ie: if f_ie.kind != fk: raise BzrCheckError("file %r entered as kind %r id %r, " "now of kind %r" % (fap, f_ie.kind, f_ie.file_id, fk)) yield fp, c, fk, (f_ie and f_ie.file_id) if fk != 'directory': continue if c != 'V': # don't descend unversioned directories continue for ff in descend(fp, f_ie.file_id, fap): yield ff for f in descend('', inv.root.file_id, self.basedir): yield f def unknowns(self): for subp in self.extras(): if not self.is_ignored(subp): yield subp def extras(self): """Yield all unknown files in this WorkingTree. If there are any unknown directories then only the directory is returned, not all its children. But if there are unknown files under a versioned subdirectory, they are returned. Currently returned depth-first, sorted by name within directories. """ ## TODO: Work from given directory downwards from osutils import isdir, appendpath for path, dir_entry in self.inventory.directories(): mutter("search for unknowns in %r" % path) dirabs = self.abspath(path) if not isdir(dirabs): # e.g. directory deleted continue fl = [] for subf in os.listdir(dirabs): if (subf != '.bzr' and (subf not in dir_entry.children)): fl.append(subf) fl.sort() for subf in fl: subp = appendpath(path, subf) yield subp def ignored_files(self): """Yield list of PATH, IGNORE_PATTERN""" for subp in self.extras(): pat = self.is_ignored(subp) if pat != None: yield subp, pat def get_ignore_list(self): """Return list of ignore patterns. Cached in the Tree object after the first call. """ if hasattr(self, '_ignorelist'): return self._ignorelist l = bzrlib.DEFAULT_IGNORE[:] if self.has_filename(bzrlib.IGNORE_FILENAME): f = self.get_file_byname(bzrlib.IGNORE_FILENAME) l.extend([line.rstrip("\n\r") for line in f.readlines()]) self._ignorelist = l return l def is_ignored(self, filename): r"""Check whether the filename matches an ignore pattern. Patterns containing '/' or '\' need to match the whole path; others match against only the last component. If the file is ignored, returns the pattern which caused it to be ignored, otherwise None. So this can simply be used as a boolean if desired.""" # TODO: Use '**' to match directories, and other extended # globbing stuff from cvs/rsync. # XXX: fnmatch is actually not quite what we want: it's only # approximately the same as real Unix fnmatch, and doesn't # treat dotfiles correctly and allows * to match /. # Eventually it should be replaced with something more # accurate. import fnmatch from osutils import splitpath for pat in self.get_ignore_list(): if '/' in pat or '\\' in pat: # as a special case, you can put ./ at the start of a # pattern; this is good to match in the top-level # only; if (pat[:2] == './') or (pat[:2] == '.\\'): newpat = pat[2:] else: newpat = pat if fnmatch.fnmatchcase(filename, newpat): return pat else: if fnmatch.fnmatchcase(splitpath(filename)[-1], pat): return pat else: return None commit refs/heads/master mark :845 committer Martin Pool 1120628033 +1000 data 3 doc from :844 M 644 inline bzrlib/statcache.py data 9097 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import stat, os, sha, time from trace import mutter from errors import BzrError, BzrCheckError """File stat cache to speed up tree comparisons. This module basically gives a quick way to find the SHA-1 and related information of a file in the working directory, without actually reading and hashing the whole file. The information is validated by checking the size, mtime, ctime, etc of the file as returned by the stat() system call. This has no relation to the deprecated standard Python module called statcache (vs bzrlib.statcache). Implementation ============== Users of this module should not need to know about how this is implemented, and in particular should not depend on the particular data which is stored or its format. This is done by maintaining a cache indexed by a file fingerprint of (path, size, mtime, ctime, ino, dev) pointing to the SHA-1. If the fingerprint has changed, we assume the file content has not changed either and the SHA-1 is therefore the same. If any of the fingerprint fields have changed then the file content *may* have changed, or it may not have. We need to reread the file contents to make sure, but this is not visible to the user or higher-level code (except as a delay of course). The mtime and ctime are stored with nanosecond fields, but not all filesystems give this level of precision. There is therefore a possible race: the file might be modified twice within a second without changing the size or mtime, and a SHA-1 cached from the first version would be wrong. We handle this by not recording a cached hash for any files which were modified in the current second and that therefore have the chance to change again before the second is up. The only known hole in this design is if the system clock jumps backwards crossing invocations of bzr. Please don't do that; use ntp to gradually adjust your clock or don't use bzr over the step. At the moment this is stored in a simple textfile; it might be nice to use a tdb instead to allow faster lookup by file-id. The cache is represented as a map from file_id to a tuple of (file_id, sha1, path, size, mtime, ctime, ino, dev). The SHA-1 is stored in memory as a hexdigest. This version of the file on disk has one line per record, and fields separated by \0 records. """ # order of fields returned by fingerprint() FP_SIZE = 0 FP_MTIME = 1 FP_CTIME = 2 FP_INO = 3 FP_DEV = 4 # order of fields in the statcache file and in the in-memory map SC_FILE_ID = 0 SC_SHA1 = 1 SC_PATH = 2 SC_SIZE = 3 SC_MTIME = 4 SC_CTIME = 5 SC_INO = 6 SC_DEV = 7 CACHE_HEADER = "### bzr statcache v4" def fingerprint(abspath): try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) def _write_cache(basedir, entries): from atomicfile import AtomicFile cachefn = os.path.join(basedir, '.bzr', 'stat-cache') outf = AtomicFile(cachefn, 'wb') try: outf.write(CACHE_HEADER + '\n') for entry in entries: if len(entry) != 8: raise ValueError("invalid statcache entry tuple %r" % entry) outf.write(entry[0].encode('utf-8')) # file id outf.write('\0') outf.write(entry[1]) # hex sha1 outf.write('\0') outf.write(entry[2].encode('utf-8')) # name for nf in entry[3:]: outf.write('\0%d' % nf) outf.write('\n') outf.commit() finally: if not outf.closed: outf.abort() def _try_write_cache(basedir, entries): try: return _write_cache(basedir, entries) except IOError, e: mutter("cannot update statcache in %s: %s" % (basedir, e)) except OSError, e: mutter("cannot update statcache in %s: %s" % (basedir, e)) def load_cache(basedir): import re cache = {} seen_paths = {} from bzrlib.trace import warning assert isinstance(basedir, basestring) sha_re = re.compile(r'[a-f0-9]{40}') try: cachefn = os.path.join(basedir, '.bzr', 'stat-cache') cachefile = open(cachefn, 'rb') except IOError: return cache line1 = cachefile.readline().rstrip('\r\n') if line1 != CACHE_HEADER: mutter('cache header marker not found at top of %s; discarding cache' % cachefn) return cache for l in cachefile: f = l.split('\0') file_id = f[0].decode('utf-8') if file_id in cache: warning("duplicated file_id in cache: {%s}" % file_id) text_sha = f[1] if len(text_sha) != 40 or not sha_re.match(text_sha): raise BzrCheckError("invalid file SHA-1 in cache: %r" % text_sha) path = f[2].decode('utf-8') if path in seen_paths: warning("duplicated path in cache: %r" % path) seen_paths[path] = True entry = (file_id, text_sha, path) + tuple([long(x) for x in f[3:]]) if len(entry) != 8: raise ValueError("invalid statcache entry tuple %r" % entry) cache[file_id] = entry return cache def _files_from_inventory(inv): for path, ie in inv.iter_entries(): if ie.kind != 'file': continue yield ie.file_id, path def update_cache(basedir, inv, flush=False): """Update and return the cache for the branch. The returned cache may contain entries that have not been written to disk for files recently touched. flush -- discard any previous cache and recalculate from scratch. """ # load the existing cache; use information there to find a list of # files ordered by inode, which is alleged to be the fastest order # to stat the files. to_update = _files_from_inventory(inv) assert isinstance(flush, bool) if flush: cache = {} else: cache = load_cache(basedir) by_inode = [] without_inode = [] for file_id, path in to_update: if file_id in cache: by_inode.append((cache[file_id][SC_INO], file_id, path)) else: without_inode.append((file_id, path)) by_inode.sort() to_update = [a[1:] for a in by_inode] + without_inode stat_cnt = missing_cnt = new_cnt = hardcheck = change_cnt = 0 # dangerfiles have been recently touched and can't be committed to # a persistent cache yet, but they are returned to the caller. dangerfiles = [] now = int(time.time()) ## mutter('update statcache under %r' % basedir) for file_id, path in to_update: abspath = os.path.join(basedir, path) fp = fingerprint(abspath) stat_cnt += 1 cacheentry = cache.get(file_id) if fp == None: # not here if cacheentry: del cache[file_id] change_cnt += 1 missing_cnt += 1 continue elif not cacheentry: new_cnt += 1 if (fp[FP_MTIME] >= now) or (fp[FP_CTIME] >= now): dangerfiles.append(file_id) if cacheentry and (cacheentry[3:] == fp): continue # all stat fields unchanged hardcheck += 1 dig = sha.new(file(abspath, 'rb').read()).hexdigest() # We update the cache even if the digest has not changed from # last time we looked, so that the fingerprint fields will # match in future. cacheentry = (file_id, dig, path) + fp cache[file_id] = cacheentry change_cnt += 1 mutter('statcache: statted %d files, read %d files, %d changed, %d dangerous, ' '%d deleted, %d new, ' '%d in cache' % (stat_cnt, hardcheck, change_cnt, len(dangerfiles), missing_cnt, new_cnt, len(cache))) if change_cnt: mutter('updating on-disk statcache') if dangerfiles: safe_cache = cache.copy() for file_id in dangerfiles: del safe_cache[file_id] else: safe_cache = cache _try_write_cache(basedir, safe_cache.itervalues()) return cache commit refs/heads/master mark :846 committer Martin Pool 1120644451 +1000 data 62 - start adding refactored/simplified hash cache not used yet from :845 M 644 inline bzrlib/hashcache.py data 3881 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def _fingerprint(abspath): import os, stat try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) class HashCache(object): """Cache for looking up file SHA-1. Files are considered to match the cached value if the fingerprint of the file has not changed. This includes its mtime, ctime, device number, inode number, and size. This should catch modifications or replacement of the file by a new one. This may not catch modifications that do not change the file's size and that occur within the resolution window of the timestamps. To handle this we specifically do not cache files which have changed since the start of the present second, since they could undetectably change again. This scheme may fail if the machine's clock steps backwards. Don't do that. This does not canonicalize the paths passed in; that should be done by the caller. cache_sha1 Indexed by path, gives the SHA-1 of the file. validator Indexed by path, gives the fingerprint of the file last time it was read. stat_count number of times files have been statted hit_count number of times files have been retrieved from the cache, avoiding a re-read miss_count number of misses (times files have been completely re-read) """ def __init__(self, basedir): self.basedir = basedir self.hit_count = 0 self.miss_count = 0 self.stat_count = 0 self.danger_count = 0 self.cache_sha1 = {} self.validator = {} def clear(self): """Discard all cached information.""" self.validator = {} self.cache_sha1 = {} def get_sha1(self, path): """Return the hex SHA-1 of the contents of the file at path. XXX: If the file does not exist or is not a plain file??? """ import os, time from bzrlib.osutils import sha_file abspath = os.path.join(self.basedir, path) fp = _fingerprint(abspath) cache_fp = self.validator.get(path) self.stat_count += 1 if not fp: # not a regular file return None elif cache_fp and (cache_fp == fp): self.hit_count += 1 return self.cache_sha1[path] else: self.miss_count += 1 digest = sha_file(file(abspath, 'rb')) now = int(time.time()) if fp[1] >= now or fp[2] >= now: # changed too recently; can't be cached. we can # return the result and it could possibly be cached # next time. self.danger_count += 1 if cache_fp: del self.validator[path] del self.cache_sha1[path] else: self.validator[path] = fp self.cache_sha1[path] = digest return digest M 644 inline bzrlib/selftest/testhashcache.py data 2816 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from bzrlib.selftest import InTempDir def sha1(t): import sha return sha.new(t).hexdigest() def pause(): import time # allow it to stabilize start = int(time.time()) while int(time.time()) == start: time.sleep(0.2) class TestStatCache(InTempDir): """Functional tests for statcache""" def runTest(self): from bzrlib.hashcache import HashCache import os import time hc = HashCache('.') file('foo', 'wb').write('hello') os.mkdir('subdir') pause() self.assertEquals(hc.get_sha1('foo'), 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d') self.assertEquals(hc.miss_count, 1) self.assertEquals(hc.hit_count, 0) # check we hit without re-reading self.assertEquals(hc.get_sha1('foo'), 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d') self.assertEquals(hc.miss_count, 1) self.assertEquals(hc.hit_count, 1) # check again without re-reading self.assertEquals(hc.get_sha1('foo'), 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d') self.assertEquals(hc.miss_count, 1) self.assertEquals(hc.hit_count, 2) # write new file and make sure it is seen file('foo', 'wb').write('goodbye') pause() self.assertEquals(hc.get_sha1('foo'), '3c8ec4874488f6090a157b014ce3397ca8e06d4f') self.assertEquals(hc.miss_count, 2) # quickly write new file of same size and make sure it is seen # this may rely on detection of timestamps that are too close # together to be safe file('foo', 'wb').write('g00dbye') self.assertEquals(hc.get_sha1('foo'), sha1('g00dbye')) # this is not quite guaranteed to be true; we might have # crossed a 1s boundary before self.assertEquals(hc.danger_count, 1) self.assertEquals(hc.get_sha1('subdir'), None) TEST_CLASSES = [ TestStatCache, ] M 644 inline bzrlib/selftest/__init__.py data 2215 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from testsweet import TestBase, run_suite, InTempDir def selftest(): from unittest import TestLoader, TestSuite import bzrlib, bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, bzrlib.commands import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning import bzrlib.selftest.testmerge3 import bzrlib.selftest.testhashcache import bzrlib.merge_core from doctest import DocTestSuite import os import shutil import time import sys TestBase.BZRPATH = os.path.join(os.path.realpath(os.path.dirname(bzrlib.__path__[0])), 'bzr') print '%-30s %s' % ('bzr binary', TestBase.BZRPATH) print suite = TestSuite() # should also test bzrlib.merge_core, but they seem to be out of date with # the code. # python2.3's TestLoader() doesn't seem to work well; don't know why for m in (bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, bzrlib.commands, bzrlib.merge3): suite.addTest(DocTestSuite(m)) for cl in (bzrlib.selftest.whitebox.TEST_CLASSES + bzrlib.selftest.versioning.TEST_CLASSES + bzrlib.selftest.testmerge3.TEST_CLASSES + bzrlib.selftest.testhashcache.TEST_CLASSES + bzrlib.selftest.blackbox.TEST_CLASSES): suite.addTest(cl()) return run_suite(suite, 'testbzr') M 644 inline bzrlib/statcache.py data 9312 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import stat, os, sha, time from trace import mutter from errors import BzrError, BzrCheckError """File stat cache to speed up tree comparisons. This module basically gives a quick way to find the SHA-1 and related information of a file in the working directory, without actually reading and hashing the whole file. The information is validated by checking the size, mtime, ctime, etc of the file as returned by the stat() system call. This has no relation to the deprecated standard Python module called statcache (vs bzrlib.statcache). Implementation ============== Users of this module should not need to know about how this is implemented, and in particular should not depend on the particular data which is stored or its format. The cache maintains a mapping from filename to the SHA-1 of the content of the file. The cache also stores a fingerprint of (size, mtime, ctime, ino, dev) which is used to validate that the entry is up-to-date. This is done by maintaining a cache indexed by a file fingerprint of (path, size, mtime, ctime, ino, dev) pointing to the SHA-1. If the fingerprint has changed, we assume the file content has not changed either and the SHA-1 is therefore the same. If any of the fingerprint fields have changed then the file content *may* have changed, or it may not have. We need to reread the file contents to make sure, but this is not visible to the user or higher-level code (except as a delay of course). The mtime and ctime are stored with nanosecond fields, but not all filesystems give this level of precision. There is therefore a possible race: the file might be modified twice within a second without changing the size or mtime, and a SHA-1 cached from the first version would be wrong. We handle this by not recording a cached hash for any files which were modified in the current second and that therefore have the chance to change again before the second is up. The only known hole in this design is if the system clock jumps backwards crossing invocations of bzr. Please don't do that; use ntp to gradually adjust your clock or don't use bzr over the step. At the moment this is stored in a simple textfile; it might be nice to use a tdb instead to allow faster lookup by file-id. The cache is represented as a map from file_id to a tuple of (file_id, sha1, path, size, mtime, ctime, ino, dev). The SHA-1 is stored in memory as a hexdigest. This version of the file on disk has one line per record, and fields separated by \0 records. """ # order of fields returned by fingerprint() FP_SIZE = 0 FP_MTIME = 1 FP_CTIME = 2 FP_INO = 3 FP_DEV = 4 # order of fields in the statcache file and in the in-memory map SC_FILE_ID = 0 SC_SHA1 = 1 SC_PATH = 2 SC_SIZE = 3 SC_MTIME = 4 SC_CTIME = 5 SC_INO = 6 SC_DEV = 7 CACHE_HEADER = "### bzr statcache v4" def fingerprint(abspath): try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) def _write_cache(basedir, entries): from atomicfile import AtomicFile cachefn = os.path.join(basedir, '.bzr', 'stat-cache') outf = AtomicFile(cachefn, 'wb') try: outf.write(CACHE_HEADER + '\n') for entry in entries: if len(entry) != 8: raise ValueError("invalid statcache entry tuple %r" % entry) outf.write(entry[0].encode('utf-8')) # file id outf.write('\0') outf.write(entry[1]) # hex sha1 outf.write('\0') outf.write(entry[2].encode('utf-8')) # name for nf in entry[3:]: outf.write('\0%d' % nf) outf.write('\n') outf.commit() finally: if not outf.closed: outf.abort() def _try_write_cache(basedir, entries): try: return _write_cache(basedir, entries) except IOError, e: mutter("cannot update statcache in %s: %s" % (basedir, e)) except OSError, e: mutter("cannot update statcache in %s: %s" % (basedir, e)) def load_cache(basedir): import re cache = {} seen_paths = {} from bzrlib.trace import warning assert isinstance(basedir, basestring) sha_re = re.compile(r'[a-f0-9]{40}') try: cachefn = os.path.join(basedir, '.bzr', 'stat-cache') cachefile = open(cachefn, 'rb') except IOError: return cache line1 = cachefile.readline().rstrip('\r\n') if line1 != CACHE_HEADER: mutter('cache header marker not found at top of %s; discarding cache' % cachefn) return cache for l in cachefile: f = l.split('\0') file_id = f[0].decode('utf-8') if file_id in cache: warning("duplicated file_id in cache: {%s}" % file_id) text_sha = f[1] if len(text_sha) != 40 or not sha_re.match(text_sha): raise BzrCheckError("invalid file SHA-1 in cache: %r" % text_sha) path = f[2].decode('utf-8') if path in seen_paths: warning("duplicated path in cache: %r" % path) seen_paths[path] = True entry = (file_id, text_sha, path) + tuple([long(x) for x in f[3:]]) if len(entry) != 8: raise ValueError("invalid statcache entry tuple %r" % entry) cache[file_id] = entry return cache def _files_from_inventory(inv): for path, ie in inv.iter_entries(): if ie.kind != 'file': continue yield ie.file_id, path def update_cache(basedir, inv, flush=False): """Update and return the cache for the branch. The returned cache may contain entries that have not been written to disk for files recently touched. flush -- discard any previous cache and recalculate from scratch. """ # load the existing cache; use information there to find a list of # files ordered by inode, which is alleged to be the fastest order # to stat the files. to_update = _files_from_inventory(inv) assert isinstance(flush, bool) if flush: cache = {} else: cache = load_cache(basedir) by_inode = [] without_inode = [] for file_id, path in to_update: if file_id in cache: by_inode.append((cache[file_id][SC_INO], file_id, path)) else: without_inode.append((file_id, path)) by_inode.sort() to_update = [a[1:] for a in by_inode] + without_inode stat_cnt = missing_cnt = new_cnt = hardcheck = change_cnt = 0 # dangerfiles have been recently touched and can't be committed to # a persistent cache yet, but they are returned to the caller. dangerfiles = [] now = int(time.time()) ## mutter('update statcache under %r' % basedir) for file_id, path in to_update: abspath = os.path.join(basedir, path) fp = fingerprint(abspath) stat_cnt += 1 cacheentry = cache.get(file_id) if fp == None: # not here if cacheentry: del cache[file_id] change_cnt += 1 missing_cnt += 1 continue elif not cacheentry: new_cnt += 1 if (fp[FP_MTIME] >= now) or (fp[FP_CTIME] >= now): dangerfiles.append(file_id) if cacheentry and (cacheentry[3:] == fp): continue # all stat fields unchanged hardcheck += 1 dig = sha.new(file(abspath, 'rb').read()).hexdigest() # We update the cache even if the digest has not changed from # last time we looked, so that the fingerprint fields will # match in future. cacheentry = (file_id, dig, path) + fp cache[file_id] = cacheentry change_cnt += 1 mutter('statcache: statted %d files, read %d files, %d changed, %d dangerous, ' '%d deleted, %d new, ' '%d in cache' % (stat_cnt, hardcheck, change_cnt, len(dangerfiles), missing_cnt, new_cnt, len(cache))) if change_cnt: mutter('updating on-disk statcache') if dangerfiles: safe_cache = cache.copy() for file_id in dangerfiles: del safe_cache[file_id] else: safe_cache = cache _try_write_cache(basedir, safe_cache.itervalues()) return cache commit refs/heads/master mark :847 committer Martin Pool 1120658359 +1000 data 108 - remove rsync plugin; better to get it from john's site at http://bzr.arbash-meinel.com/plugins/bzr-rsync from :846 D plugins/rsync D plugins/rsync/__init__.py D plugins/rsync/rsync_update.py commit refs/heads/master mark :848 committer Martin Pool 1120658826 +1000 data 3 doc from :847 M 644 inline bzrlib/inventory.py data 19221 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # This should really be an id randomly assigned when the tree is # created, but it's not for now. ROOT_ID = "TREE_ROOT" import sys, os.path, types, re import bzrlib from bzrlib.errors import BzrError, BzrCheckError from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath from bzrlib.trace import mutter class InventoryEntry(object): """Description of a versioned file. An InventoryEntry has the following fields, which are also present in the XML inventory-entry element: * *file_id* * *name*: (only the basename within the directory, must not contain slashes) * *kind*: "directory" or "file" * *directory_id*: (if absent/null means the branch root directory) * *text_sha1*: only for files * *text_size*: in bytes, only for files * *text_id*: identifier for the text version, only for files InventoryEntries can also exist inside a WorkingTree inventory, in which case they are not yet bound to a particular revision of the file. In that case the text_sha1, text_size and text_id are absent. >>> i = Inventory() >>> i.path2id('') 'TREE_ROOT' >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID)) >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123')) >>> for j in i.iter_entries(): ... print j ... ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')) ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')) >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123')) Traceback (most recent call last): ... BzrError: inventory already contains entry with id {2323} >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123')) >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123')) >>> i.path2id('src/wibble') '2325' >>> '2325' in i True >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325')) >>> i['2326'] InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325') >>> for j in i.iter_entries(): ... print j[0] ... assert i.path2id(j[0]) ... src src/bye.c src/hello.c src/wibble src/wibble/wibble.c >>> i.id2path('2326') 'src/wibble/wibble.c' TODO: Maybe also keep the full path of the entry, and the children? But those depend on its position within a particular inventory, and it would be nice not to need to hold the backpointer here. """ # TODO: split InventoryEntry into subclasses for files, # directories, etc etc. text_sha1 = None text_size = None def __init__(self, file_id, name, kind, parent_id, text_id=None): """Create an InventoryEntry The filename must be a single component, relative to the parent directory; it cannot be a whole path or relative name. >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID) >>> e.name 'hello.c' >>> e.file_id '123' >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID) Traceback (most recent call last): BzrCheckError: InventoryEntry name 'src/hello.c' is invalid """ if '/' in name or '\\' in name: raise BzrCheckError('InventoryEntry name %r is invalid' % name) self.file_id = file_id self.name = name self.kind = kind self.text_id = text_id self.parent_id = parent_id if kind == 'directory': self.children = {} elif kind == 'file': pass else: raise BzrError("unhandled entry kind %r" % kind) def sorted_children(self): l = self.children.items() l.sort() return l def copy(self): other = InventoryEntry(self.file_id, self.name, self.kind, self.parent_id, text_id=self.text_id) other.text_sha1 = self.text_sha1 other.text_size = self.text_size # note that children are *not* copied; they're pulled across when # others are added return other def __repr__(self): return ("%s(%r, %r, kind=%r, parent_id=%r)" % (self.__class__.__name__, self.file_id, self.name, self.kind, self.parent_id)) def to_element(self): """Convert to XML element""" from bzrlib.xml import Element e = Element('entry') e.set('name', self.name) e.set('file_id', self.file_id) e.set('kind', self.kind) if self.text_size != None: e.set('text_size', '%d' % self.text_size) for f in ['text_id', 'text_sha1']: v = getattr(self, f) if v != None: e.set(f, v) # to be conservative, we don't externalize the root pointers # for now, leaving them as null in the xml form. in a future # version it will be implied by nested elements. if self.parent_id != ROOT_ID: assert isinstance(self.parent_id, basestring) e.set('parent_id', self.parent_id) e.tail = '\n' return e def from_element(cls, elt): assert elt.tag == 'entry' ## original format inventories don't have a parent_id for ## nodes in the root directory, but it's cleaner to use one ## internally. parent_id = elt.get('parent_id') if parent_id == None: parent_id = ROOT_ID self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id) self.text_id = elt.get('text_id') self.text_sha1 = elt.get('text_sha1') ## mutter("read inventoryentry: %r" % (elt.attrib)) v = elt.get('text_size') self.text_size = v and int(v) return self from_element = classmethod(from_element) def __eq__(self, other): if not isinstance(other, InventoryEntry): return NotImplemented return (self.file_id == other.file_id) \ and (self.name == other.name) \ and (self.text_sha1 == other.text_sha1) \ and (self.text_size == other.text_size) \ and (self.text_id == other.text_id) \ and (self.parent_id == other.parent_id) \ and (self.kind == other.kind) def __ne__(self, other): return not (self == other) def __hash__(self): raise ValueError('not hashable') class RootEntry(InventoryEntry): def __init__(self, file_id): self.file_id = file_id self.children = {} self.kind = 'root_directory' self.parent_id = None self.name = '' def __eq__(self, other): if not isinstance(other, RootEntry): return NotImplemented return (self.file_id == other.file_id) \ and (self.children == other.children) class Inventory(object): """Inventory of versioned files in a tree. This describes which file_id is present at each point in the tree, and possibly the SHA-1 or other information about the file. Entries can be looked up either by path or by file_id. The inventory represents a typical unix file tree, with directories containing files and subdirectories. We never store the full path to a file, because renaming a directory implicitly moves all of its contents. This class internally maintains a lookup tree that allows the children under a directory to be returned quickly. InventoryEntry objects must not be modified after they are inserted, other than through the Inventory API. >>> inv = Inventory() >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID)) >>> inv['123-123'].name 'hello.c' May be treated as an iterator or set to look up file ids: >>> bool(inv.path2id('hello.c')) True >>> '123-123' in inv True May also look up by name: >>> [x[0] for x in inv.iter_entries()] ['hello.c'] """ def __init__(self): """Create or read an inventory. If a working directory is specified, the inventory is read from there. If the file is specified, read from that. If not, the inventory is created empty. The inventory is created with a default root directory, with an id of None. """ self.root = RootEntry(ROOT_ID) self._byid = {self.root.file_id: self.root} def __iter__(self): return iter(self._byid) def __len__(self): """Returns number of entries.""" return len(self._byid) def iter_entries(self, from_dir=None): """Return (path, entry) pairs, in order by name.""" if from_dir == None: assert self.root from_dir = self.root elif isinstance(from_dir, basestring): from_dir = self._byid[from_dir] kids = from_dir.children.items() kids.sort() for name, ie in kids: yield name, ie if ie.kind == 'directory': for cn, cie in self.iter_entries(from_dir=ie.file_id): yield os.path.join(name, cn), cie def entries(self): """Return list of (path, ie) for all entries except the root. This may be faster than iter_entries. """ accum = [] def descend(dir_ie, dir_path): kids = dir_ie.children.items() kids.sort() for name, ie in kids: child_path = os.path.join(dir_path, name) accum.append((child_path, ie)) if ie.kind == 'directory': descend(ie, child_path) descend(self.root, '') return accum def directories(self): """Return (path, entry) pairs for all directories, including the root. """ accum = [] def descend(parent_ie, parent_path): accum.append((parent_path, parent_ie)) kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory'] kids.sort() for name, child_ie in kids: child_path = os.path.join(parent_path, name) descend(child_ie, child_path) descend(self.root, '') return accum def __contains__(self, file_id): """True if this entry contains a file with given id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> '456' in inv False """ return file_id in self._byid def __getitem__(self, file_id): """Return the entry for given file_id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID)) >>> inv['123123'].name 'hello.c' """ try: return self._byid[file_id] except KeyError: if file_id == None: raise BzrError("can't look up file_id None") else: raise BzrError("file_id {%s} not in inventory" % file_id) def get_file_kind(self, file_id): return self._byid[file_id].kind def get_child(self, parent_id, filename): return self[parent_id].children.get(filename) def add(self, entry): """Add entry to inventory. To add a file to a branch ready to be committed, use Branch.add, which calls this.""" if entry.file_id in self._byid: raise BzrError("inventory already contains entry with id {%s}" % entry.file_id) try: parent = self._byid[entry.parent_id] except KeyError: raise BzrError("parent_id {%s} not in inventory" % entry.parent_id) if parent.children.has_key(entry.name): raise BzrError("%s is already versioned" % appendpath(self.id2path(parent.file_id), entry.name)) self._byid[entry.file_id] = entry parent.children[entry.name] = entry def add_path(self, relpath, kind, file_id=None): """Add entry from a path. The immediate parent must already be versioned""" from bzrlib.errors import NotVersionedError parts = bzrlib.osutils.splitpath(relpath) if len(parts) == 0: raise BzrError("cannot re-add root of inventory") if file_id == None: from bzrlib.branch import gen_file_id file_id = gen_file_id(relpath) parent_path = parts[:-1] parent_id = self.path2id(parent_path) if parent_id == None: raise NotVersionedError(parent_path) ie = InventoryEntry(file_id, parts[-1], kind=kind, parent_id=parent_id) return self.add(ie) def __delitem__(self, file_id): """Remove entry by id. >>> inv = Inventory() >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID)) >>> '123' in inv True >>> del inv['123'] >>> '123' in inv False """ ie = self[file_id] assert self[ie.parent_id].children[ie.name] == ie # TODO: Test deleting all children; maybe hoist to a separate # deltree method? if ie.kind == 'directory': for cie in ie.children.values(): del self[cie.file_id] del ie.children del self._byid[file_id] del self[ie.parent_id].children[ie.name] def to_element(self): """Convert to XML Element""" from bzrlib.xml import Element e = Element('inventory') e.text = '\n' for path, ie in self.iter_entries(): e.append(ie.to_element()) return e def from_element(cls, elt): """Construct from XML Element >>> inv = Inventory() >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID)) >>> elt = inv.to_element() >>> inv2 = Inventory.from_element(elt) >>> inv2 == inv True """ # XXXX: doctest doesn't run this properly under python2.3 assert elt.tag == 'inventory' o = cls() for e in elt: o.add(InventoryEntry.from_element(e)) return o from_element = classmethod(from_element) def __eq__(self, other): """Compare two sets by comparing their contents. >>> i1 = Inventory() >>> i2 = Inventory() >>> i1 == i2 True >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 False >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID)) >>> i1 == i2 True """ if not isinstance(other, Inventory): return NotImplemented if len(self._byid) != len(other._byid): # shortcut: obviously not the same return False return self._byid == other._byid def __ne__(self, other): return not (self == other) def __hash__(self): raise ValueError('not hashable') def get_idpath(self, file_id): """Return a list of file_ids for the path to an entry. The list contains one element for each directory followed by the id of the file itself. So the length of the returned list is equal to the depth of the file in the tree, counting the root directory as depth 1. """ p = [] while file_id != None: try: ie = self._byid[file_id] except KeyError: raise BzrError("file_id {%s} not found in inventory" % file_id) p.insert(0, ie.file_id) file_id = ie.parent_id return p def id2path(self, file_id): """Return as a list the path to file_id.""" # get all names, skipping root p = [self[fid].name for fid in self.get_idpath(file_id)[1:]] return os.sep.join(p) def path2id(self, name): """Walk down through directories to return entry of last component. names may be either a list of path components, or a single string, in which case it is automatically split. This returns the entry of the last component in the path, which may be either a file or a directory. Returns None iff the path is not found. """ if isinstance(name, types.StringTypes): name = splitpath(name) mutter("lookup path %r" % name) parent = self.root for f in name: try: cie = parent.children[f] assert cie.name == f assert cie.parent_id == parent.file_id parent = cie except KeyError: # or raise an error? return None return parent.file_id def has_filename(self, names): return bool(self.path2id(names)) def has_id(self, file_id): return self._byid.has_key(file_id) def rename(self, file_id, new_parent_id, new_name): """Move a file within the inventory. This can change either the name, or the parent, or both. This does not move the working file.""" if not is_valid_name(new_name): raise BzrError("not an acceptable filename: %r" % new_name) new_parent = self._byid[new_parent_id] if new_name in new_parent.children: raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id))) new_parent_idpath = self.get_idpath(new_parent_id) if file_id in new_parent_idpath: raise BzrError("cannot move directory %r into a subdirectory of itself, %r" % (self.id2path(file_id), self.id2path(new_parent_id))) file_ie = self._byid[file_id] old_parent = self._byid[file_ie.parent_id] # TODO: Don't leave things messed up if this fails del old_parent.children[file_ie.name] new_parent.children[new_name] = file_ie file_ie.name = new_name file_ie.parent_id = new_parent_id _NAME_RE = re.compile(r'^[^/\\]+$') def is_valid_name(name): return bool(_NAME_RE.match(name)) M 644 inline bzrlib/selftest/testhashcache.py data 2808 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from bzrlib.selftest import InTempDir def sha1(t): import sha return sha.new(t).hexdigest() def pause(): import time # allow it to stabilize start = int(time.time()) while int(time.time()) == start: time.sleep(0.2) class TestStatCache(InTempDir): """Functional tests for statcache""" def runTest(self): from bzrlib.hashcache import HashCache import os import time hc = HashCache('.') file('foo', 'wb').write('hello') os.mkdir('subdir') pause() self.assertEquals(hc.get_sha1('foo'), 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d') self.assertEquals(hc.miss_count, 1) self.assertEquals(hc.hit_count, 0) # check we hit without re-reading self.assertEquals(hc.get_sha1('foo'), 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d') self.assertEquals(hc.miss_count, 1) self.assertEquals(hc.hit_count, 1) # check again without re-reading self.assertEquals(hc.get_sha1('foo'), 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d') self.assertEquals(hc.miss_count, 1) self.assertEquals(hc.hit_count, 2) # write new file and make sure it is seen file('foo', 'wb').write('goodbye') pause() self.assertEquals(hc.get_sha1('foo'), '3c8ec4874488f6090a157b014ce3397ca8e06d4f') self.assertEquals(hc.miss_count, 2) # quickly write new file of same size and make sure it is seen # this may rely on detection of timestamps that are too close # together to be safe file('foo', 'wb').write('g00dbye') self.assertEquals(hc.get_sha1('foo'), sha1('g00dbye')) # this is not quite guaranteed to be true; we might have # crossed a 1s boundary before self.assertEquals(hc.danger_count, 1) self.assertEquals(hc.get_sha1('subdir'), None) TEST_CLASSES = [ TestStatCache, ] commit refs/heads/master mark :849 committer Martin Pool 1120702023 +1000 data 124 - Put files inside an exported tarball into a top-level directory rather than dumping them into the current directory. from :848 M 644 inline NEWS data 9278 DEVELOPMENT HEAD NEW FEATURES: * Python plugins, automatically loaded from the directories on BZR_PLUGIN_PATH or ~/.bzr.conf/plugins by default. * New 'bzr mkdir' command. * Commit mesage is fetched from an editor if not given on the command line; patch from Torsten Marek. CHANGES: * When exporting to a tarball with ``bzr export --format tgz``, put everything under a top directory rather than dumping it into the current directory. This can be overridden with the ``--root`` option. Patch from William Dodé and John Meinel. * New ``bzr upgrade`` command to upgrade the format of a branch, replacing ``bzr check --update``. * Files within store directories are no longer marked readonly on disk. * Changed ``bzr log`` output to a more compact form suggested by John A Meinel. Old format is available with the ``--long`` or ``-l`` option, patched by William Dodé. bzr-0.0.5 2005-06-15 CHANGES: * ``bzr`` with no command now shows help rather than giving an error. Suggested by Michael Ellerman. * ``bzr status`` output format changed, because svn-style output doesn't really match the model of bzr. Now files are grouped by status and can be shown with their IDs. ``bzr status --all`` shows all versioned files and unknown files but not ignored files. * ``bzr log`` runs from most-recent to least-recent, the reverse of the previous order. The previous behaviour can be obtained with the ``--forward`` option. * ``bzr inventory`` by default shows only filenames, and also ids if ``--show-ids`` is given, in which case the id is the second field. ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New ``bzr ignore PATTERN`` command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.sw[nop] .git .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. File may be in a branch other than the working directory. * ``bzr log`` and ``bzr root`` can be given an http URL instead of a filename. * Commands can now be defined by external programs or scripts in a directory on $BZRPATH. * New "stat cache" avoids reading the contents of files if they haven't changed since the previous time. * If the Python interpreter is too old, try to find a better one or give an error. Based on a patch from Fredrik Lundh. * New optional parameter ``bzr info [BRANCH]``. * New form ``bzr commit SELECTED`` to commit only selected files. * New form ``bzr log -r FROM:TO`` shows changes in selected range; contributed by John A Meinel. * New option ``bzr diff --diff-options 'OPTS'`` allows passing options through to an external GNU diff. * New option ``bzr add --no-recurse`` to add a directory but not their contents. * ``bzr --version`` now shows more information if bzr is being run from a branch. BUG FIXES: * Fixed diff format so that added and removed files will be handled properly by patch. Fix from Lalo Martins. * Various fixes for files whose names contain spaces or other metacharacters. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. * testbzr requires python2.4, but can be used to test bzr running under a different version. * Tests added for many other changes in this release. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. * New developer commands ``added``, ``modified``. PORTABILITY: * Cope on Windows on python2.3 by using the weaker random seed. 2.4 is now only recommended. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 53705 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import BzrError, BzrCheckError, BzrCommandError from bzrlib.branch import find_branch from bzrlib import BZRDIR plugin_cmds = {} def register_command(cmd): "Utility function to help register a command" global plugin_cmds k = cmd.__name__ if k.startswith("cmd_"): k_unsquished = _unsquish_command_name(k) else: k_unsquished = k if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = cmd else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r' % sys.modules[cmd.__module__]) def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _get_cmd_dict(plugins_override=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v # If we didn't load plugins, the plugin_cmds dict will be empty if plugins_override: d.update(plugin_cmds) else: d2 = plugin_cmds.copy() d2.update(d) d = d2 return d def get_all_cmds(plugins_override=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(plugins_override=plugins_override).iteritems(): yield k,v def get_cmd_class(cmd, plugins_override=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(plugins_override=plugins_override) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() for opt in self.takes_options: if not opt in OPTIONS: raise BzrError("Unknown option '%s' returned by external command %s" % (opt, path)) # TODO: Is there any way to check takes_args is valid here? self.takes_args = pipe.readline().split() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-usage'" % path) pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-help'" % path) def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: optname = name.replace('_','-') value = kargs[name] if OPTIONS.has_key(optname): # it's an option opts.append('--%s' % optname) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = find_branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): from bzrlib.xml import pack_xml pack_xml(find_branch('.').get_revision(revision_id), sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print find_branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): from bzrlib.add import smart_add smart_add(file_list, verbose, not no_recurse) class cmd_mkdir(Command): """Create a new versioned directory. This is equivalent to creating the directory and then adding it. """ takes_args = ['dir+'] def run(self, dir_list): b = None for d in dir_list: os.mkdir(d) if not b: b = find_branch(d) b.add([d], verbose=True) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print find_branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = find_branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = find_branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = find_branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import tempfile from shutil import rmtree import errno br_to = find_branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if e.errno != errno.ENOENT: raise if location is None: if stored_loc is None: raise BzrCommandError("No pull location known or specified.") else: print "Using last location: %s" % stored_loc location = stored_loc cache_root = tempfile.mkdtemp() from bzrlib.branch import DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: from branch import find_cached_branch, DivergedBranches br_from = find_cached_branch(location, cache_root) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged." " Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") finally: rmtree(cache_root) class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. To retrieve the branch as of a particular revision, supply the --revision parameter, as in "branch foo/bar -r 5". """ takes_args = ['from_location', 'to_location?'] takes_options = ['revision'] def run(self, from_location, to_location=None, revision=None): import errno from bzrlib.merge import merge from bzrlib.branch import DivergedBranches, NoSuchRevision, \ find_cached_branch, Branch from shutil import rmtree from meta_store import CachedStore import tempfile cache_root = tempfile.mkdtemp() try: try: br_from = find_cached_branch(from_location, cache_root) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not' ' exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location.rstrip("/\\")) try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already' ' exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) try: br_to.update_revisions(br_from, stop_revision=revision) except NoSuchRevision: rmtree(to_location) msg = "The branch %s has no revision %d." % (from_location, revision) raise BzrCommandError(msg) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False, ignore_zero=True) from_location = pull_loc(br_from) br_to.controlfile("x-pull", "wb").write(from_location + "\n") finally: rmtree(cache_root) def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = find_branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = find_branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = find_branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: raise BzrError("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = find_branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: raise BzrError("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in find_branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in find_branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): from bzrlib.branch import Branch Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = find_branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = find_branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): from bzrlib.statcache import update_cache, SC_SHA1 b = find_branch('.') inv = b.read_working_inventory() sc = update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = find_branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision','long'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None, long=False): from bzrlib.branch import find_branch from bzrlib.log import log_formatter, show_log import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') if long: log_format = 'long' else: log_format = 'short' lf = log_formatter(log_format, show_ids=show_ids, to_file=outf, show_timezone=timezone) show_log(b, lf, file_id, verbose=verbose, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = find_branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = find_branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): from bzrlib.osutils import quotefn for f in find_branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = find_branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = find_branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print find_branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, exports to a directory (equivalent to --format=dir). Root may be the top directory for tar, tgz and tbz2 formats.""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format', 'root'] def run(self, dest, revision=None, format='dir', root=None): b = find_branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) t.export(dest, format, root) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = find_branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit from bzrlib.osutils import get_text_message ## Warning: shadows builtin file() if not message and not file: import cStringIO stdout = sys.stdout catcher = cStringIO.StringIO() sys.stdout = catcher cmd_status({"file_list":selected_list}, {}) info = catcher.getvalue() sys.stdout = stdout message = get_text_message(info) if message is None: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = find_branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.check import check check(find_branch(dir)) class cmd_upgrade(Command): """Upgrade branch storage to current format. This should normally be used only after the check command tells you to run it. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.upgrade import upgrade upgrade(find_branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest return int(not selftest()) class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Restore selected files from a previous revision. """ takes_args = ['file+'] def run(self, file_list): from bzrlib.branch import find_branch if not file_list: file_list = ['.'] b = find_branch(file_list[0]) b.revert([b.relpath(f) for f in file_list]) class cmd_merge_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): from bzrlib.statcache import update_cache b = find_branch('.') update_cache(b.base, b.read_working_inventory()) class cmd_plugins(Command): """List plugins""" hidden = True def run(self): import bzrlib.plugin from pprint import pprint pprint(bzrlib.plugin.all_plugins) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'update': None, 'long': None, 'root': str, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', 'l': 'long', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) >>> parse_args('log -r 500'.split()) (['log'], {'revision': 500}) >>> parse_args('log -r500:600'.split()) (['log'], {'revision': [500, 600]}) >>> parse_args('log -vr500:600'.split()) (['log'], {'verbose': True, 'revision': [500, 600]}) >>> parse_args('log -rv500:600'.split()) #the r takes an argument Traceback (most recent call last): ... ValueError: invalid literal for int(): v500 """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: raise BzrError('unknown long option %r' % a) else: shortopt = a[1:] if shortopt in SHORT_OPTIONS: # Multi-character options must have a space to delimit # their value optname = SHORT_OPTIONS[shortopt] else: # Single character short options, can be chained, # and have their value appended to their name shortopt = a[1:2] if shortopt not in SHORT_OPTIONS: # We didn't find the multi-character name, and we # didn't find the single char name raise BzrError('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if a[2:]: # There are extra things on this option # see if it is the value, or if it is another # short option optargfn = OPTIONS[optname] if optargfn is None: # This option does not take an argument, so the # next entry is another short option, pack it back # into the list argv.insert(0, '-' + a[2:]) else: # This option takes an argument, so pack it # into the array optarg = a[2:] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? raise BzrError('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: raise BzrError('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: raise BzrError('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def _parse_master_args(argv): """Parse the arguments that always go with the original command. These are things like bzr --no-plugins, etc. There are now 2 types of option flags. Ones that come *before* the command, and ones that come *after* the command. Ones coming *before* the command are applied against all possible commands. And are generally applied before plugins are loaded. The current list are: --builtin Allow plugins to load, but don't let them override builtin commands, they will still be allowed if they do not override a builtin. --no-plugins Don't load any plugins. This lets you get back to official source behavior. --profile Enable the hotspot profile before running the command. For backwards compatibility, this is also a non-master option. --version Spit out the version of bzr that is running and exit. This is also a non-master option. --help Run help and exit, also a non-master option (I think that should stay, though) >>> argv, opts = _parse_master_args(['bzr', '--test']) Traceback (most recent call last): ... BzrCommandError: Invalid master option: 'test' >>> argv, opts = _parse_master_args(['bzr', '--version', 'command']) >>> print argv ['command'] >>> print opts['version'] True >>> argv, opts = _parse_master_args(['bzr', '--profile', 'command', '--more-options']) >>> print argv ['command', '--more-options'] >>> print opts['profile'] True >>> argv, opts = _parse_master_args(['bzr', '--no-plugins', 'command']) >>> print argv ['command'] >>> print opts['no-plugins'] True >>> print opts['profile'] False >>> argv, opts = _parse_master_args(['bzr', 'command', '--profile']) >>> print argv ['command', '--profile'] >>> print opts['profile'] False """ master_opts = {'builtin':False, 'no-plugins':False, 'version':False, 'profile':False, 'help':False } # This is the point where we could hook into argv[0] to determine # what front-end is supposed to be run # For now, we are just ignoring it. cmd_name = argv.pop(0) for arg in argv[:]: if arg[:2] != '--': # at the first non-option, we return the rest break arg = arg[2:] # Remove '--' if arg not in master_opts: # We could say that this is not an error, that we should # just let it be handled by the main section instead raise BzrCommandError('Invalid master option: %r' % arg) argv.pop(0) # We are consuming this entry master_opts[arg] = True return argv, master_opts def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: # some options like --builtin and --no-plugins have special effects argv, master_opts = _parse_master_args(argv) if not master_opts['no-plugins']: from bzrlib.plugin import load_plugins load_plugins() args, opts = parse_args(argv) if master_opts['help']: from bzrlib.help import help if argv: help(argv[0]) else: help() return 0 if 'help' in opts: from bzrlib.help import help if args: help(args[0]) else: help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 plugins_override = not (master_opts['builtin']) canonical_cmd, cmd_class = get_cmd_class(cmd, plugins_override=plugins_override) profile = master_opts['profile'] # For backwards compatibility, I would rather stick with --profile being a # master/global option if 'profile' in opts: profile = True del opts['profile'] # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): bzrlib.trace.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: import errno quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/tree.py data 11066 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Tree classes, representing directory at point in time. """ from osutils import pumpfile, appendpath, fingerprint_file import os from bzrlib.trace import mutter, note from bzrlib.errors import BzrError import bzrlib exporters = {} class Tree(object): """Abstract file tree. There are several subclasses: * `WorkingTree` exists as files on disk editable by the user. * `RevisionTree` is a tree as recorded at some point in the past. * `EmptyTree` Trees contain an `Inventory` object, and also know how to retrieve file texts mentioned in the inventory, either from a working directory or from a store. It is possible for trees to contain files that are not described in their inventory or vice versa; for this use `filenames()`. Trees can be compared, etc, regardless of whether they are working trees or versioned trees. """ def has_filename(self, filename): """True if the tree has given filename.""" raise NotImplementedError() def has_id(self, file_id): return self.inventory.has_id(file_id) __contains__ = has_id def __iter__(self): return iter(self.inventory) def id2path(self, file_id): return self.inventory.id2path(file_id) def _get_inventory(self): return self._inventory inventory = property(_get_inventory, doc="Inventory of this Tree") def _check_retrieved(self, ie, f): fp = fingerprint_file(f) f.seek(0) if ie.text_size != None: if ie.text_size != fp['size']: raise BzrError("mismatched size for file %r in %r" % (ie.file_id, self._store), ["inventory expects %d bytes" % ie.text_size, "file is actually %d bytes" % fp['size'], "store is probably damaged/corrupt"]) if ie.text_sha1 != fp['sha1']: raise BzrError("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store), ["inventory expects %s" % ie.text_sha1, "file is actually %s" % fp['sha1'], "store is probably damaged/corrupt"]) def print_file(self, fileid): """Print file with id `fileid` to stdout.""" import sys pumpfile(self.get_file(fileid), sys.stdout) def export(self, dest, format='dir', root=None): """Export this tree.""" try: exporter = exporters[format] except KeyError: from bzrlib.errors import BzrCommandError raise BzrCommandError("export format %r not supported" % format) exporter(self, dest, root) class RevisionTree(Tree): """Tree viewing a previous revision. File text can be retrieved from the text store. TODO: Some kind of `__repr__` method, but a good one probably means knowing the branch and revision number, or at least passing a description to the constructor. """ def __init__(self, store, inv): self._store = store self._inventory = inv def get_file(self, file_id): ie = self._inventory[file_id] f = self._store[ie.text_id] mutter(" get fileid{%s} from %r" % (file_id, self)) self._check_retrieved(ie, f) return f def get_file_size(self, file_id): return self._inventory[file_id].text_size def get_file_sha1(self, file_id): ie = self._inventory[file_id] return ie.text_sha1 def has_filename(self, filename): return bool(self.inventory.path2id(filename)) def list_files(self): # The only files returned by this are those from the version for path, entry in self.inventory.iter_entries(): yield path, 'V', entry.kind, entry.file_id class EmptyTree(Tree): def __init__(self): from bzrlib.inventory import Inventory self._inventory = Inventory() def has_filename(self, filename): return False def list_files(self): if False: # just to make it a generator yield None ###################################################################### # diff # TODO: Merge these two functions into a single one that can operate # on either a whole tree or a set of files. # TODO: Return the diff in order by filename, not by category or in # random order. Can probably be done by lock-stepping through the # filenames from both trees. def file_status(filename, old_tree, new_tree): """Return single-letter status, old and new names for a file. The complexity here is in deciding how to represent renames; many complex cases are possible. """ old_inv = old_tree.inventory new_inv = new_tree.inventory new_id = new_inv.path2id(filename) old_id = old_inv.path2id(filename) if not new_id and not old_id: # easy: doesn't exist in either; not versioned at all if new_tree.is_ignored(filename): return 'I', None, None else: return '?', None, None elif new_id: # There is now a file of this name, great. pass else: # There is no longer a file of this name, but we can describe # what happened to the file that used to have # this name. There are two possibilities: either it was # deleted entirely, or renamed. assert old_id if new_inv.has_id(old_id): return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id) else: return 'D', old_inv.id2path(old_id), None # if the file_id is new in this revision, it is added if new_id and not old_inv.has_id(new_id): return 'A' # if there used to be a file of this name, but that ID has now # disappeared, it is deleted if old_id and not new_inv.has_id(old_id): return 'D' return 'wtf?' def find_renames(old_inv, new_inv): for file_id in old_inv: if file_id not in new_inv: continue old_name = old_inv.id2path(file_id) new_name = new_inv.id2path(file_id) if old_name != new_name: yield (old_name, new_name) ###################################################################### # export def dir_exporter(tree, dest, root): """Export this tree to a new directory. `dest` should not exist, and will be created holding the contents of this tree. TODO: To handle subdirectories we need to create the directories first. :note: If the export fails, the destination directory will be left in a half-assed state. """ import os os.mkdir(dest) mutter('export version %r' % tree) inv = tree.inventory for dp, ie in inv.iter_entries(): kind = ie.kind fullpath = appendpath(dest, dp) if kind == 'directory': os.mkdir(fullpath) elif kind == 'file': pumpfile(tree.get_file(ie.file_id), file(fullpath, 'wb')) else: raise BzrError("don't know how to export {%s} of kind %r" % (ie.file_id, kind)) mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath)) exporters['dir'] = dir_exporter try: import tarfile except ImportError: pass else: def get_root_name(dest): """Get just the root name for a tarball. >>> get_root_name('mytar.tar') 'mytar' >>> get_root_name('mytar.tar.bz2') 'mytar' >>> get_root_name('tar.tar.tar.tgz') 'tar.tar.tar' >>> get_root_name('bzr-0.0.5.tar.gz') 'bzr-0.0.5' >>> get_root_name('a/long/path/mytar.tgz') 'mytar' >>> get_root_name('../parent/../dir/other.tbz2') 'other' """ endings = ['.tar', '.tar.gz', '.tgz', '.tar.bz2', '.tbz2'] dest = os.path.basename(dest) for end in endings: if dest.endswith(end): return dest[:-len(end)] def tar_exporter(tree, dest, root, compression=None): """Export this tree to a new tar file. `dest` will be created holding the contents of this tree; if it already exists, it will be clobbered, like with "tar -c". """ from time import time now = time() compression = str(compression or '') if root is None: root = get_root_name(dest) try: ball = tarfile.open(dest, 'w:' + compression) except tarfile.CompressionError, e: raise BzrError(str(e)) mutter('export version %r' % tree) inv = tree.inventory for dp, ie in inv.iter_entries(): mutter(" export {%s} kind %s to %s" % (ie.file_id, ie.kind, dest)) item = tarfile.TarInfo(os.path.join(root, dp)) # TODO: would be cool to actually set it to the timestamp of the # revision it was last changed item.mtime = now if ie.kind == 'directory': item.type = tarfile.DIRTYPE fileobj = None item.name += '/' item.size = 0 item.mode = 0755 elif ie.kind == 'file': item.type = tarfile.REGTYPE fileobj = tree.get_file(ie.file_id) item.size = _find_file_size(fileobj) item.mode = 0644 else: raise BzrError("don't know how to export {%s} of kind %r" % (ie.file_id, ie.kind)) ball.addfile(item, fileobj) ball.close() exporters['tar'] = tar_exporter def tgz_exporter(tree, dest, root): tar_exporter(tree, dest, root, compression='gz') exporters['tgz'] = tgz_exporter def tbz_exporter(tree, dest, root): tar_exporter(tree, dest, root, compression='bz2') exporters['tbz2'] = tbz_exporter def _find_file_size(fileobj): offset = fileobj.tell() try: fileobj.seek(0, 2) size = fileobj.tell() except TypeError: # gzip doesn't accept second argument to seek() fileobj.seek(0) size = 0 while True: nread = len(fileobj.read()) if nread == 0: break size += nread fileobj.seek(offset) return size commit refs/heads/master mark :850 committer Martin Pool 1120723336 +1000 data 94 - Merge merge updates from aaron aaron.bentley@utoronto.ca-20050706170931-9d2551019af3578d from :849 M 644 inline TODO data 13225 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Merge should ignore the destination's working directory, otherwise we get an error about the statcache when pulling from a remote branch. * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * -r option should take a revision-id as well as a revno. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * ``bzr log DIR`` should give changes to any files within DIR. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. Perhaps ~/.bzr/http-cache. Baz has a fairly simple cache under ~/.arch-cache, containing revision information encoded almost as a bunch of archives. Perhaps we could simply store full paths. * Maybe also store directories in the statcache so that we can quickly identify that they still exist. * Diff should show timestamps; for files from the working directory we can use the file itself; for files from a revision we should use the commit time of the revision. * Perhaps split command infrastructure from the actual command definitions. * Cleaner support for negative boolean options like --no-recurse. * Statcache should possibly map all file paths to / separators * quotefn doubles all backslashes on Windows; this is probably not the best thing to do. What would be a better way to safely represent filenames? Perhaps we could doublequote things containing spaces, on the principle that filenames containing quotes are unlikely? Nice for humans; less good for machine parsing. * Patches should probably use only forward slashes, even on Windows, otherwise Unix patch can't apply them. (?) * Branch.update_revisions() inefficiently fetches revisions from the remote server twice; once to find out what text and inventory they need and then again to actually get the thing. This is a bit inefficient. One complicating factor here is that we don't really want to have revisions present in the revision-store until all their constituent parts are also stored. The basic problem is that RemoteBranch.get_revision() and similar methods return object, but what we really want is the raw XML, which can be popped into our own store. That needs to be refactored. * ``bzr status FOO`` where foo is ignored should say so. * ``bzr mkdir A...`` should just create and add A. * Guard against repeatedly merging any particular patch. Medium things ------------- * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. We should be able to just get the id for the selected files, look up their location and diff just those files. No need to traverse the entire inventories. * ``bzr status DIR`` or ``bzr diff DIR`` should report on all changes under that directory. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from ElementTree to an object when it is read in, but rather wait until the program actually wants to know about that node. * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. - Selected-file commit - Impossible selected-file commit: adding things in non-versioned directories, crossing renames, etc. * Write a reproducible benchmark, perhaps importing various kernel versions. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Paranoid mode where we never trust SHA-1 matches. * Don't commit if there are no changes unless forced. * --dry-run mode for commit? (Or maybe just run with check-command=false?) * Generally, be a bit more verbose unless --silent is specified. * Function that finds all changes to files under a given directory; perhaps log should use this if a directory is given. * XML attributes might have trouble with filenames containing \n and \r. Do we really want to support this? I think perhaps not. * Remember execute bits, so that exports will work OK. * Unify smart_add and plain Branch.add(); perhaps smart_add should just build a list of files to add and pass that to the regular add function. * Function to list a directory, saying in which revision each file was last modified. Useful for web and gui interfaces, and slow to compute one file at a time. * unittest is standard, but the results are kind of ugly; would be nice to make it cleaner. * Check locking is correct during merge-related operations. * Perhaps attempts to get locks should timeout after some period of time, or at least display a progress message. * Split out upgrade functionality from check command into a separate ``bzr upgrade``. * Don't pass around command classes but rather pass objects. This'd make it cleaner to construct objects wrapping external commands. * Track all merged-in revisions in a versioned add-only metafile. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. Possibly this should be done by splitting the commit function into several parts (under a single interface). It is already rather large. Decomposition: - find tree modifications and prepare in-memory inventory - export that inventory to a temporary directory - run the test in that temporary directory - if that succeeded, continue to actually finish the commit What should be done with the text of modified files while this is underway? I don't think we want to count on holding them in memory and we can't trust the working files to stay in one place so I suppose we need to move them into the text store, or otherwise into a temporary directory. If the commit does not actually complete, we would rather the content was not left behind in the stores. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` M 644 inline bzrlib/changeset.py data 54592 # Copyright (C) 2004 Aaron Bentley # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os.path import errno import patch import stat """ Represent and apply a changeset """ __docformat__ = "restructuredtext" NULL_ID = "!NULL" class OldFailedTreeOp(Exception): def __init__(self): Exception.__init__(self, "bzr-tree-change contains files from a" " previous failed merge operation.") def invert_dict(dict): newdict = {} for (key,value) in dict.iteritems(): newdict[value] = key return newdict class PatchApply(object): """Patch application as a kind of content change""" def __init__(self, contents): """Constructor. :param contents: The text of the patch to apply :type contents: str""" self.contents = contents def __eq__(self, other): if not isinstance(other, PatchApply): return False elif self.contents != other.contents: return False else: return True def __ne__(self, other): return not (self == other) def apply(self, filename, conflict_handler, reverse=False): """Applies the patch to the specified file. :param filename: the file to apply the patch to :type filename: str :param reverse: If true, apply the patch in reverse :type reverse: bool """ input_name = filename+".orig" try: os.rename(filename, input_name) except OSError, e: if e.errno != errno.ENOENT: raise if conflict_handler.patch_target_missing(filename, self.contents)\ == "skip": return os.rename(filename, input_name) status = patch.patch(self.contents, input_name, filename, reverse) os.chmod(filename, os.stat(input_name).st_mode) if status == 0: os.unlink(input_name) elif status == 1: conflict_handler.failed_hunks(filename) class ChangeUnixPermissions(object): """This is two-way change, suitable for file modification, creation, deletion""" def __init__(self, old_mode, new_mode): self.old_mode = old_mode self.new_mode = new_mode def apply(self, filename, conflict_handler, reverse=False): if not reverse: from_mode = self.old_mode to_mode = self.new_mode else: from_mode = self.new_mode to_mode = self.old_mode try: current_mode = os.stat(filename).st_mode &0777 except OSError, e: if e.errno == errno.ENOENT: if conflict_handler.missing_for_chmod(filename) == "skip": return else: current_mode = from_mode if from_mode is not None and current_mode != from_mode: if conflict_handler.wrong_old_perms(filename, from_mode, current_mode) != "continue": return if to_mode is not None: try: os.chmod(filename, to_mode) except IOError, e: if e.errno == errno.ENOENT: conflict_handler.missing_for_chmod(filename) def __eq__(self, other): if not isinstance(other, ChangeUnixPermissions): return False elif self.old_mode != other.old_mode: return False elif self.new_mode != other.new_mode: return False else: return True def __ne__(self, other): return not (self == other) def dir_create(filename, conflict_handler, reverse): """Creates the directory, or deletes it if reverse is true. Intended to be used with ReplaceContents. :param filename: The name of the directory to create :type filename: str :param reverse: If true, delete the directory, instead :type reverse: bool """ if not reverse: try: os.mkdir(filename) except OSError, e: if e.errno != errno.EEXIST: raise if conflict_handler.dir_exists(filename) == "continue": os.mkdir(filename) except IOError, e: if e.errno == errno.ENOENT: if conflict_handler.missing_parent(filename)=="continue": file(filename, "wb").write(self.contents) else: try: os.rmdir(filename) except OSError, e: if e.errno != 39: raise if conflict_handler.rmdir_non_empty(filename) == "skip": return os.rmdir(filename) class SymlinkCreate(object): """Creates or deletes a symlink (for use with ReplaceContents)""" def __init__(self, contents): """Constructor. :param contents: The filename of the target the symlink should point to :type contents: str """ self.target = contents def __call__(self, filename, conflict_handler, reverse): """Creates or destroys the symlink. :param filename: The name of the symlink to create :type filename: str """ if reverse: assert(os.readlink(filename) == self.target) os.unlink(filename) else: try: os.symlink(self.target, filename) except OSError, e: if e.errno != errno.EEXIST: raise if conflict_handler.link_name_exists(filename) == "continue": os.symlink(self.target, filename) def __eq__(self, other): if not isinstance(other, SymlinkCreate): return False elif self.target != other.target: return False else: return True def __ne__(self, other): return not (self == other) class FileCreate(object): """Create or delete a file (for use with ReplaceContents)""" def __init__(self, contents): """Constructor :param contents: The contents of the file to write :type contents: str """ self.contents = contents def __repr__(self): return "FileCreate(%i b)" % len(self.contents) def __eq__(self, other): if not isinstance(other, FileCreate): return False elif self.contents != other.contents: return False else: return True def __ne__(self, other): return not (self == other) def __call__(self, filename, conflict_handler, reverse): """Create or delete a file :param filename: The name of the file to create :type filename: str :param reverse: Delete the file instead of creating it :type reverse: bool """ if not reverse: try: file(filename, "wb").write(self.contents) except IOError, e: if e.errno == errno.ENOENT: if conflict_handler.missing_parent(filename)=="continue": file(filename, "wb").write(self.contents) else: raise else: try: if (file(filename, "rb").read() != self.contents): direction = conflict_handler.wrong_old_contents(filename, self.contents) if direction != "continue": return os.unlink(filename) except IOError, e: if e.errno != errno.ENOENT: raise if conflict_handler.missing_for_rm(filename, undo) == "skip": return def reversed(sequence): max = len(sequence) - 1 for i in range(len(sequence)): yield sequence[max - i] class ReplaceContents(object): """A contents-replacement framework. It allows a file/directory/symlink to be created, deleted, or replaced with another file/directory/symlink. Arguments must be callable with (filename, reverse). """ def __init__(self, old_contents, new_contents): """Constructor. :param old_contents: The change to reverse apply (e.g. a deletion), \ when going forwards. :type old_contents: `dir_create`, `SymlinkCreate`, `FileCreate`, \ NoneType, etc. :param new_contents: The second change to apply (e.g. a creation), \ when going forwards. :type new_contents: `dir_create`, `SymlinkCreate`, `FileCreate`, \ NoneType, etc. """ self.old_contents=old_contents self.new_contents=new_contents def __repr__(self): return "ReplaceContents(%r -> %r)" % (self.old_contents, self.new_contents) def __eq__(self, other): if not isinstance(other, ReplaceContents): return False elif self.old_contents != other.old_contents: return False elif self.new_contents != other.new_contents: return False else: return True def __ne__(self, other): return not (self == other) def apply(self, filename, conflict_handler, reverse=False): """Applies the FileReplacement to the specified filename :param filename: The name of the file to apply changes to :type filename: str :param reverse: If true, apply the change in reverse :type reverse: bool """ if not reverse: undo = self.old_contents perform = self.new_contents else: undo = self.new_contents perform = self.old_contents mode = None if undo is not None: try: mode = os.lstat(filename).st_mode if stat.S_ISLNK(mode): mode = None except OSError, e: if e.errno != errno.ENOENT: raise if conflict_handler.missing_for_rm(filename, undo) == "skip": return undo(filename, conflict_handler, reverse=True) if perform is not None: perform(filename, conflict_handler, reverse=False) if mode is not None: os.chmod(filename, mode) class ApplySequence(object): def __init__(self, changes=None): self.changes = [] if changes is not None: self.changes.extend(changes) def __eq__(self, other): if not isinstance(other, ApplySequence): return False elif len(other.changes) != len(self.changes): return False else: for i in range(len(self.changes)): if self.changes[i] != other.changes[i]: return False return True def __ne__(self, other): return not (self == other) def apply(self, filename, conflict_handler, reverse=False): if not reverse: iter = self.changes else: iter = reversed(self.changes) for change in iter: change.apply(filename, conflict_handler, reverse) class Diff3Merge(object): def __init__(self, base_file, other_file): self.base_file = base_file self.other_file = other_file def __eq__(self, other): if not isinstance(other, Diff3Merge): return False return (self.base_file == other.base_file and self.other_file == other.other_file) def __ne__(self, other): return not (self == other) def apply(self, filename, conflict_handler, reverse=False): new_file = filename+".new" if not reverse: base = self.base_file other = self.other_file else: base = self.other_file other = self.base_file status = patch.diff3(new_file, filename, base, other) if status == 0: os.chmod(new_file, os.stat(filename).st_mode) os.rename(new_file, filename) return else: assert(status == 1) conflict_handler.merge_conflict(new_file, filename, base, other) def CreateDir(): """Convenience function to create a directory. :return: A ReplaceContents that will create a directory :rtype: `ReplaceContents` """ return ReplaceContents(None, dir_create) def DeleteDir(): """Convenience function to delete a directory. :return: A ReplaceContents that will delete a directory :rtype: `ReplaceContents` """ return ReplaceContents(dir_create, None) def CreateFile(contents): """Convenience fucntion to create a file. :param contents: The contents of the file to create :type contents: str :return: A ReplaceContents that will create a file :rtype: `ReplaceContents` """ return ReplaceContents(None, FileCreate(contents)) def DeleteFile(contents): """Convenience fucntion to delete a file. :param contents: The contents of the file to delete :type contents: str :return: A ReplaceContents that will delete a file :rtype: `ReplaceContents` """ return ReplaceContents(FileCreate(contents), None) def ReplaceFileContents(old_contents, new_contents): """Convenience fucntion to replace the contents of a file. :param old_contents: The contents of the file to replace :type old_contents: str :param new_contents: The contents to replace the file with :type new_contents: str :return: A ReplaceContents that will replace the contents of a file a file :rtype: `ReplaceContents` """ return ReplaceContents(FileCreate(old_contents), FileCreate(new_contents)) def CreateSymlink(target): """Convenience fucntion to create a symlink. :param target: The path the link should point to :type target: str :return: A ReplaceContents that will delete a file :rtype: `ReplaceContents` """ return ReplaceContents(None, SymlinkCreate(target)) def DeleteSymlink(target): """Convenience fucntion to delete a symlink. :param target: The path the link should point to :type target: str :return: A ReplaceContents that will delete a file :rtype: `ReplaceContents` """ return ReplaceContents(SymlinkCreate(target), None) def ChangeTarget(old_target, new_target): """Convenience fucntion to change the target of a symlink. :param old_target: The current link target :type old_target: str :param new_target: The new link target to use :type new_target: str :return: A ReplaceContents that will delete a file :rtype: `ReplaceContents` """ return ReplaceContents(SymlinkCreate(old_target), SymlinkCreate(new_target)) class InvalidEntry(Exception): """Raise when a ChangesetEntry is invalid in some way""" def __init__(self, entry, problem): """Constructor. :param entry: The invalid ChangesetEntry :type entry: `ChangesetEntry` :param problem: The problem with the entry :type problem: str """ msg = "Changeset entry for %s (%s) is invalid.\n%s" % (entry.id, entry.path, problem) Exception.__init__(self, msg) self.entry = entry class SourceRootHasName(InvalidEntry): """This changeset entry has a name other than "", but its parent is !NULL""" def __init__(self, entry, name): """Constructor. :param entry: The invalid ChangesetEntry :type entry: `ChangesetEntry` :param name: The name of the entry :type name: str """ msg = 'Child of !NULL is named "%s", not "./.".' % name InvalidEntry.__init__(self, entry, msg) class NullIDAssigned(InvalidEntry): """The id !NULL was assigned to a real entry""" def __init__(self, entry): """Constructor. :param entry: The invalid ChangesetEntry :type entry: `ChangesetEntry` """ msg = '"!NULL" id assigned to a file "%s".' % entry.path InvalidEntry.__init__(self, entry, msg) class ParentIDIsSelf(InvalidEntry): """An entry is marked as its own parent""" def __init__(self, entry): """Constructor. :param entry: The invalid ChangesetEntry :type entry: `ChangesetEntry` """ msg = 'file %s has "%s" id for both self id and parent id.' % \ (entry.path, entry.id) InvalidEntry.__init__(self, entry, msg) class ChangesetEntry(object): """An entry the changeset""" def __init__(self, id, parent, path): """Constructor. Sets parent and name assuming it was not renamed/created/deleted. :param id: The id associated with the entry :param parent: The id of the parent of this entry (or !NULL if no parent) :param path: The file path relative to the tree root of this entry """ self.id = id self.path = path self.new_path = path self.parent = parent self.new_parent = parent self.contents_change = None self.metadata_change = None if parent == NULL_ID and path !='./.': raise SourceRootHasName(self, path) if self.id == NULL_ID: raise NullIDAssigned(self) if self.id == self.parent: raise ParentIDIsSelf(self) def __str__(self): return "ChangesetEntry(%s)" % self.id def __get_dir(self): if self.path is None: return None return os.path.dirname(self.path) def __set_dir(self, dir): self.path = os.path.join(dir, os.path.basename(self.path)) dir = property(__get_dir, __set_dir) def __get_name(self): if self.path is None: return None return os.path.basename(self.path) def __set_name(self, name): self.path = os.path.join(os.path.dirname(self.path), name) name = property(__get_name, __set_name) def __get_new_dir(self): if self.new_path is None: return None return os.path.dirname(self.new_path) def __set_new_dir(self, dir): self.new_path = os.path.join(dir, os.path.basename(self.new_path)) new_dir = property(__get_new_dir, __set_new_dir) def __get_new_name(self): if self.new_path is None: return None return os.path.basename(self.new_path) def __set_new_name(self, name): self.new_path = os.path.join(os.path.dirname(self.new_path), name) new_name = property(__get_new_name, __set_new_name) def needs_rename(self): """Determines whether the entry requires renaming. :rtype: bool """ return (self.parent != self.new_parent or self.name != self.new_name) def is_deletion(self, reverse): """Return true if applying the entry would delete a file/directory. :param reverse: if true, the changeset is being applied in reverse :rtype: bool """ return ((self.new_parent is None and not reverse) or (self.parent is None and reverse)) def is_creation(self, reverse): """Return true if applying the entry would create a file/directory. :param reverse: if true, the changeset is being applied in reverse :rtype: bool """ return ((self.parent is None and not reverse) or (self.new_parent is None and reverse)) def is_creation_or_deletion(self): """Return true if applying the entry would create or delete a file/directory. :rtype: bool """ return self.parent is None or self.new_parent is None def get_cset_path(self, mod=False): """Determine the path of the entry according to the changeset. :param changeset: The changeset to derive the path from :type changeset: `Changeset` :param mod: If true, generate the MOD path. Otherwise, generate the \ ORIG path. :return: the path of the entry, or None if it did not exist in the \ requested tree. :rtype: str or NoneType """ if mod: if self.new_parent == NULL_ID: return "./." elif self.new_parent is None: return None return self.new_path else: if self.parent == NULL_ID: return "./." elif self.parent is None: return None return self.path def summarize_name(self, changeset, reverse=False): """Produce a one-line summary of the filename. Indicates renames as old => new, indicates creation as None => new, indicates deletion as old => None. :param changeset: The changeset to get paths from :type changeset: `Changeset` :param reverse: If true, reverse the names in the output :type reverse: bool :rtype: str """ orig_path = self.get_cset_path(False) mod_path = self.get_cset_path(True) if orig_path is not None: orig_path = orig_path[2:] if mod_path is not None: mod_path = mod_path[2:] if orig_path == mod_path: return orig_path else: if not reverse: return "%s => %s" % (orig_path, mod_path) else: return "%s => %s" % (mod_path, orig_path) def get_new_path(self, id_map, changeset, reverse=False): """Determine the full pathname to rename to :param id_map: The map of ids to filenames for the tree :type id_map: Dictionary :param changeset: The changeset to get data from :type changeset: `Changeset` :param reverse: If true, we're applying the changeset in reverse :type reverse: bool :rtype: str """ if reverse: parent = self.parent to_dir = self.dir from_dir = self.new_dir to_name = self.name from_name = self.new_name else: parent = self.new_parent to_dir = self.new_dir from_dir = self.dir to_name = self.new_name from_name = self.name if to_name is None: return None if parent == NULL_ID or parent is None: if to_name != '.': raise SourceRootHasName(self, to_name) else: return '.' if from_dir == to_dir: dir = os.path.dirname(id_map[self.id]) else: parent_entry = changeset.entries[parent] dir = parent_entry.get_new_path(id_map, changeset, reverse) if from_name == to_name: name = os.path.basename(id_map[self.id]) else: name = to_name assert(from_name is None or from_name == os.path.basename(id_map[self.id])) return os.path.join(dir, name) def is_boring(self): """Determines whether the entry does nothing :return: True if the entry does no renames or content changes :rtype: bool """ if self.contents_change is not None: return False elif self.metadata_change is not None: return False elif self.parent != self.new_parent: return False elif self.name != self.new_name: return False else: return True def apply(self, filename, conflict_handler, reverse=False): """Applies the file content and/or metadata changes. :param filename: the filename of the entry :type filename: str :param reverse: If true, apply the changes in reverse :type reverse: bool """ if self.is_deletion(reverse) and self.metadata_change is not None: self.metadata_change.apply(filename, conflict_handler, reverse) if self.contents_change is not None: self.contents_change.apply(filename, conflict_handler, reverse) if not self.is_deletion(reverse) and self.metadata_change is not None: self.metadata_change.apply(filename, conflict_handler, reverse) class IDPresent(Exception): def __init__(self, id): msg = "Cannot add entry because that id has already been used:\n%s" %\ id Exception.__init__(self, msg) self.id = id class Changeset(object): """A set of changes to apply""" def __init__(self): self.entries = {} def add_entry(self, entry): """Add an entry to the list of entries""" if self.entries.has_key(entry.id): raise IDPresent(entry.id) self.entries[entry.id] = entry def my_sort(sequence, key, reverse=False): """A sort function that supports supplying a key for comparison :param sequence: The sequence to sort :param key: A callable object that returns the values to be compared :param reverse: If true, sort in reverse order :type reverse: bool """ def cmp_by_key(entry_a, entry_b): if reverse: tmp=entry_a entry_a = entry_b entry_b = tmp return cmp(key(entry_a), key(entry_b)) sequence.sort(cmp_by_key) def get_rename_entries(changeset, inventory, reverse): """Return a list of entries that will be renamed. Entries are sorted from longest to shortest source path and from shortest to longest target path. :param changeset: The changeset to look in :type changeset: `Changeset` :param inventory: The source of current tree paths for the given ids :type inventory: Dictionary :param reverse: If true, the changeset is being applied in reverse :type reverse: bool :return: source entries and target entries as a tuple :rtype: (List, List) """ source_entries = [x for x in changeset.entries.itervalues() if x.needs_rename()] # these are done from longest path to shortest, to avoid deleting a # parent before its children are deleted/renamed def longest_to_shortest(entry): path = inventory.get(entry.id) if path is None: return 0 else: return len(path) my_sort(source_entries, longest_to_shortest, reverse=True) target_entries = source_entries[:] # These are done from shortest to longest path, to avoid creating a # child before its parent has been created/renamed def shortest_to_longest(entry): path = entry.get_new_path(inventory, changeset, reverse) if path is None: return 0 else: return len(path) my_sort(target_entries, shortest_to_longest) return (source_entries, target_entries) def rename_to_temp_delete(source_entries, inventory, dir, temp_dir, conflict_handler, reverse): """Delete and rename entries as appropriate. Entries are renamed to temp names. A map of id -> temp name (or None, for deletions) is returned. :param source_entries: The entries to rename and delete :type source_entries: List of `ChangesetEntry` :param inventory: The map of id -> filename in the current tree :type inventory: Dictionary :param dir: The directory to apply changes to :type dir: str :param reverse: Apply changes in reverse :type reverse: bool :return: a mapping of id to temporary name :rtype: Dictionary """ temp_name = {} for i in range(len(source_entries)): entry = source_entries[i] if entry.is_deletion(reverse): path = os.path.join(dir, inventory[entry.id]) entry.apply(path, conflict_handler, reverse) temp_name[entry.id] = None else: to_name = os.path.join(temp_dir, str(i)) src_path = inventory.get(entry.id) if src_path is not None: src_path = os.path.join(dir, src_path) try: os.rename(src_path, to_name) temp_name[entry.id] = to_name except OSError, e: if e.errno != errno.ENOENT: raise if conflict_handler.missing_for_rename(src_path) == "skip": continue return temp_name def rename_to_new_create(changed_inventory, target_entries, inventory, changeset, dir, conflict_handler, reverse): """Rename entries with temp names to their final names, create new files. :param changed_inventory: A mapping of id to temporary name :type changed_inventory: Dictionary :param target_entries: The entries to apply changes to :type target_entries: List of `ChangesetEntry` :param changeset: The changeset to apply :type changeset: `Changeset` :param dir: The directory to apply changes to :type dir: str :param reverse: If true, apply changes in reverse :type reverse: bool """ for entry in target_entries: new_tree_path = entry.get_new_path(inventory, changeset, reverse) if new_tree_path is None: continue new_path = os.path.join(dir, new_tree_path) old_path = changed_inventory.get(entry.id) if os.path.exists(new_path): if conflict_handler.target_exists(entry, new_path, old_path) == \ "skip": continue if entry.is_creation(reverse): entry.apply(new_path, conflict_handler, reverse) changed_inventory[entry.id] = new_tree_path else: if old_path is None: continue try: os.rename(old_path, new_path) changed_inventory[entry.id] = new_tree_path except OSError, e: raise Exception ("%s is missing" % new_path) class TargetExists(Exception): def __init__(self, entry, target): msg = "The path %s already exists" % target Exception.__init__(self, msg) self.entry = entry self.target = target class RenameConflict(Exception): def __init__(self, id, this_name, base_name, other_name): msg = """Trees all have different names for a file this: %s base: %s other: %s id: %s""" % (this_name, base_name, other_name, id) Exception.__init__(self, msg) self.this_name = this_name self.base_name = base_name self_other_name = other_name class MoveConflict(Exception): def __init__(self, id, this_parent, base_parent, other_parent): msg = """The file is in different directories in every tree this: %s base: %s other: %s id: %s""" % (this_parent, base_parent, other_parent, id) Exception.__init__(self, msg) self.this_parent = this_parent self.base_parent = base_parent self_other_parent = other_parent class MergeConflict(Exception): def __init__(self, this_path): Exception.__init__(self, "Conflict applying changes to %s" % this_path) self.this_path = this_path class MergePermissionConflict(Exception): def __init__(self, this_path, base_path, other_path): this_perms = os.stat(this_path).st_mode & 0755 base_perms = os.stat(base_path).st_mode & 0755 other_perms = os.stat(other_path).st_mode & 0755 msg = """Conflicting permission for %s this: %o base: %o other: %o """ % (this_path, this_perms, base_perms, other_perms) self.this_path = this_path self.base_path = base_path self.other_path = other_path Exception.__init__(self, msg) class WrongOldContents(Exception): def __init__(self, filename): msg = "Contents mismatch deleting %s" % filename self.filename = filename Exception.__init__(self, msg) class WrongOldPermissions(Exception): def __init__(self, filename, old_perms, new_perms): msg = "Permission missmatch on %s:\n" \ "Expected 0%o, got 0%o." % (filename, old_perms, new_perms) self.filename = filename Exception.__init__(self, msg) class RemoveContentsConflict(Exception): def __init__(self, filename): msg = "Conflict deleting %s, which has different contents in BASE"\ " and THIS" % filename self.filename = filename Exception.__init__(self, msg) class DeletingNonEmptyDirectory(Exception): def __init__(self, filename): msg = "Trying to remove dir %s while it still had files" % filename self.filename = filename Exception.__init__(self, msg) class PatchTargetMissing(Exception): def __init__(self, filename): msg = "Attempt to patch %s, which does not exist" % filename Exception.__init__(self, msg) self.filename = filename class MissingPermsFile(Exception): def __init__(self, filename): msg = "Attempt to change permissions on %s, which does not exist" %\ filename Exception.__init__(self, msg) self.filename = filename class MissingForRm(Exception): def __init__(self, filename): msg = "Attempt to remove missing path %s" % filename Exception.__init__(self, msg) self.filename = filename class MissingForRename(Exception): def __init__(self, filename): msg = "Attempt to move missing path %s" % (filename) Exception.__init__(self, msg) self.filename = filename class NewContentsConflict(Exception): def __init__(self, filename): msg = "Conflicting contents for new file %s" % (filename) Exception.__init__(self, msg) class MissingForMerge(Exception): def __init__(self, filename): msg = "The file %s was modified, but does not exist in this tree"\ % (filename) Exception.__init__(self, msg) class ExceptionConflictHandler(object): def __init__(self, dir): self.dir = dir def missing_parent(self, pathname): parent = os.path.dirname(pathname) raise Exception("Parent directory missing for %s" % pathname) def dir_exists(self, pathname): raise Exception("Directory already exists for %s" % pathname) def failed_hunks(self, pathname): raise Exception("Failed to apply some hunks for %s" % pathname) def target_exists(self, entry, target, old_path): raise TargetExists(entry, target) def rename_conflict(self, id, this_name, base_name, other_name): raise RenameConflict(id, this_name, base_name, other_name) def move_conflict(self, id, inventory): this_dir = inventory.this.get_dir(id) base_dir = inventory.base.get_dir(id) other_dir = inventory.other.get_dir(id) raise MoveConflict(id, this_dir, base_dir, other_dir) def merge_conflict(self, new_file, this_path, base_path, other_path): os.unlink(new_file) raise MergeConflict(this_path) def permission_conflict(self, this_path, base_path, other_path): raise MergePermissionConflict(this_path, base_path, other_path) def wrong_old_contents(self, filename, expected_contents): raise WrongOldContents(filename) def rem_contents_conflict(self, filename, this_contents, base_contents): raise RemoveContentsConflict(filename) def wrong_old_perms(self, filename, old_perms, new_perms): raise WrongOldPermissions(filename, old_perms, new_perms) def rmdir_non_empty(self, filename): raise DeletingNonEmptyDirectory(filename) def link_name_exists(self, filename): raise TargetExists(filename) def patch_target_missing(self, filename, contents): raise PatchTargetMissing(filename) def missing_for_chmod(self, filename): raise MissingPermsFile(filename) def missing_for_rm(self, filename, change): raise MissingForRm(filename) def missing_for_rename(self, filename): raise MissingForRename(filename) def missing_for_merge(self, file_id, inventory): raise MissingForMerge(inventory.other.get_path(file_id)) def new_contents_conflict(self, filename, other_contents): raise NewContentsConflict(filename) def finalize(): pass def apply_changeset(changeset, inventory, dir, conflict_handler=None, reverse=False): """Apply a changeset to a directory. :param changeset: The changes to perform :type changeset: `Changeset` :param inventory: The mapping of id to filename for the directory :type inventory: Dictionary :param dir: The path of the directory to apply the changes to :type dir: str :param reverse: If true, apply the changes in reverse :type reverse: bool :return: The mapping of the changed entries :rtype: Dictionary """ if conflict_handler is None: conflict_handler = ExceptionConflictHandler(dir) temp_dir = os.path.join(dir, "bzr-tree-change") try: os.mkdir(temp_dir) except OSError, e: if e.errno == errno.EEXIST: try: os.rmdir(temp_dir) except OSError, e: if e.errno == errno.ENOTEMPTY: raise OldFailedTreeOp() os.mkdir(temp_dir) else: raise #apply changes that don't affect filenames for entry in changeset.entries.itervalues(): if not entry.is_creation_or_deletion(): path = os.path.join(dir, inventory[entry.id]) entry.apply(path, conflict_handler, reverse) # Apply renames in stages, to minimize conflicts: # Only files whose name or parent change are interesting, because their # target name may exist in the source tree. If a directory's name changes, # that doesn't make its children interesting. (source_entries, target_entries) = get_rename_entries(changeset, inventory, reverse) changed_inventory = rename_to_temp_delete(source_entries, inventory, dir, temp_dir, conflict_handler, reverse) rename_to_new_create(changed_inventory, target_entries, inventory, changeset, dir, conflict_handler, reverse) os.rmdir(temp_dir) return changed_inventory def apply_changeset_tree(cset, tree, reverse=False): r_inventory = {} for entry in tree.source_inventory().itervalues(): inventory[entry.id] = entry.path new_inventory = apply_changeset(cset, r_inventory, tree.root, reverse=reverse) new_entries, remove_entries = \ get_inventory_change(inventory, new_inventory, cset, reverse) tree.update_source_inventory(new_entries, remove_entries) def get_inventory_change(inventory, new_inventory, cset, reverse=False): new_entries = {} remove_entries = [] for entry in cset.entries.itervalues(): if entry.needs_rename(): new_path = entry.get_new_path(inventory, cset) if new_path is None: remove_entries.append(entry.id) else: new_entries[new_path] = entry.id return new_entries, remove_entries def print_changeset(cset): """Print all non-boring changeset entries :param cset: The changeset to print :type cset: `Changeset` """ for entry in cset.entries.itervalues(): if entry.is_boring(): continue print entry.id print entry.summarize_name(cset) class CompositionFailure(Exception): def __init__(self, old_entry, new_entry, problem): msg = "Unable to conpose entries.\n %s" % problem Exception.__init__(self, msg) class IDMismatch(CompositionFailure): def __init__(self, old_entry, new_entry): problem = "Attempt to compose entries with different ids: %s and %s" %\ (old_entry.id, new_entry.id) CompositionFailure.__init__(self, old_entry, new_entry, problem) def compose_changesets(old_cset, new_cset): """Combine two changesets into one. This works well for exact patching. Otherwise, not so well. :param old_cset: The first changeset that would be applied :type old_cset: `Changeset` :param new_cset: The second changeset that would be applied :type new_cset: `Changeset` :return: A changeset that combines the changes in both changesets :rtype: `Changeset` """ composed = Changeset() for old_entry in old_cset.entries.itervalues(): new_entry = new_cset.entries.get(old_entry.id) if new_entry is None: composed.add_entry(old_entry) else: composed_entry = compose_entries(old_entry, new_entry) if composed_entry.parent is not None or\ composed_entry.new_parent is not None: composed.add_entry(composed_entry) for new_entry in new_cset.entries.itervalues(): if not old_cset.entries.has_key(new_entry.id): composed.add_entry(new_entry) return composed def compose_entries(old_entry, new_entry): """Combine two entries into one. :param old_entry: The first entry that would be applied :type old_entry: ChangesetEntry :param old_entry: The second entry that would be applied :type old_entry: ChangesetEntry :return: A changeset entry combining both entries :rtype: `ChangesetEntry` """ if old_entry.id != new_entry.id: raise IDMismatch(old_entry, new_entry) output = ChangesetEntry(old_entry.id, old_entry.parent, old_entry.path) if (old_entry.parent != old_entry.new_parent or new_entry.parent != new_entry.new_parent): output.new_parent = new_entry.new_parent if (old_entry.path != old_entry.new_path or new_entry.path != new_entry.new_path): output.new_path = new_entry.new_path output.contents_change = compose_contents(old_entry, new_entry) output.metadata_change = compose_metadata(old_entry, new_entry) return output def compose_contents(old_entry, new_entry): """Combine the contents of two changeset entries. Entries are combined intelligently where possible, but the fallback behavior returns an ApplySequence. :param old_entry: The first entry that would be applied :type old_entry: `ChangesetEntry` :param new_entry: The second entry that would be applied :type new_entry: `ChangesetEntry` :return: A combined contents change :rtype: anything supporting the apply(reverse=False) method """ old_contents = old_entry.contents_change new_contents = new_entry.contents_change if old_entry.contents_change is None: return new_entry.contents_change elif new_entry.contents_change is None: return old_entry.contents_change elif isinstance(old_contents, ReplaceContents) and \ isinstance(new_contents, ReplaceContents): if old_contents.old_contents == new_contents.new_contents: return None else: return ReplaceContents(old_contents.old_contents, new_contents.new_contents) elif isinstance(old_contents, ApplySequence): output = ApplySequence(old_contents.changes) if isinstance(new_contents, ApplySequence): output.changes.extend(new_contents.changes) else: output.changes.append(new_contents) return output elif isinstance(new_contents, ApplySequence): output = ApplySequence((old_contents.changes,)) output.extend(new_contents.changes) return output else: return ApplySequence((old_contents, new_contents)) def compose_metadata(old_entry, new_entry): old_meta = old_entry.metadata_change new_meta = new_entry.metadata_change if old_meta is None: return new_meta elif new_meta is None: return old_meta elif isinstance(old_meta, ChangeUnixPermissions) and \ isinstance(new_meta, ChangeUnixPermissions): return ChangeUnixPermissions(old_meta.old_mode, new_meta.new_mode) else: return ApplySequence(old_meta, new_meta) def changeset_is_null(changeset): for entry in changeset.entries.itervalues(): if not entry.is_boring(): return False return True class UnsuppportedFiletype(Exception): def __init__(self, full_path, stat_result): msg = "The file \"%s\" is not a supported filetype." % full_path Exception.__init__(self, msg) self.full_path = full_path self.stat_result = stat_result def generate_changeset(tree_a, tree_b, inventory_a=None, inventory_b=None): return ChangesetGenerator(tree_a, tree_b, inventory_a, inventory_b)() class ChangesetGenerator(object): def __init__(self, tree_a, tree_b, inventory_a=None, inventory_b=None): object.__init__(self) self.tree_a = tree_a self.tree_b = tree_b if inventory_a is not None: self.inventory_a = inventory_a else: self.inventory_a = tree_a.inventory() if inventory_b is not None: self.inventory_b = inventory_b else: self.inventory_b = tree_b.inventory() self.r_inventory_a = self.reverse_inventory(self.inventory_a) self.r_inventory_b = self.reverse_inventory(self.inventory_b) def reverse_inventory(self, inventory): r_inventory = {} for entry in inventory.itervalues(): if entry.id is None: continue r_inventory[entry.id] = entry return r_inventory def __call__(self): cset = Changeset() for entry in self.inventory_a.itervalues(): if entry.id is None: continue cs_entry = self.make_entry(entry.id) if cs_entry is not None and not cs_entry.is_boring(): cset.add_entry(cs_entry) for entry in self.inventory_b.itervalues(): if entry.id is None: continue if not self.r_inventory_a.has_key(entry.id): cs_entry = self.make_entry(entry.id) if cs_entry is not None and not cs_entry.is_boring(): cset.add_entry(cs_entry) for entry in list(cset.entries.itervalues()): if entry.parent != entry.new_parent: if not cset.entries.has_key(entry.parent) and\ entry.parent != NULL_ID and entry.parent is not None: parent_entry = self.make_boring_entry(entry.parent) cset.add_entry(parent_entry) if not cset.entries.has_key(entry.new_parent) and\ entry.new_parent != NULL_ID and \ entry.new_parent is not None: parent_entry = self.make_boring_entry(entry.new_parent) cset.add_entry(parent_entry) return cset def get_entry_parent(self, entry, inventory): if entry is None: return None if entry.path == "./.": return NULL_ID dirname = os.path.dirname(entry.path) if dirname == ".": dirname = "./." parent = inventory[dirname] return parent.id def get_paths(self, entry, tree): if entry is None: return (None, None) full_path = tree.readonly_path(entry.id) if entry.path == ".": return ("", full_path) return (entry.path, full_path) def make_basic_entry(self, id, only_interesting): entry_a = self.r_inventory_a.get(id) entry_b = self.r_inventory_b.get(id) if only_interesting and not self.is_interesting(entry_a, entry_b): return (None, None, None) parent = self.get_entry_parent(entry_a, self.inventory_a) (path, full_path_a) = self.get_paths(entry_a, self.tree_a) cs_entry = ChangesetEntry(id, parent, path) new_parent = self.get_entry_parent(entry_b, self.inventory_b) (new_path, full_path_b) = self.get_paths(entry_b, self.tree_b) cs_entry.new_path = new_path cs_entry.new_parent = new_parent return (cs_entry, full_path_a, full_path_b) def is_interesting(self, entry_a, entry_b): if entry_a is not None: if entry_a.interesting: return True if entry_b is not None: if entry_b.interesting: return True return False def make_boring_entry(self, id): (cs_entry, full_path_a, full_path_b) = \ self.make_basic_entry(id, only_interesting=False) if cs_entry.is_creation_or_deletion(): return self.make_entry(id, only_interesting=False) else: return cs_entry def make_entry(self, id, only_interesting=True): (cs_entry, full_path_a, full_path_b) = \ self.make_basic_entry(id, only_interesting) if cs_entry is None: return None stat_a = self.lstat(full_path_a) stat_b = self.lstat(full_path_b) if stat_b is None: cs_entry.new_parent = None cs_entry.new_path = None cs_entry.metadata_change = self.make_mode_change(stat_a, stat_b) cs_entry.contents_change = self.make_contents_change(full_path_a, stat_a, full_path_b, stat_b) return cs_entry def make_mode_change(self, stat_a, stat_b): mode_a = None if stat_a is not None and not stat.S_ISLNK(stat_a.st_mode): mode_a = stat_a.st_mode & 0777 mode_b = None if stat_b is not None and not stat.S_ISLNK(stat_b.st_mode): mode_b = stat_b.st_mode & 0777 if mode_a == mode_b: return None return ChangeUnixPermissions(mode_a, mode_b) def make_contents_change(self, full_path_a, stat_a, full_path_b, stat_b): if stat_a is None and stat_b is None: return None if None not in (stat_a, stat_b) and stat.S_ISDIR(stat_a.st_mode) and\ stat.S_ISDIR(stat_b.st_mode): return None if None not in (stat_a, stat_b) and stat.S_ISREG(stat_a.st_mode) and\ stat.S_ISREG(stat_b.st_mode): if stat_a.st_ino == stat_b.st_ino and \ stat_a.st_dev == stat_b.st_dev: return None if file(full_path_a, "rb").read() == \ file(full_path_b, "rb").read(): return None patch_contents = patch.diff(full_path_a, file(full_path_b, "rb").read()) if patch_contents is None: return None return PatchApply(patch_contents) a_contents = self.get_contents(stat_a, full_path_a) b_contents = self.get_contents(stat_b, full_path_b) if a_contents == b_contents: return None return ReplaceContents(a_contents, b_contents) def get_contents(self, stat_result, full_path): if stat_result is None: return None elif stat.S_ISREG(stat_result.st_mode): return FileCreate(file(full_path, "rb").read()) elif stat.S_ISDIR(stat_result.st_mode): return dir_create elif stat.S_ISLNK(stat_result.st_mode): return SymlinkCreate(os.readlink(full_path)) else: raise UnsupportedFiletype(full_path, stat_result) def lstat(self, full_path): stat_result = None if full_path is not None: try: stat_result = os.lstat(full_path) except OSError, e: if e.errno != errno.ENOENT: raise return stat_result def full_path(entry, tree): return os.path.join(tree.root, entry.path) def new_delete_entry(entry, tree, inventory, delete): if entry.path == "": parent = NULL_ID else: parent = inventory[dirname(entry.path)].id cs_entry = ChangesetEntry(parent, entry.path) if delete: cs_entry.new_path = None cs_entry.new_parent = None else: cs_entry.path = None cs_entry.parent = None full_path = full_path(entry, tree) status = os.lstat(full_path) if stat.S_ISDIR(file_stat.st_mode): action = dir_create class Inventory(object): def __init__(self, inventory): self.inventory = inventory self.rinventory = None def get_rinventory(self): if self.rinventory is None: self.rinventory = invert_dict(self.inventory) return self.rinventory def get_path(self, id): return self.inventory.get(id) def get_name(self, id): path = self.get_path(id) if path is None: return None else: return os.path.basename(path) def get_dir(self, id): path = self.get_path(id) if path == "": return None if path is None: return None return os.path.dirname(path) def get_parent(self, id): if self.get_path(id) is None: return None directory = self.get_dir(id) if directory == '.': directory = './.' if directory is None: return NULL_ID return self.get_rinventory().get(directory) M 644 inline bzrlib/merge.py data 10695 from merge_core import merge_flex from changeset import generate_changeset, ExceptionConflictHandler from changeset import Inventory from bzrlib import find_branch import bzrlib.osutils from bzrlib.errors import BzrCommandError from bzrlib.diff import compare_trees from trace import mutter, warning import os.path import tempfile import shutil import errno class UnrelatedBranches(BzrCommandError): def __init__(self): msg = "Branches have no common ancestor, and no base revision"\ " specified." BzrCommandError.__init__(self, msg) class MergeConflictHandler(ExceptionConflictHandler): """Handle conflicts encountered while merging""" def __init__(self, dir, ignore_zero=False): ExceptionConflictHandler.__init__(self, dir) self.conflicts = 0 self.ignore_zero = ignore_zero def copy(self, source, dest): """Copy the text and mode of a file :param source: The path of the file to copy :param dest: The distination file to create """ s_file = file(source, "rb") d_file = file(dest, "wb") for line in s_file: d_file.write(line) os.chmod(dest, 0777 & os.stat(source).st_mode) def add_suffix(self, name, suffix, last_new_name=None): """Rename a file to append a suffix. If the new name exists, the suffix is added repeatedly until a non-existant name is found :param name: The path of the file :param suffix: The suffix to append :param last_new_name: (used for recursive calls) the last name tried """ if last_new_name is None: last_new_name = name new_name = last_new_name+suffix try: os.rename(name, new_name) return new_name except OSError, e: if e.errno != errno.EEXIST and e.errno != errno.ENOTEMPTY: raise return self.add_suffix(name, suffix, last_new_name=new_name) def conflict(self, text): warning(text) self.conflicts += 1 def merge_conflict(self, new_file, this_path, base_path, other_path): """ Handle diff3 conflicts by producing a .THIS, .BASE and .OTHER. The main file will be a version with diff3 conflicts. :param new_file: Path to the output file with diff3 markers :param this_path: Path to the file text for the THIS tree :param base_path: Path to the file text for the BASE tree :param other_path: Path to the file text for the OTHER tree """ self.add_suffix(this_path, ".THIS") self.copy(base_path, this_path+".BASE") self.copy(other_path, this_path+".OTHER") os.rename(new_file, this_path) self.conflict("Diff3 conflict encountered in %s" % this_path) def target_exists(self, entry, target, old_path): """Handle the case when the target file or dir exists""" moved_path = self.add_suffix(target, ".moved") self.conflict("Moved existing %s to %s" % (target, moved_path)) def rmdir_non_empty(self, filename): """Handle the case where the dir to be removed still has contents""" self.conflict("Directory %s not removed because it is not empty"\ % filename) return "skip" def finalize(self): if not self.ignore_zero: print "%d conflicts encountered.\n" % self.conflicts class SourceFile(object): def __init__(self, path, id, present=None, isdir=None): self.path = path self.id = id self.present = present self.isdir = isdir self.interesting = True def __repr__(self): return "SourceFile(%s, %s)" % (self.path, self.id) def get_tree(treespec, temp_root, label): location, revno = treespec branch = find_branch(location) if revno is None: base_tree = branch.working_tree() elif revno == -1: base_tree = branch.basis_tree() else: base_tree = branch.revision_tree(branch.lookup_revision(revno)) temp_path = os.path.join(temp_root, label) os.mkdir(temp_path) return branch, MergeTree(base_tree, temp_path) def abspath(tree, file_id): path = tree.inventory.id2path(file_id) if path == "": return "./." return "./" + path def file_exists(tree, file_id): return tree.has_filename(tree.id2path(file_id)) def inventory_map(tree): inventory = {} for file_id in tree.inventory: path = abspath(tree, file_id) inventory[path] = SourceFile(path, file_id) return inventory class MergeTree(object): def __init__(self, tree, tempdir): object.__init__(self) if hasattr(tree, "basedir"): self.root = tree.basedir else: self.root = None self.inventory = inventory_map(tree) self.tree = tree self.tempdir = tempdir os.mkdir(os.path.join(self.tempdir, "texts")) self.cached = {} def readonly_path(self, id): if id not in self.tree: return None if self.root is not None: return self.tree.abspath(self.tree.id2path(id)) else: if self.tree.inventory[id].kind in ("directory", "root_directory"): return self.tempdir if not self.cached.has_key(id): path = os.path.join(self.tempdir, "texts", id) outfile = file(path, "wb") outfile.write(self.tree.get_file(id).read()) assert(os.path.exists(path)) self.cached[id] = path return self.cached[id] def merge(other_revision, base_revision, check_clean=True, ignore_zero=False, this_dir=None): """Merge changes into a tree. base_revision Base for three-way merge. other_revision Other revision for three-way merge. this_dir Directory to merge changes into; '.' by default. check_clean If true, this_dir must have no uncommitted changes before the merge begins. """ tempdir = tempfile.mkdtemp(prefix="bzr-") try: if this_dir is None: this_dir = '.' this_branch = find_branch(this_dir) if check_clean: changes = compare_trees(this_branch.working_tree(), this_branch.basis_tree(), False) if changes.has_changed(): raise BzrCommandError("Working tree has uncommitted changes.") other_branch, other_tree = get_tree(other_revision, tempdir, "other") if base_revision == [None, None]: if other_revision[1] == -1: o_revno = None else: o_revno = other_revision[1] base_revno = this_branch.common_ancestor(other_branch, other_revno=o_revno)[0] if base_revno is None: raise UnrelatedBranches() base_revision = ['.', base_revno] base_branch, base_tree = get_tree(base_revision, tempdir, "base") merge_inner(this_branch, other_tree, base_tree, tempdir, ignore_zero=ignore_zero) finally: shutil.rmtree(tempdir) def generate_cset_optimized(tree_a, tree_b, inventory_a, inventory_b): """Generate a changeset, using the text_id to mark really-changed files. This permits blazing comparisons when text_ids are present. It also disables metadata comparison for files with identical texts. """ for file_id in tree_a.tree.inventory: if file_id not in tree_b.tree.inventory: continue entry_a = tree_a.tree.inventory[file_id] entry_b = tree_b.tree.inventory[file_id] if (entry_a.kind, entry_b.kind) != ("file", "file"): continue if None in (entry_a.text_id, entry_b.text_id): continue if entry_a.text_id != entry_b.text_id: continue inventory_a[abspath(tree_a.tree, file_id)].interesting = False inventory_b[abspath(tree_b.tree, file_id)].interesting = False cset = generate_changeset(tree_a, tree_b, inventory_a, inventory_b) for entry in cset.entries.itervalues(): entry.metadata_change = None return cset def merge_inner(this_branch, other_tree, base_tree, tempdir, ignore_zero=False): this_tree = get_tree((this_branch.base, None), tempdir, "this")[1] def get_inventory(tree): return tree.inventory inv_changes = merge_flex(this_tree, base_tree, other_tree, generate_cset_optimized, get_inventory, MergeConflictHandler(base_tree.root, ignore_zero=ignore_zero)) adjust_ids = [] for id, path in inv_changes.iteritems(): if path is not None: if path == '.': path = '' else: assert path.startswith('./') path = path[2:] adjust_ids.append((path, id)) this_branch.set_inventory(regen_inventory(this_branch, this_tree.root, adjust_ids)) def regen_inventory(this_branch, root, new_entries): old_entries = this_branch.read_working_inventory() new_inventory = {} by_path = {} for file_id in old_entries: entry = old_entries[file_id] path = old_entries.id2path(file_id) new_inventory[file_id] = (path, file_id, entry.parent_id, entry.kind) by_path[path] = file_id deletions = 0 insertions = 0 new_path_list = [] for path, file_id in new_entries: if path is None: del new_inventory[file_id] deletions += 1 else: new_path_list.append((path, file_id)) if file_id not in old_entries: insertions += 1 # Ensure no file is added before its parent new_path_list.sort() for path, file_id in new_path_list: if path == '': parent = None else: parent = by_path[os.path.dirname(path)] kind = bzrlib.osutils.file_kind(os.path.join(root, path)) new_inventory[file_id] = (path, file_id, parent, kind) by_path[path] = file_id # Get a list in insertion order new_inventory_list = new_inventory.values() mutter ("""Inventory regeneration: old length: %i insertions: %i deletions: %i new_length: %i"""\ % (len(old_entries), insertions, deletions, len(new_inventory_list))) assert len(new_inventory_list) == len(old_entries) + insertions - deletions new_inventory_list.sort() return new_inventory_list M 644 inline bzrlib/merge_core.py data 21580 import changeset from changeset import Inventory, apply_changeset, invert_dict import os.path class ThreewayInventory(object): def __init__(self, this_inventory, base_inventory, other_inventory): self.this = this_inventory self.base = base_inventory self.other = other_inventory def invert_invent(inventory): invert_invent = {} for key, value in inventory.iteritems(): invert_invent[value.id] = key return invert_invent def make_inv(inventory): return Inventory(invert_invent(inventory)) def merge_flex(this, base, other, changeset_function, inventory_function, conflict_handler): this_inventory = inventory_function(this) base_inventory = inventory_function(base) other_inventory = inventory_function(other) inventory = ThreewayInventory(make_inv(this_inventory), make_inv(base_inventory), make_inv(other_inventory)) cset = changeset_function(base, other, base_inventory, other_inventory) new_cset = make_merge_changeset(cset, inventory, this, base, other, conflict_handler) result = apply_changeset(new_cset, invert_invent(this_inventory), this.root, conflict_handler, False) conflict_handler.finalize() return result def make_merge_changeset(cset, inventory, this, base, other, conflict_handler=None): new_cset = changeset.Changeset() def get_this_contents(id): path = os.path.join(this.root, inventory.this.get_path(id)) if os.path.isdir(path): return changeset.dir_create else: return changeset.FileCreate(file(path, "rb").read()) for entry in cset.entries.itervalues(): if entry.is_boring(): new_cset.add_entry(entry) else: new_entry = make_merged_entry(entry, inventory, conflict_handler) new_contents = make_merged_contents(entry, this, base, other, conflict_handler) new_entry.contents_change = new_contents new_entry.metadata_change = make_merged_metadata(entry, base, other) new_cset.add_entry(new_entry) return new_cset def make_merged_entry(entry, inventory, conflict_handler): this_name = inventory.this.get_name(entry.id) this_parent = inventory.this.get_parent(entry.id) this_dir = inventory.this.get_dir(entry.id) if this_dir is None: this_dir = "" base_name = inventory.base.get_name(entry.id) base_parent = inventory.base.get_parent(entry.id) base_dir = inventory.base.get_dir(entry.id) if base_dir is None: base_dir = "" other_name = inventory.other.get_name(entry.id) other_parent = inventory.other.get_parent(entry.id) other_dir = inventory.base.get_dir(entry.id) if other_dir is None: other_dir = "" if base_name == other_name: old_name = this_name new_name = this_name else: if this_name != base_name and this_name != other_name: conflict_handler.rename_conflict(entry.id, this_name, base_name, other_name) else: old_name = this_name new_name = other_name if base_parent == other_parent: old_parent = this_parent new_parent = this_parent old_dir = this_dir new_dir = this_dir else: if this_parent != base_parent and this_parent != other_parent: conflict_handler.move_conflict(entry.id, inventory) else: old_parent = this_parent old_dir = this_dir new_parent = other_parent new_dir = other_dir if old_name is not None and old_parent is not None: old_path = os.path.join(old_dir, old_name) else: old_path = None new_entry = changeset.ChangesetEntry(entry.id, old_parent, old_name) if new_name is not None and new_parent is not None: new_entry.new_path = os.path.join(new_dir, new_name) else: new_entry.new_path = None new_entry.new_parent = new_parent return new_entry def make_merged_contents(entry, this, base, other, conflict_handler): contents = entry.contents_change if contents is None: return None this_path = this.readonly_path(entry.id) def make_diff3(): if this_path is None: return conflict_handler.missing_for_merge(entry.id, inventory) base_path = base.readonly_path(entry.id) other_path = other.readonly_path(entry.id) return changeset.Diff3Merge(base_path, other_path) if isinstance(contents, changeset.PatchApply): return make_diff3() if isinstance(contents, changeset.ReplaceContents): if contents.old_contents is None and contents.new_contents is None: return None if contents.new_contents is None: if this_path is not None and os.path.exists(this_path): return contents else: return None elif contents.old_contents is None: if this_path is None or not os.path.exists(this_path): return contents else: this_contents = file(this_path, "rb").read() if this_contents == contents.new_contents: return None else: other_path = other.readonly_path(entry.id) conflict_handler.new_contents_conflict(this_path, other_path) elif isinstance(contents.old_contents, changeset.FileCreate) and \ isinstance(contents.new_contents, changeset.FileCreate): return make_diff3() else: raise Exception("Unhandled merge scenario") def make_merged_metadata(entry, base, other): if entry.metadata_change is not None: base_path = base.readonly_path(entry.id) other_path = other.readonly_path(entry.id) return PermissionsMerge(base_path, other_path) def get_merge_entry(entry, inventory, base, other, conflict_handler): if entry.contents_change is not None: new_entry.contents_change = changeset.Diff3Merge(base_path, other_path) if entry.metadata_change is not None: new_entry.metadata_change = PermissionsMerge(base_path, other_path) return new_entry class PermissionsMerge(object): def __init__(self, base_path, other_path): self.base_path = base_path self.other_path = other_path def apply(self, filename, conflict_handler, reverse=False): if not reverse: base = self.base_path other = self.other_path else: base = self.other_path other = self.base_path base_stat = os.stat(base).st_mode other_stat = os.stat(other).st_mode this_stat = os.stat(filename).st_mode if base_stat &0777 == other_stat &0777: return elif this_stat &0777 == other_stat &0777: return elif this_stat &0777 == base_stat &0777: os.chmod(filename, other_stat) else: conflict_handler.permission_conflict(filename, base, other) import unittest import tempfile import shutil class MergeTree(object): def __init__(self, dir): self.dir = dir; os.mkdir(dir) self.inventory = {'0': ""} def child_path(self, parent, name): return os.path.join(self.inventory[parent], name) def add_file(self, id, parent, name, contents, mode): path = self.child_path(parent, name) full_path = self.abs_path(path) assert not os.path.exists(full_path) file(full_path, "wb").write(contents) os.chmod(self.abs_path(path), mode) self.inventory[id] = path def add_dir(self, id, parent, name, mode): path = self.child_path(parent, name) full_path = self.abs_path(path) assert not os.path.exists(full_path) os.mkdir(self.abs_path(path)) os.chmod(self.abs_path(path), mode) self.inventory[id] = path def abs_path(self, path): return os.path.join(self.dir, path) def full_path(self, id): return self.abs_path(self.inventory[id]) def readonly_path(self, id): return self.full_path(id) def change_path(self, id, path): new = os.path.join(self.dir, self.inventory[id]) os.rename(self.abs_path(self.inventory[id]), self.abs_path(path)) self.inventory[id] = path class MergeBuilder(object): def __init__(self): self.dir = tempfile.mkdtemp(prefix="BaZing") self.base = MergeTree(os.path.join(self.dir, "base")) self.this = MergeTree(os.path.join(self.dir, "this")) self.other = MergeTree(os.path.join(self.dir, "other")) self.cset = changeset.Changeset() self.cset.add_entry(changeset.ChangesetEntry("0", changeset.NULL_ID, "./.")) def get_cset_path(self, parent, name): if name is None: assert (parent is None) return None return os.path.join(self.cset.entries[parent].path, name) def add_file(self, id, parent, name, contents, mode): self.base.add_file(id, parent, name, contents, mode) self.this.add_file(id, parent, name, contents, mode) self.other.add_file(id, parent, name, contents, mode) path = self.get_cset_path(parent, name) self.cset.add_entry(changeset.ChangesetEntry(id, parent, path)) def add_dir(self, id, parent, name, mode): path = self.get_cset_path(parent, name) self.base.add_dir(id, parent, name, mode) self.cset.add_entry(changeset.ChangesetEntry(id, parent, path)) self.this.add_dir(id, parent, name, mode) self.other.add_dir(id, parent, name, mode) def change_name(self, id, base=None, this=None, other=None): if base is not None: self.change_name_tree(id, self.base, base) self.cset.entries[id].name = base if this is not None: self.change_name_tree(id, self.this, this) if other is not None: self.change_name_tree(id, self.other, other) self.cset.entries[id].new_name = other def change_parent(self, id, base=None, this=None, other=None): if base is not None: self.change_parent_tree(id, self.base, base) self.cset.entries[id].parent = base self.cset.entries[id].dir = self.cset.entries[base].path if this is not None: self.change_parent_tree(id, self.this, this) if other is not None: self.change_parent_tree(id, self.other, other) self.cset.entries[id].new_parent = other self.cset.entries[id].new_dir = \ self.cset.entries[other].new_path def change_contents(self, id, base=None, this=None, other=None): if base is not None: self.change_contents_tree(id, self.base, base) if this is not None: self.change_contents_tree(id, self.this, this) if other is not None: self.change_contents_tree(id, self.other, other) if base is not None or other is not None: old_contents = file(self.base.full_path(id)).read() new_contents = file(self.other.full_path(id)).read() contents = changeset.ReplaceFileContents(old_contents, new_contents) self.cset.entries[id].contents_change = contents def change_perms(self, id, base=None, this=None, other=None): if base is not None: self.change_perms_tree(id, self.base, base) if this is not None: self.change_perms_tree(id, self.this, this) if other is not None: self.change_perms_tree(id, self.other, other) if base is not None or other is not None: old_perms = os.stat(self.base.full_path(id)).st_mode &077 new_perms = os.stat(self.other.full_path(id)).st_mode &077 contents = changeset.ChangeUnixPermissions(old_perms, new_perms) self.cset.entries[id].metadata_change = contents def change_name_tree(self, id, tree, name): new_path = tree.child_path(self.cset.entries[id].parent, name) tree.change_path(id, new_path) def change_parent_tree(self, id, tree, parent): new_path = tree.child_path(parent, self.cset.entries[id].name) tree.change_path(id, new_path) def change_contents_tree(self, id, tree, contents): path = tree.full_path(id) mode = os.stat(path).st_mode file(path, "w").write(contents) os.chmod(path, mode) def change_perms_tree(self, id, tree, mode): os.chmod(tree.full_path(id), mode) def merge_changeset(self): all_inventory = ThreewayInventory(Inventory(self.this.inventory), Inventory(self.base.inventory), Inventory(self.other.inventory)) conflict_handler = changeset.ExceptionConflictHandler(self.this.dir) return make_merge_changeset(self.cset, all_inventory, self.this, self.base, self.other, conflict_handler) def apply_inv_change(self, inventory_change, orig_inventory): orig_inventory_by_path = {} for file_id, path in orig_inventory.iteritems(): orig_inventory_by_path[path] = file_id def parent_id(file_id): try: parent_dir = os.path.dirname(orig_inventory[file_id]) except: print file_id raise if parent_dir == "": return None return orig_inventory_by_path[parent_dir] def new_path(file_id): if inventory_change.has_key(file_id): return inventory_change[file_id] else: parent = parent_id(file_id) if parent is None: return orig_inventory[file_id] dirname = new_path(parent) return os.path.join(dirname, orig_inventory[file_id]) new_inventory = {} for file_id in orig_inventory.iterkeys(): path = new_path(file_id) if path is None: continue new_inventory[file_id] = path for file_id, path in inventory_change.iteritems(): if orig_inventory.has_key(file_id): continue new_inventory[file_id] = path return new_inventory def apply_changeset(self, cset, conflict_handler=None, reverse=False): inventory_change = changeset.apply_changeset(cset, self.this.inventory, self.this.dir, conflict_handler, reverse) self.this.inventory = self.apply_inv_change(inventory_change, self.this.inventory) def cleanup(self): shutil.rmtree(self.dir) class MergeTest(unittest.TestCase): def test_change_name(self): """Test renames""" builder = MergeBuilder() builder.add_file("1", "0", "name1", "hello1", 0755) builder.change_name("1", other="name2") builder.add_file("2", "0", "name3", "hello2", 0755) builder.change_name("2", base="name4") builder.add_file("3", "0", "name5", "hello3", 0755) builder.change_name("3", this="name6") cset = builder.merge_changeset() assert(cset.entries["2"].is_boring()) assert(cset.entries["1"].name == "name1") assert(cset.entries["1"].new_name == "name2") assert(cset.entries["3"].is_boring()) for tree in (builder.this, builder.other, builder.base): assert(tree.dir != builder.dir and tree.dir.startswith(builder.dir)) for path in tree.inventory.itervalues(): fullpath = tree.abs_path(path) assert(fullpath.startswith(tree.dir)) assert(not path.startswith(tree.dir)) assert os.path.exists(fullpath) builder.apply_changeset(cset) builder.cleanup() builder = MergeBuilder() builder.add_file("1", "0", "name1", "hello1", 0644) builder.change_name("1", other="name2", this="name3") self.assertRaises(changeset.RenameConflict, builder.merge_changeset) builder.cleanup() def test_file_moves(self): """Test moves""" builder = MergeBuilder() builder.add_dir("1", "0", "dir1", 0755) builder.add_dir("2", "0", "dir2", 0755) builder.add_file("3", "1", "file1", "hello1", 0644) builder.add_file("4", "1", "file2", "hello2", 0644) builder.add_file("5", "1", "file3", "hello3", 0644) builder.change_parent("3", other="2") assert(Inventory(builder.other.inventory).get_parent("3") == "2") builder.change_parent("4", this="2") assert(Inventory(builder.this.inventory).get_parent("4") == "2") builder.change_parent("5", base="2") assert(Inventory(builder.base.inventory).get_parent("5") == "2") cset = builder.merge_changeset() for id in ("1", "2", "4", "5"): assert(cset.entries[id].is_boring()) assert(cset.entries["3"].parent == "1") assert(cset.entries["3"].new_parent == "2") builder.apply_changeset(cset) builder.cleanup() builder = MergeBuilder() builder.add_dir("1", "0", "dir1", 0755) builder.add_dir("2", "0", "dir2", 0755) builder.add_dir("3", "0", "dir3", 0755) builder.add_file("4", "1", "file1", "hello1", 0644) builder.change_parent("4", other="2", this="3") self.assertRaises(changeset.MoveConflict, builder.merge_changeset) builder.cleanup() def test_contents_merge(self): """Test diff3 merging""" builder = MergeBuilder() builder.add_file("1", "0", "name1", "text1", 0755) builder.change_contents("1", other="text4") builder.add_file("2", "0", "name3", "text2", 0655) builder.change_contents("2", base="text5") builder.add_file("3", "0", "name5", "text3", 0744) builder.change_contents("3", this="text6") cset = builder.merge_changeset() assert(cset.entries["1"].contents_change is not None) assert(isinstance(cset.entries["1"].contents_change, changeset.Diff3Merge)) assert(isinstance(cset.entries["2"].contents_change, changeset.Diff3Merge)) assert(cset.entries["3"].is_boring()) builder.apply_changeset(cset) assert(file(builder.this.full_path("1"), "rb").read() == "text4" ) assert(file(builder.this.full_path("2"), "rb").read() == "text2" ) assert(os.stat(builder.this.full_path("1")).st_mode &0777 == 0755) assert(os.stat(builder.this.full_path("2")).st_mode &0777 == 0655) assert(os.stat(builder.this.full_path("3")).st_mode &0777 == 0744) builder.cleanup() builder = MergeBuilder() builder.add_file("1", "0", "name1", "text1", 0755) builder.change_contents("1", other="text4", this="text3") cset = builder.merge_changeset() self.assertRaises(changeset.MergeConflict, builder.apply_changeset, cset) builder.cleanup() def test_perms_merge(self): builder = MergeBuilder() builder.add_file("1", "0", "name1", "text1", 0755) builder.change_perms("1", other=0655) builder.add_file("2", "0", "name2", "text2", 0755) builder.change_perms("2", base=0655) builder.add_file("3", "0", "name3", "text3", 0755) builder.change_perms("3", this=0655) cset = builder.merge_changeset() assert(cset.entries["1"].metadata_change is not None) assert(isinstance(cset.entries["1"].metadata_change, PermissionsMerge)) assert(isinstance(cset.entries["2"].metadata_change, PermissionsMerge)) assert(cset.entries["3"].is_boring()) builder.apply_changeset(cset) assert(os.stat(builder.this.full_path("1")).st_mode &0777 == 0655) assert(os.stat(builder.this.full_path("2")).st_mode &0777 == 0755) assert(os.stat(builder.this.full_path("3")).st_mode &0777 == 0655) builder.cleanup(); builder = MergeBuilder() builder.add_file("1", "0", "name1", "text1", 0755) builder.change_perms("1", other=0655, base=0555) cset = builder.merge_changeset() self.assertRaises(changeset.MergePermissionConflict, builder.apply_changeset, cset) builder.cleanup() def test(): changeset_suite = unittest.makeSuite(MergeTest, 'test_') runner = unittest.TextTestRunner() runner.run(changeset_suite) if __name__ == "__main__": test() commit refs/heads/master mark :851 committer Martin Pool 1120723510 +1000 data 40 - run merge_core tests from bzr selftest from :850 M 644 inline bzrlib/selftest/__init__.py data 2312 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from testsweet import TestBase, run_suite, InTempDir def selftest(): from unittest import TestLoader, TestSuite import bzrlib, bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, bzrlib.commands import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning import bzrlib.selftest.testmerge3 import bzrlib.selftest.testhashcache import bzrlib.merge_core from doctest import DocTestSuite import os import shutil import time import sys import unittest TestBase.BZRPATH = os.path.join(os.path.realpath(os.path.dirname(bzrlib.__path__[0])), 'bzr') print '%-30s %s' % ('bzr binary', TestBase.BZRPATH) print suite = TestSuite() # should also test bzrlib.merge_core, but they seem to be out of date with # the code. # python2.3's TestLoader() doesn't seem to work well; don't know why for m in (bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, bzrlib.commands, bzrlib.merge3): suite.addTest(DocTestSuite(m)) for cl in (bzrlib.selftest.whitebox.TEST_CLASSES + bzrlib.selftest.versioning.TEST_CLASSES + bzrlib.selftest.testmerge3.TEST_CLASSES + bzrlib.selftest.testhashcache.TEST_CLASSES + bzrlib.selftest.blackbox.TEST_CLASSES): suite.addTest(cl()) suite.addTest(unittest.makeSuite(bzrlib.merge_core.MergeTest, 'test_')) return run_suite(suite, 'testbzr') commit refs/heads/tmp mark :853 committer Martin Pool 1119838685 +1000 data 33 Check in old existing knit code. M 644 inline knit.py data 2686 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # GNU GPL v2 # Author: Martin Pool """knit - a weave-like structure A Knit manages versions of line-based text files, keeping track of the originating version for each line. Versions are referenced by nonnegative integers. In a full system this will be just a local shorthand for some universally-unique version identifier. Texts are represented as a seqence of (version, text, live) tuples. An empty sequence represents an empty text. The version is the version in which the line was introduced. The *live* flag is false if the line is no longer present and being tracked only for the purposes of merging. """ text1 = [(0, "hello world", True)] knit2 = [(0, "hello world", True), (1, "hello boys", True) ] def show_annotated(knit): """Show a knit in 'blame' style""" for vers, text, live in knit: if not live: continue print '%6d | %s' % (vers, text) def knit2text(knit): """Return a sequence of lines containing just the live text from a knit.""" return [text for (vers, text, live) in knit if live] def update_knit(knit, new_vers, new_lines): """Return a new knit whose text matches new_lines. First of all the knit is diffed against the new lines, considering only the text of the lines from the knit. This identifies lines unchanged from the knit, plus insertions and deletions. The deletions are marked as deleted. The insertions are added with their new values. """ if not isinstance(new_vers, int): raise TypeError('new version-id must be an int: %r' % new_vers) from difflib import SequenceMatcher knit_lines = knit2text(knit) m = SequenceMatcher(None, knit_lines, new_lines) for block in m.get_matching_blocks(): print "a[%d] and b[%d] match for %d elements" % block new_knit = [] for tag, i1, i2, j1, j2 in m.get_opcodes(): print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" % (tag, i1, i2, knit_lines[i1:i2], j1, j2, new_lines[j1:j2])) if tag == 'equal': new_knit.extend(knit[i1:i2]) elif tag == 'delete': for i in range(i1, i2): kl = knit[i] new_knit.append((kl[0], kl[1], False)) return new_knit print '***** annotated:' show_annotated(knit2) print '***** plain text:' print '\n'.join(knit2text(knit2)) text3 = """hello world an inserted line hello boys""".split('\n') print repr(knit2text(knit2)) print repr(text3) knit3 = update_knit(knit2, 3, text3) print '***** result of update:' show_annotated(knit3) M 644 inline woolyweave.py data 47 weave1 = [ ['i', 1, ['hello world']] ] commit refs/heads/tmp mark :854 committer Martin Pool 1119839885 +1000 data 153 Import testsweet module adapted from bzr. Start new test-driven development pattern, with a very trivial test that stores and retrieves a single text. from :853 M 644 inline .bzrignore data 18 test.log test.tmp M 644 inline testknit.py data 1475 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test case for knit/weave algorithm""" from testsweet import TestBase from knit import Knit # texts for use in testing TEXTS = [ ["This is my text"], ] class Easy(TestBase): def runTest(self): k = Knit() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Knit() k.add(TEXTS[0]) self.assertEqual(k.get(), TEXTS[0]) def testknit(): import testsweet from unittest import TestSuite, TestLoader import testknit tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testknit)) return testsweet.run_suite(suite) if __name__ == '__main__': import sys sys.exit(testknit()) M 644 inline testsweet.py data 8878 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") raise class CommandFailed(Exception): pass class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr, so that we can run it # through a specified Python intepreter OVERRIDE_PYTHON = None # to run with alternative python 'python' BZRPATH = 'bzr' _log_buf = "" def formcmd(self, cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = self.BZRPATH if self.OVERRIDE_PYTHON: cmd.insert(0, self.OVERRIDE_PYTHON) self.log('$ %r' % cmd) return cmd def runcmd(self, cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = self.formcmd(cmd) self.log('$ ' + ' '.join(cmd)) actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(self, cmd, retcode=0): """Run a command and return its output""" cmd = self.formcmd(cmd) child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG) outd, errd = child.communicate() self.log(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def build_tree(self, shape): """Build a test tree according to a pattern. shape is a sequence of file specifications. If the final character is '/', a directory is created. This doesn't add anything to a branch. """ # XXX: It's OK to just create them using forward slashes on windows? import os for name in shape: assert isinstance(name, basestring) if name[-1] == '/': os.mkdir(name[:-1]) else: f = file(name, 'wt') print >>f, "contents of", name f.close() def log(self, msg): """Log a message to a progress file""" self._log_buf = self._log_buf + str(msg) + '\n' print >>self.TEST_LOG, msg def check_inventory_shape(self, inv, shape): """ Compare an inventory to a list of expected names. Fail if they are not precisely equal. """ extras = [] shape = list(shape) # copy for path, ie in inv.entries(): name = path.replace('\\', '/') if ie.kind == 'dir': name = name + '/' if name in shape: shape.remove(name) else: extras.append(name) if shape: self.fail("expected paths not found in inventory: %r" % shape) if extras: self.fail("unexpected paths found in inventory: %r" % extras) def check_file_contents(self, filename, expect): self.log("check contents of file %s" % filename) contents = file(filename, 'r').read() if contents != expect: self.log("expected: %r" % expected) self.log("actually: %r" % contents) self.fail("contents of %s not as expected") class InTempDir(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.test_dir = os.path.join(self.TEST_ROOT, self.__class__.__name__) os.mkdir(self.test_dir) os.chdir(self.test_dir) def tearDown(self): import os os.chdir(self.TEST_ROOT) class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ def __init__(self, out): self.out = out TestResult.__init__(self) def startTest(self, test): # TODO: Maybe show test.shortDescription somewhere? print >>self.out, '%-60.60s' % test.id(), self.out.flush() TestResult.startTest(self, test) def stopTest(self, test): # print TestResult.stopTest(self, test) def addError(self, test, err): print >>self.out, 'ERROR' TestResult.addError(self, test, err) _show_test_failure('error', test, err, self.out) def addFailure(self, test, err): print >>self.out, 'FAILURE' TestResult.addFailure(self, test, err) _show_test_failure('failure', test, err, self.out) def addSuccess(self, test): print >>self.out, 'OK' TestResult.addSuccess(self, test) def selftest(): from unittest import TestLoader, TestSuite import bzrlib import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning from doctest import DocTestSuite import os import shutil import time import sys suite = TestSuite() tl = TestLoader() for m in bzrlib.selftest.whitebox, \ bzrlib.selftest.versioning: suite.addTest(tl.loadTestsFromModule(m)) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands: suite.addTest(DocTestSuite(m)) suite.addTest(bzrlib.selftest.blackbox.suite()) return run_suite(suite) def run_suite(suite): import os import shutil import time import sys _setup_test_log() _setup_test_dir() print # save stdout & stderr so there's no leakage from code-under-test real_stdout = sys.stdout real_stderr = sys.stderr sys.stdout = sys.stderr = TestBase.TEST_LOG try: result = _MyResult(real_stdout) suite.run(result) finally: sys.stdout = real_stdout sys.stderr = real_stderr _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os log_filename = os.path.abspath('test.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "bzr tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_ROOT = os.path.abspath("test.tmp") print '%-30s %s' % ('running tests in', TestBase.TEST_ROOT) if os.path.exists(TestBase.TEST_ROOT): shutil.rmtree(TestBase.TEST_ROOT) os.mkdir(TestBase.TEST_ROOT) os.chdir(TestBase.TEST_ROOT) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_ROOT, '.bzr')) def _show_results(result): print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, exc_info, out): from traceback import print_exception print >>out, '-' * 60 print >>out, case desc = case.shortDescription() if desc: print >>out, ' (%s)' % desc print_exception(exc_info[0], exc_info[1], exc_info[2], None, out) if isinstance(case, TestBase): print >>out print >>out, 'log from this test:' print >>out, case._log_buf print >>out, '-' * 60 M 644 inline knit.py data 3174 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # GNU GPL v2 # Author: Martin Pool """knit - a weave-like structure""" class Knit(object): """knit - versioned text file storage. A Knit manages versions of line-based text files, keeping track of the originating version for each line. Versions are referenced by nonnegative integers. In a full system this will be just a local shorthand for some universally-unique version identifier. Texts are represented as a seqence of (version, text, live) tuples. An empty sequence represents an empty text. The version is the version in which the line was introduced. The *live* flag is false if the line is no longer present and being tracked only for the purposes of merging. _l Stores the actual weave. """ def add(self, text): """Add a single text on top of the weave.""" if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) self._l = text def get(self): return self._l[:] text1 = [(0, "hello world", True)] knit2 = [(0, "hello world", True), (1, "hello boys", True) ] def show_annotated(knit): """Show a knit in 'blame' style""" for vers, text, live in knit: if not live: continue print '%6d | %s' % (vers, text) def knit2text(knit): """Return a sequence of lines containing just the live text from a knit.""" return [text for (vers, text, live) in knit if live] def update_knit(knit, new_vers, new_lines): """Return a new knit whose text matches new_lines. First of all the knit is diffed against the new lines, considering only the text of the lines from the knit. This identifies lines unchanged from the knit, plus insertions and deletions. The deletions are marked as deleted. The insertions are added with their new values. """ if not isinstance(new_vers, int): raise TypeError('new version-id must be an int: %r' % new_vers) from difflib import SequenceMatcher knit_lines = knit2text(knit) m = SequenceMatcher(None, knit_lines, new_lines) for block in m.get_matching_blocks(): print "a[%d] and b[%d] match for %d elements" % block new_knit = [] for tag, i1, i2, j1, j2 in m.get_opcodes(): print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" % (tag, i1, i2, knit_lines[i1:i2], j1, j2, new_lines[j1:j2])) if tag == 'equal': new_knit.extend(knit[i1:i2]) elif tag == 'delete': for i in range(i1, i2): kl = knit[i] new_knit.append((kl[0], kl[1], False)) return new_knit def main(): print '***** annotated:' show_annotated(knit2) print '***** plain text:' print '\n'.join(knit2text(knit2)) text3 = """hello world an inserted line hello boys""".split('\n') print repr(knit2text(knit2)) print repr(text3) knit3 = update_knit(knit2, 3, text3) print '***** result of update:' show_annotated(knit3) commit refs/heads/tmp mark :855 committer Martin Pool 1119840050 +1000 data 35 Change storage of texts for testing from :854 M 644 inline testknit.py data 1510 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test case for knit/weave algorithm""" from testsweet import TestBase from knit import Knit # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Knit() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Knit() k.add(TEXT_0) self.assertEqual(k.get(), TEXT_0) def testknit(): import testsweet from unittest import TestSuite, TestLoader import testknit tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testknit)) return testsweet.run_suite(suite) if __name__ == '__main__': import sys sys.exit(testknit()) commit refs/heads/tmp mark :856 committer Martin Pool 1119840462 +1000 data 102 Start indexing knits by both integer and version string. Use index when inserting and checking text. from :855 M 644 inline knit.py data 3322 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # GNU GPL v2 # Author: Martin Pool """knit - a weave-like structure""" class Knit(object): """knit - versioned text file storage. A Knit manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this knit and the version-id is used to reference it in the larger world. _l List of lines. _v List of versions, indexed by index number. Each one is an empty tuple. """ def __init__(self): self._l = [] self._v = [] def add(self, text): """Add a single text on top of the weave. Returns the index number of the newly added version.""" if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) idx = len(self._v) self._l = text self._v.append(()) return idx def get(self, index): self._v[index] # check index is valid return self._l[:] text1 = [(0, "hello world", True)] knit2 = [(0, "hello world", True), (1, "hello boys", True) ] def show_annotated(knit): """Show a knit in 'blame' style""" for vers, text, live in knit: if not live: continue print '%6d | %s' % (vers, text) def knit2text(knit): """Return a sequence of lines containing just the live text from a knit.""" return [text for (vers, text, live) in knit if live] def update_knit(knit, new_vers, new_lines): """Return a new knit whose text matches new_lines. First of all the knit is diffed against the new lines, considering only the text of the lines from the knit. This identifies lines unchanged from the knit, plus insertions and deletions. The deletions are marked as deleted. The insertions are added with their new values. """ if not isinstance(new_vers, int): raise TypeError('new version-id must be an int: %r' % new_vers) from difflib import SequenceMatcher knit_lines = knit2text(knit) m = SequenceMatcher(None, knit_lines, new_lines) for block in m.get_matching_blocks(): print "a[%d] and b[%d] match for %d elements" % block new_knit = [] for tag, i1, i2, j1, j2 in m.get_opcodes(): print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" % (tag, i1, i2, knit_lines[i1:i2], j1, j2, new_lines[j1:j2])) if tag == 'equal': new_knit.extend(knit[i1:i2]) elif tag == 'delete': for i in range(i1, i2): kl = knit[i] new_knit.append((kl[0], kl[1], False)) return new_knit def main(): print '***** annotated:' show_annotated(knit2) print '***** plain text:' print '\n'.join(knit2text(knit2)) text3 = """hello world an inserted line hello boys""".split('\n') print repr(knit2text(knit2)) print repr(text3) knit3 = update_knit(knit2, 3, text3) print '***** result of update:' show_annotated(knit3) M 644 inline testknit.py data 1552 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test case for knit/weave algorithm""" from testsweet import TestBase from knit import Knit # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Knit() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Knit() idx = k.add(TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) def testknit(): import testsweet from unittest import TestSuite, TestLoader import testknit tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testknit)) return testsweet.run_suite(suite) if __name__ == '__main__': import sys sys.exit(testknit()) commit refs/heads/tmp mark :857 committer Martin Pool 1119841380 +1000 data 163 Add test for storing two text versions. Store texts as (index, line) pairs, where versions include only a single index. Filter out active lines from get method. from :856 M 644 inline knit.py data 3810 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # GNU GPL v2 # Author: Martin Pool """knit - a weave-like structure""" class Knit(object): """knit - versioned text file storage. A Knit manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this knit and the version-id is used to reference it in the larger world. _l List of edit instructions. Each line is stored as a tuple of (index-id, text). The line is present in the version equal to index-id. _v List of versions, indexed by index number. Each one is an empty tuple because the version_id isn't stored yet. """ def __init__(self): self._l = [] self._v = [] def add(self, text): """Add a single text on top of the weave. Returns the index number of the newly added version.""" if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) idx = len(self._v) # all of the previous texts are turned off; just append lines at the bottom for line in text: self._l.append((idx, line)) self._v.append(()) return idx def getiter(self, index): """Yield lines for the specified version.""" self._v[index] # check index is valid for idx, line in self._l: if idx == index: yield line def get(self, index): return list(self.getiter(index)) text1 = [(0, "hello world", True)] knit2 = [(0, "hello world", True), (1, "hello boys", True) ] def show_annotated(knit): """Show a knit in 'blame' style""" for vers, text, live in knit: if not live: continue print '%6d | %s' % (vers, text) def knit2text(knit): """Return a sequence of lines containing just the live text from a knit.""" return [text for (vers, text, live) in knit if live] def update_knit(knit, new_vers, new_lines): """Return a new knit whose text matches new_lines. First of all the knit is diffed against the new lines, considering only the text of the lines from the knit. This identifies lines unchanged from the knit, plus insertions and deletions. The deletions are marked as deleted. The insertions are added with their new values. """ if not isinstance(new_vers, int): raise TypeError('new version-id must be an int: %r' % new_vers) from difflib import SequenceMatcher knit_lines = knit2text(knit) m = SequenceMatcher(None, knit_lines, new_lines) for block in m.get_matching_blocks(): print "a[%d] and b[%d] match for %d elements" % block new_knit = [] for tag, i1, i2, j1, j2 in m.get_opcodes(): print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" % (tag, i1, i2, knit_lines[i1:i2], j1, j2, new_lines[j1:j2])) if tag == 'equal': new_knit.extend(knit[i1:i2]) elif tag == 'delete': for i in range(i1, i2): kl = knit[i] new_knit.append((kl[0], kl[1], False)) return new_knit def main(): print '***** annotated:' show_annotated(knit2) print '***** plain text:' print '\n'.join(knit2text(knit2)) text3 = """hello world an inserted line hello boys""".split('\n') print repr(knit2text(knit2)) print repr(text3) knit3 = update_knit(knit2, 3, text3) print '***** result of update:' show_annotated(knit3) M 644 inline testknit.py data 1833 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test case for knit/weave algorithm""" from testsweet import TestBase from knit import Knit # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Knit() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Knit() idx = k.add(TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class StoreTwo(TestBase): def runTest(self): k = Knit() idx = k.add(TEXT_0) self.assertEqual(idx, 0) idx = k.add(TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) def testknit(): import testsweet from unittest import TestSuite, TestLoader import testknit tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testknit)) return testsweet.run_suite(suite) if __name__ == '__main__': import sys sys.exit(testknit()) commit refs/heads/tmp mark :858 committer Martin Pool 1119841424 +1000 data 18 - remove dead code from :857 M 644 inline knit.py data 3693 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # GNU GPL v2 # Author: Martin Pool """knit - a weave-like structure""" class Knit(object): """knit - versioned text file storage. A Knit manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this knit and the version-id is used to reference it in the larger world. _l List of edit instructions. Each line is stored as a tuple of (index-id, text). The line is present in the version equal to index-id. _v List of versions, indexed by index number. Each one is an empty tuple because the version_id isn't stored yet. """ def __init__(self): self._l = [] self._v = [] def add(self, text): """Add a single text on top of the weave. Returns the index number of the newly added version.""" if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) idx = len(self._v) # all of the previous texts are turned off; just append lines at the bottom for line in text: self._l.append((idx, line)) self._v.append(()) return idx def getiter(self, index): """Yield lines for the specified version.""" self._v[index] # check index is valid for idx, line in self._l: if idx == index: yield line def get(self, index): return list(self.getiter(index)) def show_annotated(knit): """Show a knit in 'blame' style""" for vers, text, live in knit: if not live: continue print '%6d | %s' % (vers, text) def knit2text(knit): """Return a sequence of lines containing just the live text from a knit.""" return [text for (vers, text, live) in knit if live] def update_knit(knit, new_vers, new_lines): """Return a new knit whose text matches new_lines. First of all the knit is diffed against the new lines, considering only the text of the lines from the knit. This identifies lines unchanged from the knit, plus insertions and deletions. The deletions are marked as deleted. The insertions are added with their new values. """ if not isinstance(new_vers, int): raise TypeError('new version-id must be an int: %r' % new_vers) from difflib import SequenceMatcher knit_lines = knit2text(knit) m = SequenceMatcher(None, knit_lines, new_lines) for block in m.get_matching_blocks(): print "a[%d] and b[%d] match for %d elements" % block new_knit = [] for tag, i1, i2, j1, j2 in m.get_opcodes(): print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" % (tag, i1, i2, knit_lines[i1:i2], j1, j2, new_lines[j1:j2])) if tag == 'equal': new_knit.extend(knit[i1:i2]) elif tag == 'delete': for i in range(i1, i2): kl = knit[i] new_knit.append((kl[0], kl[1], False)) return new_knit def main(): print '***** annotated:' show_annotated(knit2) print '***** plain text:' print '\n'.join(knit2text(knit2)) text3 = """hello world an inserted line hello boys""".split('\n') print repr(knit2text(knit2)) print repr(text3) knit3 = update_knit(knit2, 3, text3) print '***** result of update:' show_annotated(knit3) commit refs/heads/tmp mark :859 committer Martin Pool 1119841631 +1000 data 25 Add trivial annotate text from :858 M 644 inline knit.py data 4061 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # GNU GPL v2 # Author: Martin Pool """knit - a weave-like structure""" class Knit(object): """knit - versioned text file storage. A Knit manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this knit and the version-id is used to reference it in the larger world. _l List of edit instructions. Each line is stored as a tuple of (index-id, text). The line is present in the version equal to index-id. _v List of versions, indexed by index number. Each one is an empty tuple because the version_id isn't stored yet. """ def __init__(self): self._l = [] self._v = [] def add(self, text): """Add a single text on top of the weave. Returns the index number of the newly added version.""" if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) idx = len(self._v) # all of the previous texts are turned off; just append lines at the bottom for line in text: self._l.append((idx, line)) self._v.append(()) return idx def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" for origin, line in self._l: if origin == index: yield origin, line def getiter(self, index): """Yield lines for the specified version.""" self._v[index] # check index is valid for idx, line in self._l: if idx == index: yield line def get(self, index): return list(self.getiter(index)) def show_annotated(knit): """Show a knit in 'blame' style""" for vers, text, live in knit: if not live: continue print '%6d | %s' % (vers, text) def knit2text(knit): """Return a sequence of lines containing just the live text from a knit.""" return [text for (vers, text, live) in knit if live] def update_knit(knit, new_vers, new_lines): """Return a new knit whose text matches new_lines. First of all the knit is diffed against the new lines, considering only the text of the lines from the knit. This identifies lines unchanged from the knit, plus insertions and deletions. The deletions are marked as deleted. The insertions are added with their new values. """ if not isinstance(new_vers, int): raise TypeError('new version-id must be an int: %r' % new_vers) from difflib import SequenceMatcher knit_lines = knit2text(knit) m = SequenceMatcher(None, knit_lines, new_lines) for block in m.get_matching_blocks(): print "a[%d] and b[%d] match for %d elements" % block new_knit = [] for tag, i1, i2, j1, j2 in m.get_opcodes(): print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" % (tag, i1, i2, knit_lines[i1:i2], j1, j2, new_lines[j1:j2])) if tag == 'equal': new_knit.extend(knit[i1:i2]) elif tag == 'delete': for i in range(i1, i2): kl = knit[i] new_knit.append((kl[0], kl[1], False)) return new_knit def main(): print '***** annotated:' show_annotated(knit2) print '***** plain text:' print '\n'.join(knit2text(knit2)) text3 = """hello world an inserted line hello boys""".split('\n') print repr(knit2text(knit2)) print repr(text3) knit3 = update_knit(knit2, 3, text3) print '***** result of update:' show_annotated(knit3) M 644 inline testknit.py data 2012 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test case for knit/weave algorithm""" from testsweet import TestBase from knit import Knit # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Knit() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Knit() idx = k.add(TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Knit() k.add(TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Knit() idx = k.add(TEXT_0) self.assertEqual(idx, 0) idx = k.add(TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) def testknit(): import testsweet from unittest import TestSuite, TestLoader import testknit tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testknit)) return testsweet.run_suite(suite) if __name__ == '__main__': import sys sys.exit(testknit()) commit refs/heads/tmp mark :860 committer Martin Pool 1119841673 +1000 data 23 Unify get/annotate code from :859 M 644 inline knit.py data 4049 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # GNU GPL v2 # Author: Martin Pool """knit - a weave-like structure""" class Knit(object): """knit - versioned text file storage. A Knit manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this knit and the version-id is used to reference it in the larger world. _l List of edit instructions. Each line is stored as a tuple of (index-id, text). The line is present in the version equal to index-id. _v List of versions, indexed by index number. Each one is an empty tuple because the version_id isn't stored yet. """ def __init__(self): self._l = [] self._v = [] def add(self, text): """Add a single text on top of the weave. Returns the index number of the newly added version.""" if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) idx = len(self._v) # all of the previous texts are turned off; just append lines at the bottom for line in text: self._l.append((idx, line)) self._v.append(()) return idx def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" self._v[index] # check index is valid for origin, line in self._l: if origin == index: yield origin, line def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def show_annotated(knit): """Show a knit in 'blame' style""" for vers, text, live in knit: if not live: continue print '%6d | %s' % (vers, text) def knit2text(knit): """Return a sequence of lines containing just the live text from a knit.""" return [text for (vers, text, live) in knit if live] def update_knit(knit, new_vers, new_lines): """Return a new knit whose text matches new_lines. First of all the knit is diffed against the new lines, considering only the text of the lines from the knit. This identifies lines unchanged from the knit, plus insertions and deletions. The deletions are marked as deleted. The insertions are added with their new values. """ if not isinstance(new_vers, int): raise TypeError('new version-id must be an int: %r' % new_vers) from difflib import SequenceMatcher knit_lines = knit2text(knit) m = SequenceMatcher(None, knit_lines, new_lines) for block in m.get_matching_blocks(): print "a[%d] and b[%d] match for %d elements" % block new_knit = [] for tag, i1, i2, j1, j2 in m.get_opcodes(): print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" % (tag, i1, i2, knit_lines[i1:i2], j1, j2, new_lines[j1:j2])) if tag == 'equal': new_knit.extend(knit[i1:i2]) elif tag == 'delete': for i in range(i1, i2): kl = knit[i] new_knit.append((kl[0], kl[1], False)) return new_knit def main(): print '***** annotated:' show_annotated(knit2) print '***** plain text:' print '\n'.join(knit2text(knit2)) text3 = """hello world an inserted line hello boys""".split('\n') print repr(knit2text(knit2)) print repr(text3) knit3 = update_knit(knit2, 3, text3) print '***** result of update:' show_annotated(knit3) commit refs/heads/tmp mark :861 committer Martin Pool 1119841722 +1000 data 21 Remove dead test code from :860 M 644 inline knit.py data 3325 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # GNU GPL v2 # Author: Martin Pool """knit - a weave-like structure""" class Knit(object): """knit - versioned text file storage. A Knit manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this knit and the version-id is used to reference it in the larger world. _l List of edit instructions. Each line is stored as a tuple of (index-id, text). The line is present in the version equal to index-id. _v List of versions, indexed by index number. Each one is an empty tuple because the version_id isn't stored yet. """ def __init__(self): self._l = [] self._v = [] def add(self, text): """Add a single text on top of the weave. Returns the index number of the newly added version.""" if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) idx = len(self._v) # all of the previous texts are turned off; just append lines at the bottom for line in text: self._l.append((idx, line)) self._v.append(()) return idx def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" self._v[index] # check index is valid for origin, line in self._l: if origin == index: yield origin, line def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def update_knit(knit, new_vers, new_lines): """Return a new knit whose text matches new_lines. First of all the knit is diffed against the new lines, considering only the text of the lines from the knit. This identifies lines unchanged from the knit, plus insertions and deletions. The deletions are marked as deleted. The insertions are added with their new values. """ if not isinstance(new_vers, int): raise TypeError('new version-id must be an int: %r' % new_vers) from difflib import SequenceMatcher knit_lines = knit2text(knit) m = SequenceMatcher(None, knit_lines, new_lines) for block in m.get_matching_blocks(): print "a[%d] and b[%d] match for %d elements" % block new_knit = [] for tag, i1, i2, j1, j2 in m.get_opcodes(): print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" % (tag, i1, i2, knit_lines[i1:i2], j1, j2, new_lines[j1:j2])) if tag == 'equal': new_knit.extend(knit[i1:i2]) elif tag == 'delete': for i in range(i1, i2): kl = knit[i] new_knit.append((kl[0], kl[1], False)) return new_knit commit refs/heads/tmp mark :862 committer Martin Pool 1119842050 +1000 data 48 Log messages when test is starting and finishing from :861 M 644 inline testsweet.py data 9120 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") raise class CommandFailed(Exception): pass class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr, so that we can run it # through a specified Python intepreter OVERRIDE_PYTHON = None # to run with alternative python 'python' BZRPATH = 'bzr' _log_buf = "" def setUp(self): super(TestBase, self).setUp() self.log("%s setup" % self.id()) def tearDown(self): super(TestBase, self).tearDown() self.log("%s teardown" % self.id()) self.log('') def formcmd(self, cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = self.BZRPATH if self.OVERRIDE_PYTHON: cmd.insert(0, self.OVERRIDE_PYTHON) self.log('$ %r' % cmd) return cmd def runcmd(self, cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = self.formcmd(cmd) self.log('$ ' + ' '.join(cmd)) actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(self, cmd, retcode=0): """Run a command and return its output""" cmd = self.formcmd(cmd) child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG) outd, errd = child.communicate() self.log(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def build_tree(self, shape): """Build a test tree according to a pattern. shape is a sequence of file specifications. If the final character is '/', a directory is created. This doesn't add anything to a branch. """ # XXX: It's OK to just create them using forward slashes on windows? import os for name in shape: assert isinstance(name, basestring) if name[-1] == '/': os.mkdir(name[:-1]) else: f = file(name, 'wt') print >>f, "contents of", name f.close() def log(self, msg): """Log a message to a progress file""" self._log_buf = self._log_buf + str(msg) + '\n' print >>self.TEST_LOG, msg def check_inventory_shape(self, inv, shape): """ Compare an inventory to a list of expected names. Fail if they are not precisely equal. """ extras = [] shape = list(shape) # copy for path, ie in inv.entries(): name = path.replace('\\', '/') if ie.kind == 'dir': name = name + '/' if name in shape: shape.remove(name) else: extras.append(name) if shape: self.fail("expected paths not found in inventory: %r" % shape) if extras: self.fail("unexpected paths found in inventory: %r" % extras) def check_file_contents(self, filename, expect): self.log("check contents of file %s" % filename) contents = file(filename, 'r').read() if contents != expect: self.log("expected: %r" % expected) self.log("actually: %r" % contents) self.fail("contents of %s not as expected") class InTempDir(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.test_dir = os.path.join(self.TEST_ROOT, self.__class__.__name__) os.mkdir(self.test_dir) os.chdir(self.test_dir) def tearDown(self): import os os.chdir(self.TEST_ROOT) class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ def __init__(self, out): self.out = out TestResult.__init__(self) def startTest(self, test): # TODO: Maybe show test.shortDescription somewhere? print >>self.out, '%-60.60s' % test.id(), self.out.flush() TestResult.startTest(self, test) def stopTest(self, test): # print TestResult.stopTest(self, test) def addError(self, test, err): print >>self.out, 'ERROR' TestResult.addError(self, test, err) _show_test_failure('error', test, err, self.out) def addFailure(self, test, err): print >>self.out, 'FAILURE' TestResult.addFailure(self, test, err) _show_test_failure('failure', test, err, self.out) def addSuccess(self, test): print >>self.out, 'OK' TestResult.addSuccess(self, test) def selftest(): from unittest import TestLoader, TestSuite import bzrlib import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning from doctest import DocTestSuite import os import shutil import time import sys suite = TestSuite() tl = TestLoader() for m in bzrlib.selftest.whitebox, \ bzrlib.selftest.versioning: suite.addTest(tl.loadTestsFromModule(m)) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands: suite.addTest(DocTestSuite(m)) suite.addTest(bzrlib.selftest.blackbox.suite()) return run_suite(suite) def run_suite(suite): import os import shutil import time import sys _setup_test_log() _setup_test_dir() print # save stdout & stderr so there's no leakage from code-under-test real_stdout = sys.stdout real_stderr = sys.stderr sys.stdout = sys.stderr = TestBase.TEST_LOG try: result = _MyResult(real_stdout) suite.run(result) finally: sys.stdout = real_stdout sys.stderr = real_stderr _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os log_filename = os.path.abspath('test.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "bzr tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_ROOT = os.path.abspath("test.tmp") print '%-30s %s' % ('running tests in', TestBase.TEST_ROOT) if os.path.exists(TestBase.TEST_ROOT): shutil.rmtree(TestBase.TEST_ROOT) os.mkdir(TestBase.TEST_ROOT) os.chdir(TestBase.TEST_ROOT) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_ROOT, '.bzr')) def _show_results(result): print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, exc_info, out): from traceback import print_exception print >>out, '-' * 60 print >>out, case desc = case.shortDescription() if desc: print >>out, ' (%s)' % desc print_exception(exc_info[0], exc_info[1], exc_info[2], None, out) if isinstance(case, TestBase): print >>out print >>out, 'log from this test:' print >>out, case._log_buf print >>out, '-' * 60 commit refs/heads/tmp mark :863 committer Martin Pool 1119842062 +1000 data 20 Add Knit.dump method from :862 M 644 inline knit.py data 3462 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # GNU GPL v2 # Author: Martin Pool """knit - a weave-like structure""" class Knit(object): """knit - versioned text file storage. A Knit manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this knit and the version-id is used to reference it in the larger world. _l List of edit instructions. Each line is stored as a tuple of (index-id, text). The line is present in the version equal to index-id. _v List of versions, indexed by index number. Each one is an empty tuple because the version_id isn't stored yet. """ def __init__(self): self._l = [] self._v = [] def add(self, text): """Add a single text on top of the weave. Returns the index number of the newly added version.""" if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) idx = len(self._v) # all of the previous texts are turned off; just append lines at the bottom for line in text: self._l.append((idx, line)) self._v.append(()) return idx def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" self._v[index] # check index is valid for origin, line in self._l: if origin == index: yield origin, line def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "knit lines:" pprint(self._l, to_file) def update_knit(knit, new_vers, new_lines): """Return a new knit whose text matches new_lines. First of all the knit is diffed against the new lines, considering only the text of the lines from the knit. This identifies lines unchanged from the knit, plus insertions and deletions. The deletions are marked as deleted. The insertions are added with their new values. """ if not isinstance(new_vers, int): raise TypeError('new version-id must be an int: %r' % new_vers) from difflib import SequenceMatcher knit_lines = knit2text(knit) m = SequenceMatcher(None, knit_lines, new_lines) for block in m.get_matching_blocks(): print "a[%d] and b[%d] match for %d elements" % block new_knit = [] for tag, i1, i2, j1, j2 in m.get_opcodes(): print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" % (tag, i1, i2, knit_lines[i1:i2], j1, j2, new_lines[j1:j2])) if tag == 'equal': new_knit.extend(knit[i1:i2]) elif tag == 'delete': for i in range(i1, i2): kl = knit[i] new_knit.append((kl[0], kl[1], False)) return new_knit commit refs/heads/tmp mark :864 committer Martin Pool 1119842373 +1000 data 27 Add stubbed-out TestSkipped from :863 M 644 inline testsweet.py data 9257 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") raise class CommandFailed(Exception): pass class TestSkipped(Exception): """Indicates that a test was intentionally skipped, rather than failing.""" # XXX: Not used yet class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr, so that we can run it # through a specified Python intepreter OVERRIDE_PYTHON = None # to run with alternative python 'python' BZRPATH = 'bzr' _log_buf = "" def setUp(self): super(TestBase, self).setUp() self.log("%s setup" % self.id()) def tearDown(self): super(TestBase, self).tearDown() self.log("%s teardown" % self.id()) self.log('') def formcmd(self, cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = self.BZRPATH if self.OVERRIDE_PYTHON: cmd.insert(0, self.OVERRIDE_PYTHON) self.log('$ %r' % cmd) return cmd def runcmd(self, cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = self.formcmd(cmd) self.log('$ ' + ' '.join(cmd)) actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(self, cmd, retcode=0): """Run a command and return its output""" cmd = self.formcmd(cmd) child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG) outd, errd = child.communicate() self.log(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def build_tree(self, shape): """Build a test tree according to a pattern. shape is a sequence of file specifications. If the final character is '/', a directory is created. This doesn't add anything to a branch. """ # XXX: It's OK to just create them using forward slashes on windows? import os for name in shape: assert isinstance(name, basestring) if name[-1] == '/': os.mkdir(name[:-1]) else: f = file(name, 'wt') print >>f, "contents of", name f.close() def log(self, msg): """Log a message to a progress file""" self._log_buf = self._log_buf + str(msg) + '\n' print >>self.TEST_LOG, msg def check_inventory_shape(self, inv, shape): """ Compare an inventory to a list of expected names. Fail if they are not precisely equal. """ extras = [] shape = list(shape) # copy for path, ie in inv.entries(): name = path.replace('\\', '/') if ie.kind == 'dir': name = name + '/' if name in shape: shape.remove(name) else: extras.append(name) if shape: self.fail("expected paths not found in inventory: %r" % shape) if extras: self.fail("unexpected paths found in inventory: %r" % extras) def check_file_contents(self, filename, expect): self.log("check contents of file %s" % filename) contents = file(filename, 'r').read() if contents != expect: self.log("expected: %r" % expected) self.log("actually: %r" % contents) self.fail("contents of %s not as expected") class InTempDir(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.test_dir = os.path.join(self.TEST_ROOT, self.__class__.__name__) os.mkdir(self.test_dir) os.chdir(self.test_dir) def tearDown(self): import os os.chdir(self.TEST_ROOT) class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ def __init__(self, out): self.out = out TestResult.__init__(self) def startTest(self, test): # TODO: Maybe show test.shortDescription somewhere? print >>self.out, '%-60.60s' % test.id(), self.out.flush() TestResult.startTest(self, test) def stopTest(self, test): # print TestResult.stopTest(self, test) def addError(self, test, err): print >>self.out, 'ERROR' TestResult.addError(self, test, err) _show_test_failure('error', test, err, self.out) def addFailure(self, test, err): print >>self.out, 'FAILURE' TestResult.addFailure(self, test, err) _show_test_failure('failure', test, err, self.out) def addSuccess(self, test): print >>self.out, 'OK' TestResult.addSuccess(self, test) def selftest(): from unittest import TestLoader, TestSuite import bzrlib import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning from doctest import DocTestSuite import os import shutil import time import sys suite = TestSuite() tl = TestLoader() for m in bzrlib.selftest.whitebox, \ bzrlib.selftest.versioning: suite.addTest(tl.loadTestsFromModule(m)) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands: suite.addTest(DocTestSuite(m)) suite.addTest(bzrlib.selftest.blackbox.suite()) return run_suite(suite) def run_suite(suite): import os import shutil import time import sys _setup_test_log() _setup_test_dir() print # save stdout & stderr so there's no leakage from code-under-test real_stdout = sys.stdout real_stderr = sys.stderr sys.stdout = sys.stderr = TestBase.TEST_LOG try: result = _MyResult(real_stdout) suite.run(result) finally: sys.stdout = real_stdout sys.stderr = real_stderr _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os log_filename = os.path.abspath('test.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "bzr tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_ROOT = os.path.abspath("test.tmp") print '%-30s %s' % ('running tests in', TestBase.TEST_ROOT) if os.path.exists(TestBase.TEST_ROOT): shutil.rmtree(TestBase.TEST_ROOT) os.mkdir(TestBase.TEST_ROOT) os.chdir(TestBase.TEST_ROOT) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_ROOT, '.bzr')) def _show_results(result): print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, exc_info, out): from traceback import print_exception print >>out, '-' * 60 print >>out, case desc = case.shortDescription() if desc: print >>out, ' (%s)' % desc print_exception(exc_info[0], exc_info[1], exc_info[2], None, out) if isinstance(case, TestBase): print >>out print >>out, 'log from this test:' print >>out, case._log_buf print >>out, '-' * 60 commit refs/heads/tmp mark :865 committer Martin Pool 1119843118 +1000 data 287 Knit structure now allows for versions to include the lines present in other versions. (Not applied transitively for the moment.) This is applied when unpacking texts. Start adding Knit.check() Hand-pack a knit and check that included versions are respected when unpacking lines. from :864 M 644 inline knit.py data 4129 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # GNU GPL v2 # Author: Martin Pool """knit - a weave-like structure""" class Knit(object): """knit - versioned text file storage. A Knit manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this knit and the version-id is used to reference it in the larger world. _l List of edit instructions. Each line is stored as a tuple of (index-id, text). The line is present in the version equal to index-id. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, text): """Add a single text on top of the weave. Returns the index number of the newly added version.""" if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) idx = len(self._v) # all of the previous texts are turned off; just append lines at the bottom for line in text: self._l.append((idx, line)) included = () vers_info = (included,) self._v.append(vers_info) return idx def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" vers_info = self._v[index] included = set(vers_info[0]) included.add(index) for origin, line in self._l: if origin in included: yield origin, line def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "knit lines:" pprint(self._l, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise ValueError("invalid included_version %d for index %d" % (vi, index)) if vi in included: raise ValueError("repeated included_version %d for index %d" % (vi, index)) included.add(vi) def update_knit(knit, new_vers, new_lines): """Return a new knit whose text matches new_lines. First of all the knit is diffed against the new lines, considering only the text of the lines from the knit. This identifies lines unchanged from the knit, plus insertions and deletions. The deletions are marked as deleted. The insertions are added with their new values. """ if not isinstance(new_vers, int): raise TypeError('new version-id must be an int: %r' % new_vers) from difflib import SequenceMatcher knit_lines = knit2text(knit) m = SequenceMatcher(None, knit_lines, new_lines) for block in m.get_matching_blocks(): print "a[%d] and b[%d] match for %d elements" % block new_knit = [] for tag, i1, i2, j1, j2 in m.get_opcodes(): print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" % (tag, i1, i2, knit_lines[i1:i2], j1, j2, new_lines[j1:j2])) if tag == 'equal': new_knit.extend(knit[i1:i2]) elif tag == 'delete': for i in range(i1, i2): kl = knit[i] new_knit.append((kl[0], kl[1], False)) return new_knit M 644 inline testknit.py data 3269 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test case for knit/weave algorithm""" from testsweet import TestBase from knit import Knit # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Knit() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Knit() idx = k.add(TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Knit() k.add(TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Knit() idx = k.add(TEXT_0) self.assertEqual(idx, 0) idx = k.add(TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class MatchedLine(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): return #################################### SKIPPED k = Knit() k.add(['line 1']) k.add(['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a knit with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Knit() k._v = [((),), ((0,),)] k._l = [(0, "first line"), (1, "second line")] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) def testknit(): import testsweet from unittest import TestSuite, TestLoader import testknit tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testknit)) return testsweet.run_suite(suite) if __name__ == '__main__': import sys sys.exit(testknit()) commit refs/heads/tmp mark :866 committer Martin Pool 1119843297 +1000 data 34 Another test for version inclusion from :865 M 644 inline testknit.py data 3930 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test case for knit/weave algorithm""" from testsweet import TestBase from knit import Knit # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Knit() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Knit() idx = k.add(TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Knit() k.add(TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Knit() idx = k.add(TEXT_0) self.assertEqual(idx, 0) idx = k.add(TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class MatchedLine(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): return #################################### SKIPPED k = Knit() k.add(['line 1']) k.add(['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a knit with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Knit() k._v = [((),), ((0,),)] k._l = [(0, "first line"), (1, "second line")] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Knit with two diverged texts based on version 0. """ def runTest(self): k = Knit() k._v = [((),), ((0,),), ((0,),),] k._l = [(0, "first line"), (1, "second line"), (2, "alternative second line"),] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) def testknit(): import testsweet from unittest import TestSuite, TestLoader import testknit tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testknit)) return testsweet.run_suite(suite) if __name__ == '__main__': import sys sys.exit(testknit()) commit refs/heads/tmp mark :867 committer Martin Pool 1119843380 +1000 data 43 Fix inverted shell return code for testknit from :866 M 644 inline testknit.py data 3958 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test case for knit/weave algorithm""" from testsweet import TestBase from knit import Knit # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Knit() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Knit() idx = k.add(TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Knit() k.add(TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Knit() idx = k.add(TEXT_0) self.assertEqual(idx, 0) idx = k.add(TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class MatchedLine(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): return #################################### SKIPPED k = Knit() k.add(['line 1']) k.add(['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a knit with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Knit() k._v = [((),), ((0,),)] k._l = [(0, "first line"), (1, "second line")] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Knit with two diverged texts based on version 0. """ def runTest(self): k = Knit() k._v = [((),), ((0,),), ((0,),),] k._l = [(0, "first line"), (1, "second line"), (2, "alternative second line"),] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) def testknit(): import testsweet from unittest import TestSuite, TestLoader import testknit tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testknit)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testknit()) commit refs/heads/tmp mark :868 committer Martin Pool 1119843446 +1000 data 11 formatting from :867 M 644 inline knit.py data 4129 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # GNU GPL v2 # Author: Martin Pool """knit - a weave-like structure""" class Knit(object): """knit - versioned text file storage. A Knit manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this knit and the version-id is used to reference it in the larger world. _l List of edit instructions. Each line is stored as a tuple of (index-id, text). The line is present in the version equal to index-id. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, text): """Add a single text on top of the weave. Returns the index number of the newly added version.""" if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) idx = len(self._v) # all of the previous texts are turned off; just append lines at the bottom for line in text: self._l.append((idx, line)) included = () vers_info = (included,) self._v.append(vers_info) return idx def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" vers_info = self._v[index] included = set(vers_info[0]) included.add(index) for origin, line in self._l: if origin in included: yield origin, line def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "knit lines:" pprint(self._l, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise ValueError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise ValueError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def update_knit(knit, new_vers, new_lines): """Return a new knit whose text matches new_lines. First of all the knit is diffed against the new lines, considering only the text of the lines from the knit. This identifies lines unchanged from the knit, plus insertions and deletions. The deletions are marked as deleted. The insertions are added with their new values. """ if not isinstance(new_vers, int): raise TypeError('new version-id must be an int: %r' % new_vers) from difflib import SequenceMatcher knit_lines = knit2text(knit) m = SequenceMatcher(None, knit_lines, new_lines) for block in m.get_matching_blocks(): print "a[%d] and b[%d] match for %d elements" % block new_knit = [] for tag, i1, i2, j1, j2 in m.get_opcodes(): print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" % (tag, i1, i2, knit_lines[i1:i2], j1, j2, new_lines[j1:j2])) if tag == 'equal': new_knit.extend(knit[i1:i2]) elif tag == 'delete': for i in range(i1, i2): kl = knit[i] new_knit.append((kl[0], kl[1], False)) return new_knit commit refs/heads/tmp mark :869 committer Martin Pool 1119845544 +1000 data 75 Use objects rather than tuples for tracking VerInfo for better readability from :868 M 644 inline knit.py data 4198 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # GNU GPL v2 # Author: Martin Pool """knit - a weave-like structure""" class VerInfo(object): included = frozenset() def __init__(self, included=None): if included: self.included = set(included) class Knit(object): """knit - versioned text file storage. A Knit manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this knit and the version-id is used to reference it in the larger world. _l List of edit instructions. Each line is stored as a tuple of (index-id, text). The line is present in the version equal to index-id. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, text): """Add a single text on top of the weave. Returns the index number of the newly added version.""" if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) idx = len(self._v) # all of the previous texts are turned off; just append lines at the bottom for line in text: self._l.append((idx, line)) vi = VerInfo() self._v.append(vi) return idx def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" vi = self._v[index] for origin, line in self._l: if (origin == index) or (origin in vi.included): yield origin, line def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "knit lines:" pprint(self._l, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise ValueError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise ValueError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def update_knit(knit, new_vers, new_lines): """Return a new knit whose text matches new_lines. First of all the knit is diffed against the new lines, considering only the text of the lines from the knit. This identifies lines unchanged from the knit, plus insertions and deletions. The deletions are marked as deleted. The insertions are added with their new values. """ if not isinstance(new_vers, int): raise TypeError('new version-id must be an int: %r' % new_vers) from difflib import SequenceMatcher knit_lines = knit2text(knit) m = SequenceMatcher(None, knit_lines, new_lines) for block in m.get_matching_blocks(): print "a[%d] and b[%d] match for %d elements" % block new_knit = [] for tag, i1, i2, j1, j2 in m.get_opcodes(): print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" % (tag, i1, i2, knit_lines[i1:i2], j1, j2, new_lines[j1:j2])) if tag == 'equal': new_knit.extend(knit[i1:i2]) elif tag == 'delete': for i in range(i1, i2): kl = knit[i] new_knit.append((kl[0], kl[1], False)) return new_knit M 644 inline testknit.py data 3989 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test case for knit/weave algorithm""" from testsweet import TestBase from knit import Knit, VerInfo # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Knit() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Knit() idx = k.add(TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Knit() k.add(TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Knit() idx = k.add(TEXT_0) self.assertEqual(idx, 0) idx = k.add(TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class MatchedLine(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): return k = Knit() k.add(['line 1']) k.add(['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a knit with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Knit() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [(0, "first line"), (1, "second line")] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Knit with two diverged texts based on version 0. """ def runTest(self): k = Knit() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [(0, "first line"), (1, "second line"), (2, "alternative second line"),] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) def testknit(): import testsweet from unittest import TestSuite, TestLoader import testknit tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testknit)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testknit()) commit refs/heads/tmp mark :870 committer Martin Pool 1119845871 +1000 data 45 Better Knit.dump method Add VerInfo.__repr__ from :869 M 644 inline knit.py data 4440 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # GNU GPL v2 # Author: Martin Pool """knit - a weave-like structure""" class VerInfo(object): included = frozenset() def __init__(self, included=None): if included: self.included = set(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class Knit(object): """knit - versioned text file storage. A Knit manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this knit and the version-id is used to reference it in the larger world. _l List of edit instructions. Each line is stored as a tuple of (index-id, text). The line is present in the version equal to index-id. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, text): """Add a single text on top of the weave. Returns the index number of the newly added version.""" if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) idx = len(self._v) # all of the previous texts are turned off; just append lines at the bottom for line in text: self._l.append((idx, line)) vi = VerInfo() self._v.append(vi) return idx def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" vi = self._v[index] for origin, line in self._l: if (origin == index) or (origin in vi.included): yield origin, line def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Knit._l = ", pprint(self._l, to_file) print >>to_file, "Knit._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise ValueError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise ValueError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def update_knit(knit, new_vers, new_lines): """Return a new knit whose text matches new_lines. First of all the knit is diffed against the new lines, considering only the text of the lines from the knit. This identifies lines unchanged from the knit, plus insertions and deletions. The deletions are marked as deleted. The insertions are added with their new values. """ if not isinstance(new_vers, int): raise TypeError('new version-id must be an int: %r' % new_vers) from difflib import SequenceMatcher knit_lines = knit2text(knit) m = SequenceMatcher(None, knit_lines, new_lines) for block in m.get_matching_blocks(): print "a[%d] and b[%d] match for %d elements" % block new_knit = [] for tag, i1, i2, j1, j2 in m.get_opcodes(): print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" % (tag, i1, i2, knit_lines[i1:i2], j1, j2, new_lines[j1:j2])) if tag == 'equal': new_knit.extend(knit[i1:i2]) elif tag == 'delete': for i in range(i1, i2): kl = knit[i] new_knit.append((kl[0], kl[1], False)) return new_knit commit refs/heads/tmp mark :871 committer Martin Pool 1119845890 +1000 data 40 remove a reference to bzr from testsweet from :870 M 644 inline testsweet.py data 9253 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase try: import shutil from subprocess import call, Popen, PIPE except ImportError, e: sys.stderr.write("sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") raise class CommandFailed(Exception): pass class TestSkipped(Exception): """Indicates that a test was intentionally skipped, rather than failing.""" # XXX: Not used yet class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr, so that we can run it # through a specified Python intepreter OVERRIDE_PYTHON = None # to run with alternative python 'python' BZRPATH = 'bzr' _log_buf = "" def setUp(self): super(TestBase, self).setUp() self.log("%s setup" % self.id()) def tearDown(self): super(TestBase, self).tearDown() self.log("%s teardown" % self.id()) self.log('') def formcmd(self, cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = self.BZRPATH if self.OVERRIDE_PYTHON: cmd.insert(0, self.OVERRIDE_PYTHON) self.log('$ %r' % cmd) return cmd def runcmd(self, cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" cmd = self.formcmd(cmd) self.log('$ ' + ' '.join(cmd)) actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(self, cmd, retcode=0): """Run a command and return its output""" cmd = self.formcmd(cmd) child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG) outd, errd = child.communicate() self.log(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def build_tree(self, shape): """Build a test tree according to a pattern. shape is a sequence of file specifications. If the final character is '/', a directory is created. This doesn't add anything to a branch. """ # XXX: It's OK to just create them using forward slashes on windows? import os for name in shape: assert isinstance(name, basestring) if name[-1] == '/': os.mkdir(name[:-1]) else: f = file(name, 'wt') print >>f, "contents of", name f.close() def log(self, msg): """Log a message to a progress file""" self._log_buf = self._log_buf + str(msg) + '\n' print >>self.TEST_LOG, msg def check_inventory_shape(self, inv, shape): """ Compare an inventory to a list of expected names. Fail if they are not precisely equal. """ extras = [] shape = list(shape) # copy for path, ie in inv.entries(): name = path.replace('\\', '/') if ie.kind == 'dir': name = name + '/' if name in shape: shape.remove(name) else: extras.append(name) if shape: self.fail("expected paths not found in inventory: %r" % shape) if extras: self.fail("unexpected paths found in inventory: %r" % extras) def check_file_contents(self, filename, expect): self.log("check contents of file %s" % filename) contents = file(filename, 'r').read() if contents != expect: self.log("expected: %r" % expected) self.log("actually: %r" % contents) self.fail("contents of %s not as expected") class InTempDir(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.test_dir = os.path.join(self.TEST_ROOT, self.__class__.__name__) os.mkdir(self.test_dir) os.chdir(self.test_dir) def tearDown(self): import os os.chdir(self.TEST_ROOT) class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ def __init__(self, out): self.out = out TestResult.__init__(self) def startTest(self, test): # TODO: Maybe show test.shortDescription somewhere? print >>self.out, '%-60.60s' % test.id(), self.out.flush() TestResult.startTest(self, test) def stopTest(self, test): # print TestResult.stopTest(self, test) def addError(self, test, err): print >>self.out, 'ERROR' TestResult.addError(self, test, err) _show_test_failure('error', test, err, self.out) def addFailure(self, test, err): print >>self.out, 'FAILURE' TestResult.addFailure(self, test, err) _show_test_failure('failure', test, err, self.out) def addSuccess(self, test): print >>self.out, 'OK' TestResult.addSuccess(self, test) def selftest(): from unittest import TestLoader, TestSuite import bzrlib import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning from doctest import DocTestSuite import os import shutil import time import sys suite = TestSuite() tl = TestLoader() for m in bzrlib.selftest.whitebox, \ bzrlib.selftest.versioning: suite.addTest(tl.loadTestsFromModule(m)) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands: suite.addTest(DocTestSuite(m)) suite.addTest(bzrlib.selftest.blackbox.suite()) return run_suite(suite) def run_suite(suite): import os import shutil import time import sys _setup_test_log() _setup_test_dir() print # save stdout & stderr so there's no leakage from code-under-test real_stdout = sys.stdout real_stderr = sys.stderr sys.stdout = sys.stderr = TestBase.TEST_LOG try: result = _MyResult(real_stdout) suite.run(result) finally: sys.stdout = real_stdout sys.stderr = real_stderr _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os log_filename = os.path.abspath('test.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_ROOT = os.path.abspath("test.tmp") print '%-30s %s' % ('running tests in', TestBase.TEST_ROOT) if os.path.exists(TestBase.TEST_ROOT): shutil.rmtree(TestBase.TEST_ROOT) os.mkdir(TestBase.TEST_ROOT) os.chdir(TestBase.TEST_ROOT) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_ROOT, '.bzr')) def _show_results(result): print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, exc_info, out): from traceback import print_exception print >>out, '-' * 60 print >>out, case desc = case.shortDescription() if desc: print >>out, ' (%s)' % desc print_exception(exc_info[0], exc_info[1], exc_info[2], None, out) if isinstance(case, TestBase): print >>out print >>out, 'log from this test:' print >>out, case._log_buf print >>out, '-' * 60 commit refs/heads/tmp mark :872 committer Martin Pool 1119846115 +1000 data 32 Factor out Knit.extract() method from :871 M 644 inline knit.py data 4704 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # GNU GPL v2 # Author: Martin Pool """knit - a weave-like structure""" class VerInfo(object): included = frozenset() def __init__(self, included=None): if included: self.included = set(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class Knit(object): """knit - versioned text file storage. A Knit manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this knit and the version-id is used to reference it in the larger world. _l List of edit instructions. Each line is stored as a tuple of (index-id, text). The line is present in the version equal to index-id. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, text): """Add a single text on top of the weave. Returns the index number of the newly added version.""" if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) idx = len(self._v) # all of the previous texts are turned off; just append lines at the bottom for line in text: self._l.append((idx, line)) vi = VerInfo() self._v.append(vi) return idx def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" vi = self._v[index] included = set(vi.included) included.add(index) return iter(self.extract(included)) def extract(self, included): """Yield annotation of lines in included set. The set typically but not necessarily corresponds to a version. """ for origin, line in self._l: if origin in included: yield origin, line def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Knit._l = ", pprint(self._l, to_file) print >>to_file, "Knit._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise ValueError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise ValueError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def update_knit(knit, new_vers, new_lines): """Return a new knit whose text matches new_lines. First of all the knit is diffed against the new lines, considering only the text of the lines from the knit. This identifies lines unchanged from the knit, plus insertions and deletions. The deletions are marked as deleted. The insertions are added with their new values. """ if not isinstance(new_vers, int): raise TypeError('new version-id must be an int: %r' % new_vers) from difflib import SequenceMatcher knit_lines = knit2text(knit) m = SequenceMatcher(None, knit_lines, new_lines) for block in m.get_matching_blocks(): print "a[%d] and b[%d] match for %d elements" % block new_knit = [] for tag, i1, i2, j1, j2 in m.get_opcodes(): print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" % (tag, i1, i2, knit_lines[i1:i2], j1, j2, new_lines[j1:j2])) if tag == 'equal': new_knit.extend(knit[i1:i2]) elif tag == 'delete': for i in range(i1, i2): kl = knit[i] new_knit.append((kl[0], kl[1], False)) return new_knit commit refs/heads/tmp mark :873 committer Martin Pool 1119846367 +1000 data 48 Start computing a delta to insert a new revision from :872 M 644 inline knit.py data 4932 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # GNU GPL v2 # Author: Martin Pool """knit - a weave-like structure""" class VerInfo(object): included = frozenset() def __init__(self, included=None): if included: self.included = set(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class Knit(object): """knit - versioned text file storage. A Knit manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this knit and the version-id is used to reference it in the larger world. _l List of edit instructions. Each line is stored as a tuple of (index-id, text). The line is present in the version equal to index-id. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, text): """Add a single text on top of the weave. Returns the index number of the newly added version.""" if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) idx = len(self._v) # all of the previous texts are turned off; just append lines at the bottom for line in text: self._l.append((idx, line)) vi = VerInfo() self._v.append(vi) return idx def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" vi = self._v[index] included = set(vi.included) included.add(index) return iter(self.extract(included)) def extract(self, included): """Yield annotation of lines in included set. The set typically but not necessarily corresponds to a version. """ for origin, line in self._l: if origin in included: yield origin, line def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Knit._l = ", pprint(self._l, to_file) print >>to_file, "Knit._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise ValueError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise ValueError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. """ def update_knit(knit, new_vers, new_lines): """Return a new knit whose text matches new_lines. First of all the knit is diffed against the new lines, considering only the text of the lines from the knit. This identifies lines unchanged from the knit, plus insertions and deletions. The deletions are marked as deleted. The insertions are added with their new values. """ if not isinstance(new_vers, int): raise TypeError('new version-id must be an int: %r' % new_vers) from difflib import SequenceMatcher knit_lines = knit2text(knit) m = SequenceMatcher(None, knit_lines, new_lines) for block in m.get_matching_blocks(): print "a[%d] and b[%d] match for %d elements" % block new_knit = [] for tag, i1, i2, j1, j2 in m.get_opcodes(): print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" % (tag, i1, i2, knit_lines[i1:i2], j1, j2, new_lines[j1:j2])) if tag == 'equal': new_knit.extend(knit[i1:i2]) elif tag == 'delete': for i in range(i1, i2): kl = knit[i] new_knit.append((kl[0], kl[1], False)) return new_knit M 644 inline testknit.py data 4390 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test case for knit/weave algorithm""" from testsweet import TestBase from knit import Knit, VerInfo # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Knit() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Knit() idx = k.add(TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Knit() k.add(TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Knit() idx = k.add(TEXT_0) self.assertEqual(idx, 0) idx = k.add(TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class Delta1(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): from pprint import pformat k = Knit() k.add(['line 1']) changes = k._delta(set([0]), ['line 1', 'new line']) self.log('raw changes: ' + pformat(changes)) class MatchedLine(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): return ########################## SKIPPED k = Knit() k.add(['line 1']) k.add(['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a knit with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Knit() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [(0, "first line"), (1, "second line")] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Knit with two diverged texts based on version 0. """ def runTest(self): k = Knit() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [(0, "first line"), (1, "second line"), (2, "alternative second line"),] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) def testknit(): import testsweet from unittest import TestSuite, TestLoader import testknit tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testknit)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testknit()) commit refs/heads/tmp mark :874 committer Martin Pool 1119858225 +1000 data 107 Calculate delta for new versions relative to a set of parent versions. Simple test for delta calculation. from :873 M 644 inline knit.py data 7072 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # GNU GPL v2 # Author: Martin Pool """knit - a weave-like structure""" class VerInfo(object): included = frozenset() def __init__(self, included=None): if included: self.included = set(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class Knit(object): """knit - versioned text file storage. A Knit manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this knit and the version-id is used to reference it in the larger world. _l List of edit instructions. Each line is stored as a tuple of (index-id, text). The line is present in the version equal to index-id. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, text): """Add a single text on top of the weave. Returns the index number of the newly added version.""" if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) idx = len(self._v) # all of the previous texts are turned off; just append lines at the bottom for line in text: self._l.append((idx, line)) vi = VerInfo() self._v.append(vi) return idx def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" vi = self._v[index] included = set(vi.included) included.add(index) return iter(self._extract(included)) def _extract(self, included): """Yield annotation of lines in included set. The set typically but not necessarily corresponds to a version. """ for origin, line in self._l: if origin in included: yield origin, line def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Knit._l = ", pprint(self._l, to_file) print >>to_file, "Knit._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise ValueError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise ValueError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (line1, line2, newlines), indicating that line1 through line2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] print 'my lines:' pprint(self._l) lineno = 0 for origin, line in self._l: if origin in included: basis.append((lineno, line)) lineno += 1 assert lineno == len(self._l) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) print 'basis sequence:' pprint(basis) for tag, i1, i2, j1, j2 in s.get_opcodes(): print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][0] real_i2 = basis[i2][0] # find the text identified by j: if j1 == j2: newlines = [] else: assert j1 >= 0 assert j2 >= j1 assert j2 <= len(lines) newlines = lines[j1:j2] yield real_i1, real_i2, newlines def update_knit(knit, new_vers, new_lines): """Return a new knit whose text matches new_lines. First of all the knit is diffed against the new lines, considering only the text of the lines from the knit. This identifies lines unchanged from the knit, plus insertions and deletions. The deletions are marked as deleted. The insertions are added with their new values. """ if not isinstance(new_vers, int): raise TypeError('new version-id must be an int: %r' % new_vers) from difflib import SequenceMatcher knit_lines = knit2text(knit) m = SequenceMatcher(None, knit_lines, new_lines) for block in m.get_matching_blocks(): print "a[%d] and b[%d] match for %d elements" % block new_knit = [] for tag, i1, i2, j1, j2 in m.get_opcodes(): print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" % (tag, i1, i2, knit_lines[i1:i2], j1, j2, new_lines[j1:j2])) if tag == 'equal': new_knit.extend(knit[i1:i2]) elif tag == 'delete': for i in range(i1, i2): kl = knit[i] new_knit.append((kl[0], kl[1], False)) return new_knit M 644 inline testknit.py data 4543 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test case for knit/weave algorithm""" from testsweet import TestBase from knit import Knit, VerInfo # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Knit() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Knit() idx = k.add(TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Knit() k.add(TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Knit() idx = k.add(TEXT_0) self.assertEqual(idx, 0) idx = k.add(TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class Delta1(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): from pprint import pformat k = Knit() k.add(['line 1']) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # should be one inserted line after line 0 self.assertEquals(changes, [(1, 1, ['new line'])]) class MatchedLine(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): return ########################## SKIPPED k = Knit() k.add(['line 1']) k.add(['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a knit with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Knit() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [(0, "first line"), (1, "second line")] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Knit with two diverged texts based on version 0. """ def runTest(self): k = Knit() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [(0, "first line"), (1, "second line"), (2, "alternative second line"),] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) def testknit(): import testsweet from unittest import TestSuite, TestLoader import testknit tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testknit)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testknit()) commit refs/heads/tmp mark :875 committer Martin Pool 1119858280 +1000 data 7 tidy up from :874 M 644 inline knit.py data 7084 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # GNU GPL v2 # Author: Martin Pool """knit - a weave-like structure""" class VerInfo(object): included = frozenset() def __init__(self, included=None): if included: self.included = set(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class Knit(object): """knit - versioned text file storage. A Knit manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this knit and the version-id is used to reference it in the larger world. _l List of edit instructions. Each line is stored as a tuple of (index-id, text). The line is present in the version equal to index-id. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, text): """Add a single text on top of the weave. Returns the index number of the newly added version.""" if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) idx = len(self._v) # all of the previous texts are turned off; just append lines at the bottom for line in text: self._l.append((idx, line)) vi = VerInfo() self._v.append(vi) return idx def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" vi = self._v[index] included = set(vi.included) included.add(index) return iter(self._extract(included)) def _extract(self, included): """Yield annotation of lines in included set. The set typically but not necessarily corresponds to a version. """ for origin, line in self._l: if origin in included: yield origin, line def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Knit._l = ", pprint(self._l, to_file) print >>to_file, "Knit._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise ValueError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise ValueError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (line1, line2, newlines), indicating that line1 through line2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) lineno = 0 for origin, line in self._l: if origin in included: basis.append((lineno, line)) lineno += 1 assert lineno == len(self._l) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][0] real_i2 = basis[i2][0] # find the text identified by j: if j1 == j2: newlines = [] else: assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) newlines = lines[j1:j2] yield real_i1, real_i2, newlines def update_knit(knit, new_vers, new_lines): """Return a new knit whose text matches new_lines. First of all the knit is diffed against the new lines, considering only the text of the lines from the knit. This identifies lines unchanged from the knit, plus insertions and deletions. The deletions are marked as deleted. The insertions are added with their new values. """ if not isinstance(new_vers, int): raise TypeError('new version-id must be an int: %r' % new_vers) from difflib import SequenceMatcher knit_lines = knit2text(knit) m = SequenceMatcher(None, knit_lines, new_lines) for block in m.get_matching_blocks(): print "a[%d] and b[%d] match for %d elements" % block new_knit = [] for tag, i1, i2, j1, j2 in m.get_opcodes(): print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" % (tag, i1, i2, knit_lines[i1:i2], j1, j2, new_lines[j1:j2])) if tag == 'equal': new_knit.extend(knit[i1:i2]) elif tag == 'delete': for i in range(i1, i2): kl = knit[i] new_knit.append((kl[0], kl[1], False)) return new_knit commit refs/heads/tmp mark :876 committer Martin Pool 1119858459 +1000 data 45 Add another change for delta of new version. from :875 M 644 inline testknit.py data 4762 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test case for knit/weave algorithm""" from testsweet import TestBase from knit import Knit, VerInfo # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Knit() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Knit() idx = k.add(TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Knit() k.add(TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Knit() idx = k.add(TEXT_0) self.assertEqual(idx, 0) idx = k.add(TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class Delta1(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): from pprint import pformat k = Knit() k.add(['line 1']) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # should be one inserted line after line 0q self.assertEquals(changes, [(1, 1, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(0, 0, ['top line'])]) class MatchedLine(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): return ########################## SKIPPED k = Knit() k.add(['line 1']) k.add(['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a knit with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Knit() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [(0, "first line"), (1, "second line")] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Knit with two diverged texts based on version 0. """ def runTest(self): k = Knit() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [(0, "first line"), (1, "second line"), (2, "alternative second line"),] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) def testknit(): import testsweet from unittest import TestSuite, TestLoader import testknit tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testknit)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testknit()) commit refs/heads/tmp mark :877 committer Martin Pool 1119859361 +1000 data 151 Handle insertion of new weave layers that insert text on top of the basis versions. Knit.add takes a list of basis versions. Turn on test for this. from :876 M 644 inline knit.py data 7939 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # GNU GPL v2 # Author: Martin Pool """knit - a weave-like structure""" class VerInfo(object): included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class Knit(object): """knit - versioned text file storage. A Knit manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this knit and the version-id is used to reference it in the larger world. _l List of edit instructions. Each line is stored as a tuple of (index-id, text). The line is present in the version equal to index-id. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, text, basis=None): """Add a single text on top of the weave. Returns the index number of the newly added version.""" if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) idx = len(self._v) if basis: basis = frozenset(basis) delta = self._delta(basis, text) for i1, i2, newlines in delta: # TODO: handle lines being offset as we insert stuff if i1 != i2: raise NotImplementedError("can't handle replacing weave lines %d-%d yet" % (i1, i2)) # a pure insertion to_insert = [] for line in newlines: to_insert.append((idx, line)) self._l[i1:i1] = to_insert self._v.append(VerInfo(basis)) else: # special case; adding with no basis revision; can do this # more quickly by just appending unconditionally for line in text: self._l.append((idx, line)) self._v.append(VerInfo()) return idx def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) return iter(self._extract(included)) def _extract(self, included): """Yield annotation of lines in included set. The set typically but not necessarily corresponds to a version. """ for origin, line in self._l: if origin in included: yield origin, line def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Knit._l = ", pprint(self._l, to_file) print >>to_file, "Knit._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise ValueError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise ValueError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (line1, line2, newlines), indicating that line1 through line2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) lineno = 0 for origin, line in self._l: if origin in included: basis.append((lineno, line)) lineno += 1 assert lineno == len(self._l) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][0] real_i2 = basis[i2][0] # find the text identified by j: if j1 == j2: newlines = [] else: assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) newlines = lines[j1:j2] yield real_i1, real_i2, newlines def update_knit(knit, new_vers, new_lines): """Return a new knit whose text matches new_lines. First of all the knit is diffed against the new lines, considering only the text of the lines from the knit. This identifies lines unchanged from the knit, plus insertions and deletions. The deletions are marked as deleted. The insertions are added with their new values. """ if not isinstance(new_vers, int): raise TypeError('new version-id must be an int: %r' % new_vers) from difflib import SequenceMatcher knit_lines = knit2text(knit) m = SequenceMatcher(None, knit_lines, new_lines) for block in m.get_matching_blocks(): print "a[%d] and b[%d] match for %d elements" % block new_knit = [] for tag, i1, i2, j1, j2 in m.get_opcodes(): print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" % (tag, i1, i2, knit_lines[i1:i2], j1, j2, new_lines[j1:j2])) if tag == 'equal': new_knit.extend(knit[i1:i2]) elif tag == 'delete': for i in range(i1, i2): kl = knit[i] new_knit.append((kl[0], kl[1], False)) return new_knit M 644 inline testknit.py data 4840 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test case for knit/weave algorithm""" from testsweet import TestBase from knit import Knit, VerInfo # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Knit() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Knit() idx = k.add(TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Knit() k.add(TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Knit() idx = k.add(TEXT_0) self.assertEqual(idx, 0) idx = k.add(TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class Delta1(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): from pprint import pformat k = Knit() k.add(['line 1']) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # should be one inserted line after line 0q self.assertEquals(changes, [(1, 1, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(0, 0, ['top line'])]) class MatchedLine(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): k = Knit() k.add(['line 1']) k.add(['line 1', 'line 2'], [0]) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a knit with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Knit() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [(0, "first line"), (1, "second line")] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Knit with two diverged texts based on version 0. """ def runTest(self): k = Knit() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [(0, "first line"), (1, "second line"), (2, "alternative second line"),] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) def testknit(): import testsweet from unittest import TestSuite, TestLoader import testknit tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testknit)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testknit()) commit refs/heads/tmp mark :878 committer Martin Pool 1119859556 +1000 data 34 Refactor parameters to add command from :877 M 644 inline knit.py data 8092 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # GNU GPL v2 # Author: Martin Pool """knit - a weave-like structure""" class VerInfo(object): included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class Knit(object): """knit - versioned text file storage. A Knit manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this knit and the version-id is used to reference it in the larger world. _l List of edit instructions. Each line is stored as a tuple of (index-id, text). The line is present in the version equal to index-id. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. text Sequence of lines to be added in the new version.""" if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) for i1, i2, newlines in delta: # TODO: handle lines being offset as we insert stuff if i1 != i2: raise NotImplementedError("can't handle replacing weave lines %d-%d yet" % (i1, i2)) # a pure insertion to_insert = [] for line in newlines: to_insert.append((idx, line)) self._l[i1:i1] = to_insert self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally for line in text: self._l.append((idx, line)) self._v.append(VerInfo()) return idx def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) return iter(self._extract(included)) def _extract(self, included): """Yield annotation of lines in included set. The set typically but not necessarily corresponds to a version. """ for origin, line in self._l: if origin in included: yield origin, line def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Knit._l = ", pprint(self._l, to_file) print >>to_file, "Knit._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise ValueError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise ValueError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (line1, line2, newlines), indicating that line1 through line2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) lineno = 0 for origin, line in self._l: if origin in included: basis.append((lineno, line)) lineno += 1 assert lineno == len(self._l) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][0] real_i2 = basis[i2][0] # find the text identified by j: if j1 == j2: newlines = [] else: assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) newlines = lines[j1:j2] yield real_i1, real_i2, newlines def update_knit(knit, new_vers, new_lines): """Return a new knit whose text matches new_lines. First of all the knit is diffed against the new lines, considering only the text of the lines from the knit. This identifies lines unchanged from the knit, plus insertions and deletions. The deletions are marked as deleted. The insertions are added with their new values. """ if not isinstance(new_vers, int): raise TypeError('new version-id must be an int: %r' % new_vers) from difflib import SequenceMatcher knit_lines = knit2text(knit) m = SequenceMatcher(None, knit_lines, new_lines) for block in m.get_matching_blocks(): print "a[%d] and b[%d] match for %d elements" % block new_knit = [] for tag, i1, i2, j1, j2 in m.get_opcodes(): print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" % (tag, i1, i2, knit_lines[i1:i2], j1, j2, new_lines[j1:j2])) if tag == 'equal': new_knit.extend(knit[i1:i2]) elif tag == 'delete': for i in range(i1, i2): kl = knit[i] new_knit.append((kl[0], kl[1], False)) return new_knit M 644 inline testknit.py data 4851 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test case for knit/weave algorithm""" from testsweet import TestBase from knit import Knit, VerInfo # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Knit() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Knit() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Knit() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Knit() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class Delta1(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): from pprint import pformat k = Knit() k.add([], ['line 1']) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # should be one inserted line after line 0q self.assertEquals(changes, [(1, 1, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(0, 0, ['top line'])]) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): k = Knit() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a knit with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Knit() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [(0, "first line"), (1, "second line")] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Knit with two diverged texts based on version 0. """ def runTest(self): k = Knit() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [(0, "first line"), (1, "second line"), (2, "alternative second line"),] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) def testknit(): import testsweet from unittest import TestSuite, TestLoader import testknit tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testknit)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testknit()) commit refs/heads/tmp mark :879 committer Martin Pool 1119859816 +1000 data 51 Check that version numbers passed in are reasonable from :878 M 644 inline knit.py data 8447 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # GNU GPL v2 # Author: Martin Pool """knit - a weave-like structure""" class VerInfo(object): included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class Knit(object): """knit - versioned text file storage. A Knit manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this knit and the version-id is used to reference it in the larger world. _l List of edit instructions. Each line is stored as a tuple of (index-id, text). The line is present in the version equal to index-id. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. text Sequence of lines to be added in the new version.""" if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) self._check_versions(parents) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) for i1, i2, newlines in delta: # TODO: handle lines being offset as we insert stuff if i1 != i2: raise NotImplementedError("can't handle replacing weave lines %d-%d yet" % (i1, i2)) # a pure insertion to_insert = [] for line in newlines: to_insert.append((idx, line)) self._l[i1:i1] = to_insert self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally for line in text: self._l.append((idx, line)) self._v.append(VerInfo()) return idx def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) return iter(self._extract(included)) def _extract(self, included): """Yield annotation of lines in included set. The set typically but not necessarily corresponds to a version. """ for origin, line in self._l: if origin in included: yield origin, line def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Knit._l = ", pprint(self._l, to_file) print >>to_file, "Knit._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise ValueError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise ValueError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (line1, line2, newlines), indicating that line1 through line2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) lineno = 0 for origin, line in self._l: if origin in included: basis.append((lineno, line)) lineno += 1 assert lineno == len(self._l) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][0] real_i2 = basis[i2][0] # find the text identified by j: if j1 == j2: newlines = [] else: assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) newlines = lines[j1:j2] yield real_i1, real_i2, newlines def update_knit(knit, new_vers, new_lines): """Return a new knit whose text matches new_lines. First of all the knit is diffed against the new lines, considering only the text of the lines from the knit. This identifies lines unchanged from the knit, plus insertions and deletions. The deletions are marked as deleted. The insertions are added with their new values. """ if not isinstance(new_vers, int): raise TypeError('new version-id must be an int: %r' % new_vers) from difflib import SequenceMatcher knit_lines = knit2text(knit) m = SequenceMatcher(None, knit_lines, new_lines) for block in m.get_matching_blocks(): print "a[%d] and b[%d] match for %d elements" % block new_knit = [] for tag, i1, i2, j1, j2 in m.get_opcodes(): print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" % (tag, i1, i2, knit_lines[i1:i2], j1, j2, new_lines[j1:j2])) if tag == 'equal': new_knit.extend(knit[i1:i2]) elif tag == 'delete': for i in range(i1, i2): kl = knit[i] new_knit.append((kl[0], kl[1], False)) return new_knit M 644 inline testknit.py data 5124 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test case for knit/weave algorithm""" from testsweet import TestBase from knit import Knit, VerInfo # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Knit() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Knit() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Knit() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Knit() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class Delta1(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): from pprint import pformat k = Knit() k.add([], ['line 1']) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # should be one inserted line after line 0q self.assertEquals(changes, [(1, 1, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(0, 0, ['top line'])]) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Knit() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): k = Knit() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a knit with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Knit() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [(0, "first line"), (1, "second line")] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Knit with two diverged texts based on version 0. """ def runTest(self): k = Knit() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [(0, "first line"), (1, "second line"), (2, "alternative second line"),] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) def testknit(): import testsweet from unittest import TestSuite, TestLoader import testknit tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testknit)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testknit()) commit refs/heads/tmp mark :880 committer Martin Pool 1119860287 +1000 data 51 More tests for insertion of lines in new versions. from :879 M 644 inline testknit.py data 5548 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test case for knit/weave algorithm""" from testsweet import TestBase from knit import Knit, VerInfo # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Knit() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Knit() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Knit() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Knit() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class Delta1(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): from pprint import pformat k = Knit() k.add([], ['line 1']) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # should be one inserted line after line 0q self.assertEquals(changes, [(1, 1, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(0, 0, ['top line'])]) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Knit() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): k = Knit() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) k.add([0, 1], ['line 1', 'middle line', 'line 2']) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a knit with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Knit() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [(0, "first line"), (1, "second line")] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Knit with two diverged texts based on version 0. """ def runTest(self): k = Knit() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [(0, "first line"), (1, "second line"), (2, "alternative second line"),] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) def testknit(): import testsweet from unittest import TestSuite, TestLoader import testknit tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testknit)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testknit()) commit refs/heads/tmp mark :881 committer Martin Pool 1119860574 +1000 data 21 Better internal error from :880 M 644 inline knit.py data 8565 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # GNU GPL v2 # Author: Martin Pool """knit - a weave-like structure""" class VerInfo(object): included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class Knit(object): """knit - versioned text file storage. A Knit manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this knit and the version-id is used to reference it in the larger world. _l List of edit instructions. Each line is stored as a tuple of (index-id, text). The line is present in the version equal to index-id. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. text Sequence of lines to be added in the new version.""" if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) self._check_versions(parents) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) if i1 != i2: raise NotImplementedError("can't handle replacing weave [%d:%d] yet" % (i1, i2)) # TODO: handle lines being offset as we insert stuff # a pure insertion to_insert = [] for line in newlines: to_insert.append((idx, line)) self._l[i1:i1] = to_insert self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally for line in text: self._l.append((idx, line)) self._v.append(VerInfo()) return idx def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) return iter(self._extract(included)) def _extract(self, included): """Yield annotation of lines in included set. The set typically but not necessarily corresponds to a version. """ for origin, line in self._l: if origin in included: yield origin, line def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Knit._l = ", pprint(self._l, to_file) print >>to_file, "Knit._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise ValueError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise ValueError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (line1, line2, newlines), indicating that line1 through line2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) lineno = 0 for origin, line in self._l: if origin in included: basis.append((lineno, line)) lineno += 1 assert lineno == len(self._l) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][0] real_i2 = basis[i2][0] # find the text identified by j: if j1 == j2: newlines = [] else: assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) newlines = lines[j1:j2] yield real_i1, real_i2, newlines def update_knit(knit, new_vers, new_lines): """Return a new knit whose text matches new_lines. First of all the knit is diffed against the new lines, considering only the text of the lines from the knit. This identifies lines unchanged from the knit, plus insertions and deletions. The deletions are marked as deleted. The insertions are added with their new values. """ if not isinstance(new_vers, int): raise TypeError('new version-id must be an int: %r' % new_vers) from difflib import SequenceMatcher knit_lines = knit2text(knit) m = SequenceMatcher(None, knit_lines, new_lines) for block in m.get_matching_blocks(): print "a[%d] and b[%d] match for %d elements" % block new_knit = [] for tag, i1, i2, j1, j2 in m.get_opcodes(): print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" % (tag, i1, i2, knit_lines[i1:i2], j1, j2, new_lines[j1:j2])) if tag == 'equal': new_knit.extend(knit[i1:i2]) elif tag == 'delete': for i in range(i1, i2): kl = knit[i] new_knit.append((kl[0], kl[1], False)) return new_knit commit refs/heads/tmp mark :882 committer Martin Pool 1119860589 +1000 data 36 Start adding tests for line deletion from :881 M 644 inline testknit.py data 6183 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test case for knit/weave algorithm""" from testsweet import TestBase from knit import Knit, VerInfo # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Knit() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Knit() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Knit() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Knit() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class Delta1(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): from pprint import pformat k = Knit() k.add([], ['line 1']) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # should be one inserted line after line 0q self.assertEquals(changes, [(1, 1, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(0, 0, ['top line'])]) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Knit() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): k = Knit() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) k.add([0, 1], ['line 1', 'middle line', 'line 2']) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) class DeleteLines(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Knit() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) return ################################## SKIPPED k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a knit with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Knit() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [(0, "first line"), (1, "second line")] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Knit with two diverged texts based on version 0. """ def runTest(self): k = Knit() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [(0, "first line"), (1, "second line"), (2, "alternative second line"),] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) def testknit(): import testsweet from unittest import TestSuite, TestLoader import testknit tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testknit)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testknit()) commit refs/heads/tmp mark :883 committer Martin Pool 1119860835 +1000 data 99 Fix insertion of multiple regions, calculating the right line offset as we go. Add test for this. from :882 M 644 inline knit.py data 8877 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # GNU GPL v2 # Author: Martin Pool """knit - a weave-like structure""" class VerInfo(object): included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class Knit(object): """knit - versioned text file storage. A Knit manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this knit and the version-id is used to reference it in the larger world. _l List of edit instructions. Each line is stored as a tuple of (index-id, text). The line is present in the version equal to index-id. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. text Sequence of lines to be added in the new version.""" if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) self._check_versions(parents) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) if i1 != i2: raise NotImplementedError("can't handle replacing weave [%d:%d] yet" % (i1, i2)) # TODO: handle lines being offset as we insert stuff # a pure insertion to_insert = [] for line in newlines: to_insert.append((idx, line)) self._l[(i1+offset):(i1+offset)] = to_insert offset += len(newlines) self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally for line in text: self._l.append((idx, line)) self._v.append(VerInfo()) return idx def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) return iter(self._extract(included)) def _extract(self, included): """Yield annotation of lines in included set. The set typically but not necessarily corresponds to a version. """ for origin, line in self._l: if origin in included: yield origin, line def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Knit._l = ", pprint(self._l, to_file) print >>to_file, "Knit._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise ValueError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise ValueError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (line1, line2, newlines), indicating that line1 through line2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) lineno = 0 for origin, line in self._l: if origin in included: basis.append((lineno, line)) lineno += 1 assert lineno == len(self._l) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][0] real_i2 = basis[i2][0] # find the text identified by j: if j1 == j2: newlines = [] else: assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) newlines = lines[j1:j2] yield real_i1, real_i2, newlines def update_knit(knit, new_vers, new_lines): """Return a new knit whose text matches new_lines. First of all the knit is diffed against the new lines, considering only the text of the lines from the knit. This identifies lines unchanged from the knit, plus insertions and deletions. The deletions are marked as deleted. The insertions are added with their new values. """ if not isinstance(new_vers, int): raise TypeError('new version-id must be an int: %r' % new_vers) from difflib import SequenceMatcher knit_lines = knit2text(knit) m = SequenceMatcher(None, knit_lines, new_lines) for block in m.get_matching_blocks(): print "a[%d] and b[%d] match for %d elements" % block new_knit = [] for tag, i1, i2, j1, j2 in m.get_opcodes(): print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" % (tag, i1, i2, knit_lines[i1:i2], j1, j2, new_lines[j1:j2])) if tag == 'equal': new_knit.extend(knit[i1:i2]) elif tag == 'delete': for i in range(i1, i2): kl = knit[i] new_knit.append((kl[0], kl[1], False)) return new_knit M 644 inline testknit.py data 6619 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test case for knit/weave algorithm""" from testsweet import TestBase from knit import Knit, VerInfo # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Knit() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Knit() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Knit() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Knit() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class Delta1(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): from pprint import pformat k = Knit() k.add([], ['line 1']) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # should be one inserted line after line 0q self.assertEquals(changes, [(1, 1, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(0, 0, ['top line'])]) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Knit() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): k = Knit() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) k.add([0, 1], ['line 1', 'middle line', 'line 2']) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) # now multiple insertions at different places k.add([0, 1, 3], ['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc']) self.assertEqual(k.annotate(4), [(0, 'line 1'), (4, 'aaa'), (3, 'middle line'), (4, 'bbb'), (1, 'line 2'), (4, 'ccc')]) class DeleteLines(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Knit() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) return ################################## SKIPPED k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a knit with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Knit() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [(0, "first line"), (1, "second line")] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Knit with two diverged texts based on version 0. """ def runTest(self): k = Knit() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [(0, "first line"), (1, "second line"), (2, "alternative second line"),] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) def testknit(): import testsweet from unittest import TestSuite, TestLoader import testknit tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testknit)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testknit()) commit refs/heads/tmp mark :884 committer Martin Pool 1119861207 +1000 data 34 Clean up code to insert into weave from :883 M 644 inline knit.py data 8682 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # GNU GPL v2 # Author: Martin Pool """knit - a weave-like structure""" class VerInfo(object): included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class Knit(object): """knit - versioned text file storage. A Knit manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this knit and the version-id is used to reference it in the larger world. _l List of edit instructions. Each line is stored as a tuple of (index-id, text). The line is present in the version equal to index-id. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. text Sequence of lines to be added in the new version.""" if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) self._check_versions(parents) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) if i1 != i2: raise NotImplementedError("can't handle replacing weave [%d:%d] yet" % (i1, i2)) for line in newlines: self._l.insert(i1 + offset, (idx, line)) offset += 1 self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally for line in text: self._l.append((idx, line)) self._v.append(VerInfo()) return idx def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) return iter(self._extract(included)) def _extract(self, included): """Yield annotation of lines in included set. The set typically but not necessarily corresponds to a version. """ for origin, line in self._l: if origin in included: yield origin, line def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Knit._l = ", pprint(self._l, to_file) print >>to_file, "Knit._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise ValueError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise ValueError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (line1, line2, newlines), indicating that line1 through line2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) lineno = 0 for origin, line in self._l: if origin in included: basis.append((lineno, line)) lineno += 1 assert lineno == len(self._l) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][0] real_i2 = basis[i2][0] # find the text identified by j: if j1 == j2: newlines = [] else: assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) newlines = lines[j1:j2] yield real_i1, real_i2, newlines def update_knit(knit, new_vers, new_lines): """Return a new knit whose text matches new_lines. First of all the knit is diffed against the new lines, considering only the text of the lines from the knit. This identifies lines unchanged from the knit, plus insertions and deletions. The deletions are marked as deleted. The insertions are added with their new values. """ if not isinstance(new_vers, int): raise TypeError('new version-id must be an int: %r' % new_vers) from difflib import SequenceMatcher knit_lines = knit2text(knit) m = SequenceMatcher(None, knit_lines, new_lines) for block in m.get_matching_blocks(): print "a[%d] and b[%d] match for %d elements" % block new_knit = [] for tag, i1, i2, j1, j2 in m.get_opcodes(): print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" % (tag, i1, i2, knit_lines[i1:i2], j1, j2, new_lines[j1:j2])) if tag == 'equal': new_knit.extend(knit[i1:i2]) elif tag == 'delete': for i in range(i1, i2): kl = knit[i] new_knit.append((kl[0], kl[1], False)) return new_knit commit refs/heads/tmp mark :885 committer Martin Pool 1119861231 +1000 data 12 add gpl text from :884 M 644 inline knit.py data 9367 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """knit - a weave-like structure""" class VerInfo(object): included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class Knit(object): """knit - versioned text file storage. A Knit manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this knit and the version-id is used to reference it in the larger world. _l List of edit instructions. Each line is stored as a tuple of (index-id, text). The line is present in the version equal to index-id. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. text Sequence of lines to be added in the new version.""" if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) self._check_versions(parents) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) if i1 != i2: raise NotImplementedError("can't handle replacing weave [%d:%d] yet" % (i1, i2)) for line in newlines: self._l.insert(i1 + offset, (idx, line)) offset += 1 self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally for line in text: self._l.append((idx, line)) self._v.append(VerInfo()) return idx def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) return iter(self._extract(included)) def _extract(self, included): """Yield annotation of lines in included set. The set typically but not necessarily corresponds to a version. """ for origin, line in self._l: if origin in included: yield origin, line def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Knit._l = ", pprint(self._l, to_file) print >>to_file, "Knit._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise ValueError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise ValueError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (line1, line2, newlines), indicating that line1 through line2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) lineno = 0 for origin, line in self._l: if origin in included: basis.append((lineno, line)) lineno += 1 assert lineno == len(self._l) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][0] real_i2 = basis[i2][0] # find the text identified by j: if j1 == j2: newlines = [] else: assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) newlines = lines[j1:j2] yield real_i1, real_i2, newlines def update_knit(knit, new_vers, new_lines): """Return a new knit whose text matches new_lines. First of all the knit is diffed against the new lines, considering only the text of the lines from the knit. This identifies lines unchanged from the knit, plus insertions and deletions. The deletions are marked as deleted. The insertions are added with their new values. """ if not isinstance(new_vers, int): raise TypeError('new version-id must be an int: %r' % new_vers) from difflib import SequenceMatcher knit_lines = knit2text(knit) m = SequenceMatcher(None, knit_lines, new_lines) for block in m.get_matching_blocks(): print "a[%d] and b[%d] match for %d elements" % block new_knit = [] for tag, i1, i2, j1, j2 in m.get_opcodes(): print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" % (tag, i1, i2, knit_lines[i1:i2], j1, j2, new_lines[j1:j2])) if tag == 'equal': new_knit.extend(knit[i1:i2]) elif tag == 'delete': for i in range(i1, i2): kl = knit[i] new_knit.append((kl[0], kl[1], False)) return new_knit commit refs/heads/tmp mark :886 committer Martin Pool 1119861276 +1000 data 16 remove dead code from :885 M 644 inline knit.py data 8166 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """knit - a weave-like structure""" class VerInfo(object): included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class Knit(object): """knit - versioned text file storage. A Knit manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this knit and the version-id is used to reference it in the larger world. _l List of edit instructions. Each line is stored as a tuple of (index-id, text). The line is present in the version equal to index-id. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. text Sequence of lines to be added in the new version.""" if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) self._check_versions(parents) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) if i1 != i2: raise NotImplementedError("can't handle replacing weave [%d:%d] yet" % (i1, i2)) for line in newlines: self._l.insert(i1 + offset, (idx, line)) offset += 1 self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally for line in text: self._l.append((idx, line)) self._v.append(VerInfo()) return idx def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) return iter(self._extract(included)) def _extract(self, included): """Yield annotation of lines in included set. The set typically but not necessarily corresponds to a version. """ for origin, line in self._l: if origin in included: yield origin, line def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Knit._l = ", pprint(self._l, to_file) print >>to_file, "Knit._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise ValueError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise ValueError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (line1, line2, newlines), indicating that line1 through line2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) lineno = 0 for origin, line in self._l: if origin in included: basis.append((lineno, line)) lineno += 1 assert lineno == len(self._l) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][0] real_i2 = basis[i2][0] # find the text identified by j: if j1 == j2: newlines = [] else: assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) newlines = lines[j1:j2] yield real_i1, real_i2, newlines commit refs/heads/tmp mark :887 committer Martin Pool 1119861383 +1000 data 28 Clean up Knit._delta method from :886 M 644 inline knit.py data 8050 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """knit - a weave-like structure""" # TODO: Perhaps have copy and comparison methods? class VerInfo(object): included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class Knit(object): """knit - versioned text file storage. A Knit manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this knit and the version-id is used to reference it in the larger world. _l List of edit instructions. Each line is stored as a tuple of (index-id, text). The line is present in the version equal to index-id. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. text Sequence of lines to be added in the new version.""" if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) self._check_versions(parents) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) if i1 != i2: raise NotImplementedError("can't handle replacing weave [%d:%d] yet" % (i1, i2)) for line in newlines: self._l.insert(i1 + offset, (idx, line)) offset += 1 self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally for line in text: self._l.append((idx, line)) self._v.append(VerInfo()) return idx def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) return iter(self._extract(included)) def _extract(self, included): """Yield annotation of lines in included set. The set typically but not necessarily corresponds to a version. """ for origin, line in self._l: if origin in included: yield origin, line def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Knit._l = ", pprint(self._l, to_file) print >>to_file, "Knit._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise ValueError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise ValueError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (line1, line2, newlines), indicating that line1 through line2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) lineno = 0 for origin, line in self._l: if origin in included: basis.append((lineno, line)) lineno += 1 assert lineno == len(self._l) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][0] real_i2 = basis[i2][0] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] commit refs/heads/tmp mark :888 committer Martin Pool 1119938426 +1000 data 3 doc from :887 M 644 inline knit.py data 8101 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """knit - a weave-like structure""" # TODO: Perhaps have copy and comparison methods? class VerInfo(object): """Information about a version in a Knit.""" included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class Knit(object): """knit - versioned text file storage. A Knit manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this knit and the version-id is used to reference it in the larger world. _l List of edit instructions. Each line is stored as a tuple of (index-id, text). The line is present in the version equal to index-id. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. text Sequence of lines to be added in the new version.""" if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) self._check_versions(parents) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) if i1 != i2: raise NotImplementedError("can't handle replacing weave [%d:%d] yet" % (i1, i2)) for line in newlines: self._l.insert(i1 + offset, (idx, line)) offset += 1 self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally for line in text: self._l.append((idx, line)) self._v.append(VerInfo()) return idx def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) return iter(self._extract(included)) def _extract(self, included): """Yield annotation of lines in included set. The set typically but not necessarily corresponds to a version. """ for origin, line in self._l: if origin in included: yield origin, line def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Knit._l = ", pprint(self._l, to_file) print >>to_file, "Knit._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise ValueError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise ValueError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (line1, line2, newlines), indicating that line1 through line2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) lineno = 0 for origin, line in self._l: if origin in included: basis.append((lineno, line)) lineno += 1 assert lineno == len(self._l) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][0] real_i2 = basis[i2][0] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] commit refs/heads/tmp mark :889 committer Martin Pool 1119938451 +1000 data 20 remove old dead code from :888 D woolyweave.py commit refs/heads/tmp mark :890 committer Martin Pool 1119938892 +1000 data 80 Rename knit to weave. (I don't think there's an existing module called weave.) from :889 R knit.py weave.py R testknit.py testweave.py M 644 inline weave.py data 8124 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy and comparison methods? class VerInfo(object): """Information about a version in a Weave.""" included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. _l List of edit instructions. Each line is stored as a tuple of (index-id, text). The line is present in the version equal to index-id. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. text Sequence of lines to be added in the new version.""" if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) self._check_versions(parents) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) if i1 != i2: raise NotImplementedError("can't handle replacing weave [%d:%d] yet" % (i1, i2)) for line in newlines: self._l.insert(i1 + offset, (idx, line)) offset += 1 self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally for line in text: self._l.append((idx, line)) self._v.append(VerInfo()) return idx def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) return iter(self._extract(included)) def _extract(self, included): """Yield annotation of lines in included set. The set typically but not necessarily corresponds to a version. """ for origin, line in self._l: if origin in included: yield origin, line def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise ValueError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise ValueError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (line1, line2, newlines), indicating that line1 through line2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) lineno = 0 for origin, line in self._l: if origin in included: basis.append((lineno, line)) lineno += 1 assert lineno == len(self._l) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][0] real_i2 = basis[i2][0] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] M 644 inline testweave.py data 6633 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test suite for weave algorithm""" from testsweet import TestBase from weave import Weave, VerInfo # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Weave() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Weave() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class Delta1(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): from pprint import pformat k = Weave() k.add([], ['line 1']) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # should be one inserted line after line 0q self.assertEquals(changes, [(1, 1, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(0, 0, ['top line'])]) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Weave() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): k = Weave() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) k.add([0, 1], ['line 1', 'middle line', 'line 2']) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) # now multiple insertions at different places k.add([0, 1, 3], ['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc']) self.assertEqual(k.annotate(4), [(0, 'line 1'), (4, 'aaa'), (3, 'middle line'), (4, 'bbb'), (1, 'line 2'), (4, 'ccc')]) class DeleteLines(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Weave() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) return ################################## SKIPPED k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a weave with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [(0, "first line"), (1, "second line")] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Weave with two diverged texts based on version 0. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [(0, "first line"), (1, "second line"), (2, "alternative second line"),] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) def testweave(): import testsweet from unittest import TestSuite, TestLoader import testweave tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testweave)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testweave()) commit refs/heads/tmp mark :891 committer Martin Pool 1119941435 +1000 data 163 Change to a more realistic weave structure which can represent insertions and deletions. Tests which calculate deltas are currently not working and are skipped. from :890 M 644 inline testweave.py data 7010 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test suite for weave algorithm""" from testsweet import TestBase from weave import Weave, VerInfo # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Weave() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Weave() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class Delta1(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): return ########################## SKIPPED from pprint import pformat k = Weave() k.add([], ['line 1']) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # should be one inserted line after line 0q self.assertEquals(changes, [(1, 1, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(0, 0, ['top line'])]) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Weave() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): return ########################## SKIPPED k = Weave() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) k.add([0, 1], ['line 1', 'middle line', 'line 2']) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) # now multiple insertions at different places k.add([0, 1, 3], ['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc']) self.assertEqual(k.annotate(4), [(0, 'line 1'), (4, 'aaa'), (3, 'middle line'), (4, 'bbb'), (1, 'line 2'), (4, 'ccc')]) class DeleteLines(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Weave() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) return ################################## SKIPPED k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a weave with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1)] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Weave with two diverged texts based on version 0. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1), ('{', 2), "alternative second line", ('}', 2), ] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) def testweave(): import testsweet from unittest import TestSuite, TestLoader import testweave tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testweave)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testweave()) M 644 inline weave.py data 10010 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy and comparison methods of Weave instances? class VerInfo(object): """Information about a version in a Weave.""" included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. Constraints: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. text Sequence of lines to be added in the new version.""" self._check_versions(parents) self._check_lines(text) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) if i1 != i2: raise NotImplementedError("can't handle replacing weave [%d:%d] yet" % (i1, i2)) self._l.insert(i1 + offset, ('{', idx)) i = i1 + offset + 1 self._l[i:i] = newlines self._l.insert(i + 1, ('}', idx)) offset += 2 + len(newlines) self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._v.append(VerInfo()) return idx def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ stack = [] isactive = False lineno = 0 for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': stack.append(l) isactive = (v in included) elif c == '}': oldc, oldv = stack.pop() assert oldc == '{' assert oldv == v isactive == stack and (stack[-1][1] in included) else: raise ValueError("invalid processing instruction %r" % (l,)) else: assert isinstance(l, basestring) if isactive: origin = stack[-1][1] yield origin, lineno, l lineno += 1 def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise ValueError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise ValueError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (line1, line2, newlines), indicating that line1 through line2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) basis = list(self._extract(included)) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (origin, lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][0] real_i2 = basis[i2][0] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] commit refs/heads/tmp mark :892 committer Martin Pool 1119941914 +1000 data 135 Add test for extracting from weave with nested insertions Fix nested insertion processing Check that stack is emptied at end of weave from :891 M 644 inline testweave.py data 7719 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test suite for weave algorithm""" from testsweet import TestBase from weave import Weave, VerInfo # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Weave() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Weave() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class Delta1(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): return ########################## SKIPPED from pprint import pformat k = Weave() k.add([], ['line 1']) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # should be one inserted line after line 0q self.assertEquals(changes, [(1, 1, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(0, 0, ['top line'])]) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Weave() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): return ########################## SKIPPED k = Weave() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) k.add([0, 1], ['line 1', 'middle line', 'line 2']) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) # now multiple insertions at different places k.add([0, 1, 3], ['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc']) self.assertEqual(k.annotate(4), [(0, 'line 1'), (4, 'aaa'), (3, 'middle line'), (4, 'bbb'), (1, 'line 2'), (4, 'ccc')]) class InsertNested(TestBase): """Insertion with nested instructions.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('}', 1), '}', ('}', 0)] self.assertEqual(k.get(0), ['foo {', '}']) self.assertEqual(k.get(1), ['foo {', ' added in version 1', '}']) class DeleteLines(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Weave() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) return ################################## SKIPPED k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a weave with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1)] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Weave with two diverged texts based on version 0. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1), ('{', 2), "alternative second line", ('}', 2), ] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) def testweave(): import testsweet from unittest import TestSuite, TestLoader import testweave tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testweave)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testweave()) M 644 inline weave.py data 10128 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy and comparison methods of Weave instances? class VerInfo(object): """Information about a version in a Weave.""" included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. Constraints: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. text Sequence of lines to be added in the new version.""" self._check_versions(parents) self._check_lines(text) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) if i1 != i2: raise NotImplementedError("can't handle replacing weave [%d:%d] yet" % (i1, i2)) self._l.insert(i1 + offset, ('{', idx)) i = i1 + offset + 1 self._l[i:i] = newlines self._l.insert(i + 1, ('}', idx)) offset += 2 + len(newlines) self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._v.append(VerInfo()) return idx def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ stack = [] isactive = False lineno = 0 for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': stack.append(l) isactive = (v in included) elif c == '}': oldc, oldv = stack.pop() assert oldc == '{' assert oldv == v isactive = stack and (stack[-1][1] in included) else: raise ValueError("invalid processing instruction %r" % (l,)) else: assert isinstance(l, basestring) if isactive: origin = stack[-1][1] yield origin, lineno, l lineno += 1 if stack: raise ValueError("unclosed blocks at end of weave", stack) def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise ValueError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise ValueError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (line1, line2, newlines), indicating that line1 through line2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) basis = list(self._extract(included)) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (origin, lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][0] real_i2 = basis[i2][0] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] commit refs/heads/tmp mark :893 committer Martin Pool 1119942245 +1000 data 3 Doc from :892 M 644 inline weave.py data 10652 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy and comparison methods of Weave instances? class VerInfo(object): """Information about a version in a Weave.""" included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. text Sequence of lines to be added in the new version.""" self._check_versions(parents) self._check_lines(text) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) if i1 != i2: raise NotImplementedError("can't handle replacing weave [%d:%d] yet" % (i1, i2)) self._l.insert(i1 + offset, ('{', idx)) i = i1 + offset + 1 self._l[i:i] = newlines self._l.insert(i + 1, ('}', idx)) offset += 2 + len(newlines) self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._v.append(VerInfo()) return idx def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ stack = [] isactive = False lineno = 0 for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': stack.append(l) isactive = (v in included) elif c == '}': oldc, oldv = stack.pop() assert oldc == '{' assert oldv == v isactive = stack and (stack[-1][1] in included) else: raise ValueError("invalid processing instruction %r" % (l,)) else: assert isinstance(l, basestring) if isactive: origin = stack[-1][1] yield origin, lineno, l lineno += 1 if stack: raise ValueError("unclosed blocks at end of weave", stack) def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise ValueError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise ValueError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (line1, line2, newlines), indicating that line1 through line2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) basis = list(self._extract(included)) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (origin, lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][0] real_i2 = basis[i2][0] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] commit refs/heads/tmp mark :894 committer Martin Pool 1119942255 +1000 data 41 More tests for nested insert instructions from :893 M 644 inline testweave.py data 7882 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test suite for weave algorithm""" from testsweet import TestBase from weave import Weave, VerInfo # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Weave() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Weave() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class Delta1(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): return ########################## SKIPPED from pprint import pformat k = Weave() k.add([], ['line 1']) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # should be one inserted line after line 0q self.assertEquals(changes, [(1, 1, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(0, 0, ['top line'])]) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Weave() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): return ########################## SKIPPED k = Weave() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) k.add([0, 1], ['line 1', 'middle line', 'line 2']) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) # now multiple insertions at different places k.add([0, 1, 3], ['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc']) self.assertEqual(k.annotate(4), [(0, 'line 1'), (4, 'aaa'), (3, 'middle line'), (4, 'bbb'), (1, 'line 2'), (4, 'ccc')]) class InsertNested(TestBase): """Insertion with nested instructions.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertEqual(k.get(0), ['foo {', '}']) self.assertEqual(k.get(1), ['foo {', ' added in version 1', ' also from v1', '}']) class DeleteLines(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Weave() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) return ################################## SKIPPED k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a weave with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1)] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Weave with two diverged texts based on version 0. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1), ('{', 2), "alternative second line", ('}', 2), ] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) def testweave(): import testsweet from unittest import TestSuite, TestLoader import testweave tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testweave)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testweave()) commit refs/heads/tmp mark :895 committer Martin Pool 1119942449 +1000 data 35 More assertions on weave extraction from :894 M 644 inline weave.py data 10855 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy and comparison methods of Weave instances? class VerInfo(object): """Information about a version in a Weave.""" included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. text Sequence of lines to be added in the new version.""" self._check_versions(parents) self._check_lines(text) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) if i1 != i2: raise NotImplementedError("can't handle replacing weave [%d:%d] yet" % (i1, i2)) self._l.insert(i1 + offset, ('{', idx)) i = i1 + offset + 1 self._l[i:i] = newlines self._l.insert(i + 1, ('}', idx)) offset += 2 + len(newlines) self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._v.append(VerInfo()) return idx def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ stack = [] isactive = False lineno = 0 for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': stack.append(l) isactive = (v in included) elif c == '}': oldc, oldv = stack.pop() assert oldc == '{' assert oldv == v isactive = stack and (stack[-1][1] in included) else: raise ValueError("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not stack: raise ValueError("literal at top level on line %d" % lineno) if isactive: origin = stack[-1][1] yield origin, lineno, l lineno += 1 if stack: raise ValueError("unclosed blocks at end of weave", stack) def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise ValueError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise ValueError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (line1, line2, newlines), indicating that line1 through line2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) basis = list(self._extract(included)) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (origin, lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][0] real_i2 = basis[i2][0] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] commit refs/heads/tmp mark :896 committer Martin Pool 1119942538 +1000 data 41 More tests for nested insert instructions from :895 M 644 inline testweave.py data 8351 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test suite for weave algorithm""" from testsweet import TestBase from weave import Weave, VerInfo # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Weave() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Weave() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class Delta1(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): return ########################## SKIPPED from pprint import pformat k = Weave() k.add([], ['line 1']) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # should be one inserted line after line 0q self.assertEquals(changes, [(1, 1, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(0, 0, ['top line'])]) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Weave() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): return ########################## SKIPPED k = Weave() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) k.add([0, 1], ['line 1', 'middle line', 'line 2']) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) # now multiple insertions at different places k.add([0, 1, 3], ['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc']) self.assertEqual(k.annotate(4), [(0, 'line 1'), (4, 'aaa'), (3, 'middle line'), (4, 'bbb'), (1, 'line 2'), (4, 'ccc')]) class InsertNested(TestBase): """Insertion with nested instructions.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertEqual(k.get(0), ['foo {', '}']) self.assertEqual(k.get(1), ['foo {', ' added in version 1', ' also from v1', '}']) self.assertEqual(k.get(2), ['foo {', ' added in v2', '}']) self.assertEqual(k.get(3), ['foo {', ' added in version 1', ' added in v2', ' also from v1', '}']) class DeleteLines(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Weave() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) return ################################## SKIPPED k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a weave with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1)] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Weave with two diverged texts based on version 0. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1), ('{', 2), "alternative second line", ('}', 2), ] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) def testweave(): import testsweet from unittest import TestSuite, TestLoader import testweave tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testweave)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testweave()) commit refs/heads/tmp mark :897 committer Martin Pool 1119949068 +1000 data 3 doc from :896 M 644 inline testweave.py data 8319 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test suite for weave algorithm""" from testsweet import TestBase from weave import Weave, VerInfo # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Weave() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Weave() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class Delta1(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): return ########################## SKIPPED from pprint import pformat k = Weave() k.add([], ['line 1']) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # should be one inserted line after line 0q self.assertEquals(changes, [(1, 1, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(0, 0, ['top line'])]) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Weave() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): return ########################## SKIPPED k = Weave() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) k.add([0, 1], ['line 1', 'middle line', 'line 2']) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) # now multiple insertions at different places k.add([0, 1, 3], ['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc']) self.assertEqual(k.annotate(4), [(0, 'line 1'), (4, 'aaa'), (3, 'middle line'), (4, 'bbb'), (1, 'line 2'), (4, 'ccc')]) class InsertNested(TestBase): """Insertion with nested instructions.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertEqual(k.get(0), ['foo {', '}']) self.assertEqual(k.get(1), ['foo {', ' added in version 1', ' also from v1', '}']) self.assertEqual(k.get(2), ['foo {', ' added in v2', '}']) self.assertEqual(k.get(3), ['foo {', ' added in version 1', ' added in v2', ' also from v1', '}']) class DeleteLines(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Weave() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) return ################################## SKIPPED k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a weave with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1)] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Weave with two diverged texts based on version 0. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1), ('{', 2), "alternative second line", ('}', 2), ] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) def testweave(): import testsweet from unittest import TestSuite, TestLoader import testweave tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testweave)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testweave()) M 644 inline weave.py data 11162 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy and comparison methods of Weave instances? class VerInfo(object): """Information about a version in a Weave.""" included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. text Sequence of lines to be added in the new version.""" self._check_versions(parents) self._check_lines(text) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) if i1 != i2: raise NotImplementedError("can't handle replacing weave [%d:%d] yet" % (i1, i2)) self._l.insert(i1 + offset, ('{', idx)) i = i1 + offset + 1 self._l[i:i] = newlines self._l.insert(i + 1, ('}', idx)) offset += 2 + len(newlines) self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._v.append(VerInfo()) return idx def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ stack = [] isactive = False lineno = 0 for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': stack.append(l) isactive = (v in included) elif c == '}': oldc, oldv = stack.pop() assert oldc == '{' assert oldv == v isactive = stack and (stack[-1][1] in included) else: raise ValueError("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not stack: raise ValueError("literal at top level on line %d" % lineno) if isactive: origin = stack[-1][1] yield origin, lineno, l lineno += 1 if stack: raise ValueError("unclosed blocks at end of weave", stack) def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise ValueError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise ValueError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (line1, line2, newlines), indicating that line1 through line2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) basis = list(self._extract(included)) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (origin, lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][0] real_i2 = basis[i2][0] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] commit refs/heads/tmp mark :898 committer Martin Pool 1119949426 +1000 data 65 More constraints on structure of weave, and checks that they work from :897 M 644 inline testweave.py data 9675 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test suite for weave algorithm""" from testsweet import TestBase from weave import Weave, VerInfo # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Weave() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Weave() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class Delta1(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): return ########################## SKIPPED from pprint import pformat k = Weave() k.add([], ['line 1']) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # should be one inserted line after line 0q self.assertEquals(changes, [(1, 1, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(0, 0, ['top line'])]) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Weave() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): return ########################## SKIPPED k = Weave() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) k.add([0, 1], ['line 1', 'middle line', 'line 2']) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) # now multiple insertions at different places k.add([0, 1, 3], ['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc']) self.assertEqual(k.annotate(4), [(0, 'line 1'), (4, 'aaa'), (3, 'middle line'), (4, 'bbb'), (1, 'line 2'), (4, 'ccc')]) class BadWeave(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = ['bad line', ('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertRaises(ValueError, k.get, 0) class BadInsert(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 1), ' more in 1', ('}', 1), ('}', 1), ('}', 0)] self.assertRaises(AssertionError, k.get, 0) self.assertRaises(AssertionError, k.get, 1) class InsertNested(TestBase): """Insertion with nested instructions.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertEqual(k.get(0), ['foo {', '}']) self.assertEqual(k.get(1), ['foo {', ' added in version 1', ' also from v1', '}']) self.assertEqual(k.get(2), ['foo {', ' added in v2', '}']) self.assertEqual(k.get(3), ['foo {', ' added in version 1', ' added in v2', ' also from v1', '}']) class DeleteLines(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Weave() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) return ################################## SKIPPED k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a weave with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1)] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Weave with two diverged texts based on version 0. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1), ('{', 2), "alternative second line", ('}', 2), ] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) def testweave(): import testsweet from unittest import TestSuite, TestLoader import testweave tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testweave)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testweave()) M 644 inline weave.py data 11408 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy and comparison methods of Weave instances? class VerInfo(object): """Information about a version in a Weave.""" included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. text Sequence of lines to be added in the new version.""" self._check_versions(parents) self._check_lines(text) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) if i1 != i2: raise NotImplementedError("can't handle replacing weave [%d:%d] yet" % (i1, i2)) self._l.insert(i1 + offset, ('{', idx)) i = i1 + offset + 1 self._l[i:i] = newlines self._l.insert(i + 1, ('}', idx)) offset += 2 + len(newlines) self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._v.append(VerInfo()) return idx def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] isactive = False lineno = 0 for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': if istack: assert istack[-1][1] < v, \ ("improperly nested insertions %d>=%d on line %d" % (istack[-1][1], v, lineno)) istack.append(l) isactive = (v in included) elif c == '}': oldc, oldv = istack.pop() assert oldc == '{' assert oldv == v isactive = istack and (istack[-1][1] in included) else: raise ValueError("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise ValueError("literal at top level on line %d" % lineno) if isactive: origin = istack[-1][1] yield origin, lineno, l lineno += 1 if istack: raise ValueError("unclosed insertion blocks at end of weave", istack) def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise ValueError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise ValueError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (line1, line2, newlines), indicating that line1 through line2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) basis = list(self._extract(included)) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (origin, lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][0] real_i2 = basis[i2][0] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] commit refs/heads/tmp mark :899 committer Martin Pool 1119949692 +1000 data 60 New WeaveError and WeaveFormatError rather than assertions. from :898 M 644 inline testweave.py data 9703 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test suite for weave algorithm""" from testsweet import TestBase from weave import Weave, VerInfo, WeaveFormatError # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Weave() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Weave() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class Delta1(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): return ########################## SKIPPED from pprint import pformat k = Weave() k.add([], ['line 1']) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # should be one inserted line after line 0q self.assertEquals(changes, [(1, 1, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(0, 0, ['top line'])]) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Weave() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): return ########################## SKIPPED k = Weave() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) k.add([0, 1], ['line 1', 'middle line', 'line 2']) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) # now multiple insertions at different places k.add([0, 1, 3], ['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc']) self.assertEqual(k.annotate(4), [(0, 'line 1'), (4, 'aaa'), (3, 'middle line'), (4, 'bbb'), (1, 'line 2'), (4, 'ccc')]) class BadWeave(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = ['bad line', ('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) class BadInsert(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 1), ' more in 1', ('}', 1), ('}', 1), ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) self.assertRaises(WeaveFormatError, k.get, 1) class InsertNested(TestBase): """Insertion with nested instructions.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertEqual(k.get(0), ['foo {', '}']) self.assertEqual(k.get(1), ['foo {', ' added in version 1', ' also from v1', '}']) self.assertEqual(k.get(2), ['foo {', ' added in v2', '}']) self.assertEqual(k.get(3), ['foo {', ' added in version 1', ' added in v2', ' also from v1', '}']) class DeleteLines(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Weave() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) return ################################## SKIPPED k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a weave with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1)] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Weave with two diverged texts based on version 0. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1), ('{', 2), "alternative second line", ('}', 2), ] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) def testweave(): import testsweet from unittest import TestSuite, TestLoader import testweave tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testweave)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testweave()) M 644 inline weave.py data 11627 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy and comparison methods of Weave instances? class VerInfo(object): """Information about a version in a Weave.""" included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. text Sequence of lines to be added in the new version.""" self._check_versions(parents) self._check_lines(text) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) if i1 != i2: raise NotImplementedError("can't handle replacing weave [%d:%d] yet" % (i1, i2)) self._l.insert(i1 + offset, ('{', idx)) i = i1 + offset + 1 self._l[i:i] = newlines self._l.insert(i + 1, ('}', idx)) offset += 2 + len(newlines) self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._v.append(VerInfo()) return idx def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] isactive = False lineno = 0 for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': if istack and (istack[-1][1] >= v): raise WeaveFormatError("improperly nested insertions %d>=%d on line %d" % (istack[-1][1], v, lineno)) istack.append(l) isactive = (v in included) elif c == '}': oldc, oldv = istack.pop() assert oldc == '{' assert oldv == v isactive = istack and (istack[-1][1] in included) else: raise WeaveFormatError("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WeaveFormatError("literal at top level on line %d" % lineno) if isactive: origin = istack[-1][1] yield origin, lineno, l lineno += 1 if istack: raise WeaveFormatError("unclosed insertion blocks at end of weave", istack) def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise WeaveFormatError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise WeaveFormatError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (line1, line2, newlines), indicating that line1 through line2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) basis = list(self._extract(included)) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (origin, lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][0] real_i2 = basis[i2][0] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] commit refs/heads/tmp mark :900 committer Martin Pool 1119950490 +1000 data 127 Basic parsing of delete instructions. Check that they don't interfere with extracting revisions composed only of insertions. from :899 M 644 inline testweave.py data 10319 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test suite for weave algorithm""" from testsweet import TestBase from weave import Weave, VerInfo, WeaveFormatError # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Weave() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Weave() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class Delta1(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): return ########################## SKIPPED from pprint import pformat k = Weave() k.add([], ['line 1']) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # should be one inserted line after line 0q self.assertEquals(changes, [(1, 1, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(0, 0, ['top line'])]) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Weave() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): return ########################## SKIPPED k = Weave() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) k.add([0, 1], ['line 1', 'middle line', 'line 2']) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) # now multiple insertions at different places k.add([0, 1, 3], ['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc']) self.assertEqual(k.annotate(4), [(0, 'line 1'), (4, 'aaa'), (3, 'middle line'), (4, 'bbb'), (1, 'line 2'), (4, 'ccc')]) class CannedDelete(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) class BadWeave(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = ['bad line', ('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) class BadInsert(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 1), ' more in 1', ('}', 1), ('}', 1), ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) self.assertRaises(WeaveFormatError, k.get, 1) class InsertNested(TestBase): """Insertion with nested instructions.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertEqual(k.get(0), ['foo {', '}']) self.assertEqual(k.get(1), ['foo {', ' added in version 1', ' also from v1', '}']) self.assertEqual(k.get(2), ['foo {', ' added in v2', '}']) self.assertEqual(k.get(3), ['foo {', ' added in version 1', ' added in v2', ' also from v1', '}']) class DeleteLines(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Weave() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) return ################################## SKIPPED k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a weave with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1)] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Weave with two diverged texts based on version 0. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1), ('{', 2), "alternative second line", ('}', 2), ] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) def testweave(): import testsweet from unittest import TestSuite, TestLoader import testweave tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testweave)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testweave()) M 644 inline weave.py data 12814 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy and comparison methods of Weave instances? class VerInfo(object): """Information about a version in a Weave.""" included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. text Sequence of lines to be added in the new version.""" self._check_versions(parents) self._check_lines(text) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) if i1 != i2: raise NotImplementedError("can't handle replacing weave [%d:%d] yet" % (i1, i2)) self._l.insert(i1 + offset, ('{', idx)) i = i1 + offset + 1 self._l[i:i] = newlines self._l.insert(i + 1, ('}', idx)) offset += 2 + len(newlines) self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._v.append(VerInfo()) return idx def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = False lineno = 0 # line of weave, 0-based for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': if istack and (istack[-1] >= v): raise WeaveFormatError("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WeaveFormatError("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WeaveFormatError("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WeaveFormatError("repeated deletion marker for version %d on line %d" % (v, lineno)) else: dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WeaveFormatError("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WeaveFormatError("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WeaveFormatError("literal at top level on line %d" % lineno) isactive = istack[-1] in included if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WeaveFormatError("unclosed insertion blocks at end of weave", istack) if dset: raise WeaveFormatError("unclosed deletion blocks at end of weave", dset) def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise WeaveFormatError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise WeaveFormatError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (line1, line2, newlines), indicating that line1 through line2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) basis = list(self._extract(included)) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (origin, lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][0] real_i2 = basis[i2][0] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] commit refs/heads/tmp mark :901 committer Martin Pool 1119950848 +1000 data 80 Add another constraint: revisions should not delete text that they just inserted from :900 M 644 inline testweave.py data 10743 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test suite for weave algorithm""" from testsweet import TestBase from weave import Weave, VerInfo, WeaveFormatError # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Weave() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Weave() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class Delta1(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): return ########################## SKIPPED from pprint import pformat k = Weave() k.add([], ['line 1']) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # should be one inserted line after line 0q self.assertEquals(changes, [(1, 1, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(0, 0, ['top line'])]) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Weave() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): return ########################## SKIPPED k = Weave() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) k.add([0, 1], ['line 1', 'middle line', 'line 2']) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) # now multiple insertions at different places k.add([0, 1, 3], ['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc']) self.assertEqual(k.annotate(4), [(0, 'line 1'), (4, 'aaa'), (3, 'middle line'), (4, 'bbb'), (1, 'line 2'), (4, 'ccc')]) class SuicideDelete(TestBase): def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = [('{', 0), 'first line', ('[', 0), 'deleted in 0', (']', 0), ('}', 0), ] self.assertRaises(WeaveFormatError, k.get, 0) class CannedDelete(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) class BadWeave(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = ['bad line', ('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) class BadInsert(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 1), ' more in 1', ('}', 1), ('}', 1), ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) self.assertRaises(WeaveFormatError, k.get, 1) class InsertNested(TestBase): """Insertion with nested instructions.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertEqual(k.get(0), ['foo {', '}']) self.assertEqual(k.get(1), ['foo {', ' added in version 1', ' also from v1', '}']) self.assertEqual(k.get(2), ['foo {', ' added in v2', '}']) self.assertEqual(k.get(3), ['foo {', ' added in version 1', ' added in v2', ' also from v1', '}']) class DeleteLines(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Weave() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) return ################################## SKIPPED k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a weave with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1)] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Weave with two diverged texts based on version 0. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1), ('{', 2), "alternative second line", ('}', 2), ] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) def testweave(): import testsweet from unittest import TestSuite, TestLoader import testweave tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testweave)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testweave()) M 644 inline weave.py data 13234 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy and comparison methods of Weave instances? class VerInfo(object): """Information about a version in a Weave.""" included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. text Sequence of lines to be added in the new version.""" self._check_versions(parents) self._check_lines(text) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) if i1 != i2: raise NotImplementedError("can't handle replacing weave [%d:%d] yet" % (i1, i2)) self._l.insert(i1 + offset, ('{', idx)) i = i1 + offset + 1 self._l[i:i] = newlines self._l.insert(i + 1, ('}', idx)) offset += 2 + len(newlines) self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._v.append(VerInfo()) return idx def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = False lineno = 0 # line of weave, 0-based for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': if istack and (istack[-1] >= v): raise WeaveFormatError("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WeaveFormatError("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WeaveFormatError("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WeaveFormatError("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WeaveFormatError("version %d deletes own text on line %d" % (v, lineno)) dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WeaveFormatError("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WeaveFormatError("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WeaveFormatError("literal at top level on line %d" % lineno) isactive = istack[-1] in included if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WeaveFormatError("unclosed insertion blocks at end of weave", istack) if dset: raise WeaveFormatError("unclosed deletion blocks at end of weave", dset) def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise WeaveFormatError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise WeaveFormatError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (line1, line2, newlines), indicating that line1 through line2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) basis = list(self._extract(included)) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (origin, lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][0] real_i2 = basis[i2][0] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] commit refs/heads/tmp mark :902 committer Martin Pool 1119951123 +1000 data 40 Basic implementation of deletion markers from :901 M 644 inline testweave.py data 10887 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test suite for weave algorithm""" from testsweet import TestBase from weave import Weave, VerInfo, WeaveFormatError # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Weave() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Weave() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class Delta1(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): return ########################## SKIPPED from pprint import pformat k = Weave() k.add([], ['line 1']) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # should be one inserted line after line 0q self.assertEquals(changes, [(1, 1, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(0, 0, ['top line'])]) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Weave() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): return ########################## SKIPPED k = Weave() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) k.add([0, 1], ['line 1', 'middle line', 'line 2']) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) # now multiple insertions at different places k.add([0, 1, 3], ['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc']) self.assertEqual(k.annotate(4), [(0, 'line 1'), (4, 'aaa'), (3, 'middle line'), (4, 'bbb'), (1, 'line 2'), (4, 'ccc')]) class SuicideDelete(TestBase): def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = [('{', 0), 'first line', ('[', 0), 'deleted in 0', (']', 0), ('}', 0), ] self.assertRaises(WeaveFormatError, k.get, 0) class CannedDelete(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'last line', ]) class BadWeave(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = ['bad line', ('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) class BadInsert(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 1), ' more in 1', ('}', 1), ('}', 1), ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) self.assertRaises(WeaveFormatError, k.get, 1) class InsertNested(TestBase): """Insertion with nested instructions.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertEqual(k.get(0), ['foo {', '}']) self.assertEqual(k.get(1), ['foo {', ' added in version 1', ' also from v1', '}']) self.assertEqual(k.get(2), ['foo {', ' added in v2', '}']) self.assertEqual(k.get(3), ['foo {', ' added in version 1', ' added in v2', ' also from v1', '}']) class DeleteLines(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Weave() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) return ################################## SKIPPED k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a weave with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1)] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Weave with two diverged texts based on version 0. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1), ('{', 2), "alternative second line", ('}', 2), ] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) def testweave(): import testsweet from unittest import TestSuite, TestLoader import testweave tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testweave)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testweave()) M 644 inline weave.py data 13301 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy and comparison methods of Weave instances? class VerInfo(object): """Information about a version in a Weave.""" included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. text Sequence of lines to be added in the new version.""" self._check_versions(parents) self._check_lines(text) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) if i1 != i2: raise NotImplementedError("can't handle replacing weave [%d:%d] yet" % (i1, i2)) self._l.insert(i1 + offset, ('{', idx)) i = i1 + offset + 1 self._l[i:i] = newlines self._l.insert(i + 1, ('}', idx)) offset += 2 + len(newlines) self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._v.append(VerInfo()) return idx def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = False lineno = 0 # line of weave, 0-based for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': if istack and (istack[-1] >= v): raise WeaveFormatError("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WeaveFormatError("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WeaveFormatError("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WeaveFormatError("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WeaveFormatError("version %d deletes own text on line %d" % (v, lineno)) dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WeaveFormatError("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WeaveFormatError("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WeaveFormatError("literal at top level on line %d" % lineno) isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WeaveFormatError("unclosed insertion blocks at end of weave", istack) if dset: raise WeaveFormatError("unclosed deletion blocks at end of weave", dset) def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise WeaveFormatError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise WeaveFormatError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (line1, line2, newlines), indicating that line1 through line2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) basis = list(self._extract(included)) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (origin, lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][0] real_i2 = basis[i2][0] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] commit refs/heads/tmp mark :903 committer Martin Pool 1119951243 +1000 data 30 Add test for replacement lines from :902 M 644 inline testweave.py data 11801 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test suite for weave algorithm""" from testsweet import TestBase from weave import Weave, VerInfo, WeaveFormatError # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Weave() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Weave() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class Delta1(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): return ########################## SKIPPED from pprint import pformat k = Weave() k.add([], ['line 1']) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # should be one inserted line after line 0q self.assertEquals(changes, [(1, 1, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(0, 0, ['top line'])]) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Weave() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): return ########################## SKIPPED k = Weave() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) k.add([0, 1], ['line 1', 'middle line', 'line 2']) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) # now multiple insertions at different places k.add([0, 1, 3], ['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc']) self.assertEqual(k.annotate(4), [(0, 'line 1'), (4, 'aaa'), (3, 'middle line'), (4, 'bbb'), (1, 'line 2'), (4, 'ccc')]) class SuicideDelete(TestBase): def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = [('{', 0), 'first line', ('[', 0), 'deleted in 0', (']', 0), ('}', 0), ] self.assertRaises(WeaveFormatError, k.get, 0) class CannedDelete(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'last line', ]) class CannedReplacement(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), ('{', 1), 'replacement line', ('}', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'replacement line', 'last line', ]) class BadWeave(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = ['bad line', ('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) class BadInsert(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 1), ' more in 1', ('}', 1), ('}', 1), ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) self.assertRaises(WeaveFormatError, k.get, 1) class InsertNested(TestBase): """Insertion with nested instructions.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertEqual(k.get(0), ['foo {', '}']) self.assertEqual(k.get(1), ['foo {', ' added in version 1', ' also from v1', '}']) self.assertEqual(k.get(2), ['foo {', ' added in v2', '}']) self.assertEqual(k.get(3), ['foo {', ' added in version 1', ' added in v2', ' also from v1', '}']) class DeleteLines(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Weave() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) return ################################## SKIPPED k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a weave with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1)] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Weave with two diverged texts based on version 0. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1), ('{', 2), "alternative second line", ('}', 2), ] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) def testweave(): import testsweet from unittest import TestSuite, TestLoader import testweave tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testweave)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testweave()) commit refs/heads/tmp mark :904 committer Martin Pool 1119962624 +1000 data 42 Update tests for new weave representation from :903 M 644 inline testweave.py data 12094 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test suite for weave algorithm""" from testsweet import TestBase from weave import Weave, VerInfo, WeaveFormatError # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Weave() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Weave() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class Delta1(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): from pprint import pformat k = Weave() k.add([], ['line 1']) self.assertEqual(k._l, [('{', 0), 'line 1', ('}', 0), ]) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # currently there are 3 lines in the weave, and we insert after them self.assertEquals(changes, [(3, 3, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(0, 0, ['top line'])]) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Weave() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): return ########################## SKIPPED k = Weave() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) k.add([0, 1], ['line 1', 'middle line', 'line 2']) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) # now multiple insertions at different places k.add([0, 1, 3], ['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc']) self.assertEqual(k.annotate(4), [(0, 'line 1'), (4, 'aaa'), (3, 'middle line'), (4, 'bbb'), (1, 'line 2'), (4, 'ccc')]) class SuicideDelete(TestBase): def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = [('{', 0), 'first line', ('[', 0), 'deleted in 0', (']', 0), ('}', 0), ] self.assertRaises(WeaveFormatError, k.get, 0) class CannedDelete(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'last line', ]) class CannedReplacement(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), ('{', 1), 'replacement line', ('}', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'replacement line', 'last line', ]) class BadWeave(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = ['bad line', ('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) class BadInsert(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 1), ' more in 1', ('}', 1), ('}', 1), ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) self.assertRaises(WeaveFormatError, k.get, 1) class InsertNested(TestBase): """Insertion with nested instructions.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertEqual(k.get(0), ['foo {', '}']) self.assertEqual(k.get(1), ['foo {', ' added in version 1', ' also from v1', '}']) self.assertEqual(k.get(2), ['foo {', ' added in v2', '}']) self.assertEqual(k.get(3), ['foo {', ' added in version 1', ' added in v2', ' also from v1', '}']) class DeleteLines(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Weave() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) return ################################## SKIPPED k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a weave with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1)] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Weave with two diverged texts based on version 0. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1), ('{', 2), "alternative second line", ('}', 2), ] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) def testweave(): import testsweet from unittest import TestSuite, TestLoader import testweave tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testweave)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testweave()) M 644 inline weave.py data 13300 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy and comparison methods of Weave instances? class VerInfo(object): """Information about a version in a Weave.""" included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. text Sequence of lines to be added in the new version.""" self._check_versions(parents) self._check_lines(text) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) if i1 != i2: raise NotImplementedError("can't handle replacing weave [%d:%d] yet" % (i1, i2)) self._l.insert(i1 + offset, ('{', idx)) i = i1 + offset + 1 self._l[i:i] = newlines self._l.insert(i + 1, ('}', idx)) offset += 2 + len(newlines) self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._v.append(VerInfo()) return idx def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = False lineno = 0 # line of weave, 0-based for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': if istack and (istack[-1] >= v): raise WeaveFormatError("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WeaveFormatError("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WeaveFormatError("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WeaveFormatError("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WeaveFormatError("version %d deletes own text on line %d" % (v, lineno)) dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WeaveFormatError("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WeaveFormatError("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WeaveFormatError("literal at top level on line %d" % lineno) isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WeaveFormatError("unclosed insertion blocks at end of weave", istack) if dset: raise WeaveFormatError("unclosed deletion blocks at end of weave", dset) def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise WeaveFormatError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise WeaveFormatError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (line1, line2, newlines), indicating that line1 through line2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) basis = list(self._extract(included)) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (origin, lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][0] real_i2 = basis[i2][0] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] commit refs/heads/tmp mark :905 committer Martin Pool 1119964500 +1000 data 3 doc from :904 M 644 inline weave.py data 13558 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy and comparison methods of Weave instances? class VerInfo(object): """Information about a version in a Weave.""" included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. text Sequence of lines to be added in the new version.""" self._check_versions(parents) self._check_lines(text) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) if i1 != i2: raise NotImplementedError("can't handle replacing weave [%d:%d] yet" % (i1, i2)) self._l.insert(i1 + offset, ('{', idx)) i = i1 + offset + 1 self._l[i:i] = newlines self._l.insert(i + 1, ('}', idx)) offset += 2 + len(newlines) self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._v.append(VerInfo()) return idx def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = False lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': if istack and (istack[-1] >= v): raise WeaveFormatError("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WeaveFormatError("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WeaveFormatError("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WeaveFormatError("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WeaveFormatError("version %d deletes own text on line %d" % (v, lineno)) dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WeaveFormatError("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WeaveFormatError("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WeaveFormatError("literal at top level on line %d" % lineno) isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WeaveFormatError("unclosed insertion blocks at end of weave", istack) if dset: raise WeaveFormatError("unclosed deletion blocks at end of weave", dset) def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise WeaveFormatError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise WeaveFormatError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (line1, line2, newlines), indicating that line1 through line2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) basis = list(self._extract(included)) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (origin, lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][0] real_i2 = basis[i2][0] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] commit refs/heads/tmp mark :906 committer Martin Pool 1119965501 +1000 data 45 Fix weave line calculation when making deltas from :905 M 644 inline testweave.py data 12222 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test suite for weave algorithm""" from testsweet import TestBase from weave import Weave, VerInfo, WeaveFormatError # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Weave() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Weave() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class Delta1(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): from pprint import pformat k = Weave() k.add([], ['line 1']) self.assertEqual(k._l, [('{', 0), 'line 1', ('}', 0), ]) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # currently there are 3 lines in the weave, and we insert after them self.assertEquals(changes, [(3, 3, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(1, 1, ['top line'])]) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Weave() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): k = Weave() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) text3 = ['line 1', 'middle line', 'line 2'] k.add([0, 1], text3) from pprint import pformat self.log("changes to text3: " + pformat(list(k._delta(set([0, 1]), text3)))) self.log("k._l=" + pformat(k._l)) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) # now multiple insertions at different places k.add([0, 1, 3], ['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc']) self.assertEqual(k.annotate(4), [(0, 'line 1'), (4, 'aaa'), (3, 'middle line'), (4, 'bbb'), (1, 'line 2'), (4, 'ccc')]) class SuicideDelete(TestBase): def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = [('{', 0), 'first line', ('[', 0), 'deleted in 0', (']', 0), ('}', 0), ] self.assertRaises(WeaveFormatError, k.get, 0) class CannedDelete(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'last line', ]) class CannedReplacement(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), ('{', 1), 'replacement line', ('}', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'replacement line', 'last line', ]) class BadWeave(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = ['bad line', ('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) class BadInsert(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 1), ' more in 1', ('}', 1), ('}', 1), ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) self.assertRaises(WeaveFormatError, k.get, 1) class InsertNested(TestBase): """Insertion with nested instructions.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertEqual(k.get(0), ['foo {', '}']) self.assertEqual(k.get(1), ['foo {', ' added in version 1', ' also from v1', '}']) self.assertEqual(k.get(2), ['foo {', ' added in v2', '}']) self.assertEqual(k.get(3), ['foo {', ' added in version 1', ' added in v2', ' also from v1', '}']) class DeleteLines(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Weave() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) return ################################## SKIPPED k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a weave with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1)] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Weave with two diverged texts based on version 0. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1), ('{', 2), "alternative second line", ('}', 2), ] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) def testweave(): import testsweet from unittest import TestSuite, TestLoader import testweave tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testweave)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testweave()) M 644 inline weave.py data 13599 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy and comparison methods of Weave instances? class VerInfo(object): """Information about a version in a Weave.""" included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. text Sequence of lines to be added in the new version.""" self._check_versions(parents) self._check_lines(text) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) if i1 != i2: raise NotImplementedError("can't handle replacing weave [%d:%d] yet" % (i1, i2)) i = i1 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._v.append(VerInfo()) return idx def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = False lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': if istack and (istack[-1] >= v): raise WeaveFormatError("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WeaveFormatError("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WeaveFormatError("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WeaveFormatError("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WeaveFormatError("version %d deletes own text on line %d" % (v, lineno)) dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WeaveFormatError("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WeaveFormatError("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WeaveFormatError("literal at top level on line %d" % lineno) isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WeaveFormatError("unclosed insertion blocks at end of weave", istack) if dset: raise WeaveFormatError("unclosed deletion blocks at end of weave", dset) def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise WeaveFormatError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise WeaveFormatError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (line1, line2, newlines), indicating that line1 through line2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) # basis a list of (origin, lineno, line) basis = list(self._extract(included)) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (origin, lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((None, len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][1] real_i2 = basis[i2][1] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] commit refs/heads/tmp mark :907 committer Martin Pool 1119965935 +1000 data 3 doc from :906 M 644 inline testweave.py data 12317 #! /usr/bin/python2.4 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test suite for weave algorithm""" from testsweet import TestBase from weave import Weave, VerInfo, WeaveFormatError # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Weave() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Weave() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class DeltaAdd(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): from pprint import pformat k = Weave() k.add([], ['line 1']) self.assertEqual(k._l, [('{', 0), 'line 1', ('}', 0), ]) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # currently there are 3 lines in the weave, and we insert after them self.assertEquals(changes, [(3, 3, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(1, 1, ['top line'])]) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Weave() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): k = Weave() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) text3 = ['line 1', 'middle line', 'line 2'] k.add([0, 1], text3) from pprint import pformat self.log("changes to text3: " + pformat(list(k._delta(set([0, 1]), text3)))) self.log("k._l=" + pformat(k._l)) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) # now multiple insertions at different places k.add([0, 1, 3], ['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc']) self.assertEqual(k.annotate(4), [(0, 'line 1'), (4, 'aaa'), (3, 'middle line'), (4, 'bbb'), (1, 'line 2'), (4, 'ccc')]) class SuicideDelete(TestBase): """Invalid weave which tries to add and delete simultaneously.""" def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = [('{', 0), 'first line', ('[', 0), 'deleted in 0', (']', 0), ('}', 0), ] self.assertRaises(WeaveFormatError, k.get, 0) class CannedDelete(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'last line', ]) class CannedReplacement(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), ('{', 1), 'replacement line', ('}', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'replacement line', 'last line', ]) class BadWeave(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = ['bad line', ('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) class BadInsert(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 1), ' more in 1', ('}', 1), ('}', 1), ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) self.assertRaises(WeaveFormatError, k.get, 1) class InsertNested(TestBase): """Insertion with nested instructions.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertEqual(k.get(0), ['foo {', '}']) self.assertEqual(k.get(1), ['foo {', ' added in version 1', ' also from v1', '}']) self.assertEqual(k.get(2), ['foo {', ' added in v2', '}']) self.assertEqual(k.get(3), ['foo {', ' added in version 1', ' added in v2', ' also from v1', '}']) class DeleteLines(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Weave() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) return ################################## SKIPPED k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a weave with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1)] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Weave with two diverged texts based on version 0. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1), ('{', 2), "alternative second line", ('}', 2), ] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) def testweave(): import testsweet from unittest import TestSuite, TestLoader import testweave tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testweave)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testweave()) M 644 inline weave.py data 13682 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy and comparison methods of Weave instances? class VerInfo(object): """Information about a version in a Weave.""" included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. text Sequence of lines to be added in the new version.""" self._check_versions(parents) self._check_lines(text) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) if i1 != i2: raise NotImplementedError("can't handle replacing weave [%d:%d] yet" % (i1, i2)) i = i1 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._v.append(VerInfo()) return idx def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = False lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': if istack and (istack[-1] >= v): raise WeaveFormatError("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WeaveFormatError("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WeaveFormatError("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WeaveFormatError("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WeaveFormatError("version %d deletes own text on line %d" % (v, lineno)) dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WeaveFormatError("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WeaveFormatError("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WeaveFormatError("literal at top level on line %d" % lineno) isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WeaveFormatError("unclosed insertion blocks at end of weave", istack) if dset: raise WeaveFormatError("unclosed deletion blocks at end of weave", dset) def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise WeaveFormatError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise WeaveFormatError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) # basis a list of (origin, lineno, line) basis = list(self._extract(included)) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (origin, lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((None, len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][1] real_i2 = basis[i2][1] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] commit refs/heads/tmp mark :908 committer Martin Pool 1119966937 +1000 data 78 Handle deletion of lines by marking the region with a deletion Test deletions from :907 M 644 inline testweave.py data 13145 #! /usr/bin/python2.4 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test suite for weave algorithm""" from testsweet import TestBase from weave import Weave, VerInfo, WeaveFormatError # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Weave() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Weave() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class DeltaAdd(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): from pprint import pformat k = Weave() k.add([], ['line 1']) self.assertEqual(k._l, [('{', 0), 'line 1', ('}', 0), ]) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # currently there are 3 lines in the weave, and we insert after them self.assertEquals(changes, [(3, 3, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(1, 1, ['top line'])]) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Weave() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): k = Weave() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) text3 = ['line 1', 'middle line', 'line 2'] k.add([0, 1], text3) from pprint import pformat self.log("changes to text3: " + pformat(list(k._delta(set([0, 1]), text3)))) self.log("k._l=" + pformat(k._l)) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) # now multiple insertions at different places k.add([0, 1, 3], ['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc']) self.assertEqual(k.annotate(4), [(0, 'line 1'), (4, 'aaa'), (3, 'middle line'), (4, 'bbb'), (1, 'line 2'), (4, 'ccc')]) class DeleteLines(TestBase): """Deletion of lines from existing text. Try various texts all based on a common ancestor.""" def runTest(self): k = Weave() base_text = ['one', 'two', 'three', 'four'] k.add([], base_text) texts = [['one', 'two', 'three'], ['two', 'three', 'four'], ['one', 'four'], ['one', 'two', 'three', 'four'], ] for t in texts: ver = k.add([0], t) from pprint import pformat self.log('final weave:') self.log('k._l=' + pformat(k._l)) for i in range(len(texts)): self.assertEqual(k.get(i+1), texts[i]) class SuicideDelete(TestBase): """Invalid weave which tries to add and delete simultaneously.""" def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = [('{', 0), 'first line', ('[', 0), 'deleted in 0', (']', 0), ('}', 0), ] self.assertRaises(WeaveFormatError, k.get, 0) class CannedDelete(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'last line', ]) class CannedReplacement(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), ('{', 1), 'replacement line', ('}', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'replacement line', 'last line', ]) class BadWeave(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = ['bad line', ('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) class BadInsert(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 1), ' more in 1', ('}', 1), ('}', 1), ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) self.assertRaises(WeaveFormatError, k.get, 1) class InsertNested(TestBase): """Insertion with nested instructions.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertEqual(k.get(0), ['foo {', '}']) self.assertEqual(k.get(1), ['foo {', ' added in version 1', ' also from v1', '}']) self.assertEqual(k.get(2), ['foo {', ' added in v2', '}']) self.assertEqual(k.get(3), ['foo {', ' added in version 1', ' added in v2', ' also from v1', '}']) class DeleteLines2(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Weave() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) self.assertEqual(k.annotate(1), [(0, "line the first"), (0, "fine")]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a weave with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1)] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Weave with two diverged texts based on version 0. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1), ('{', 2), "alternative second line", ('}', 2), ] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) def testweave(): import testsweet from unittest import TestSuite, TestLoader import testweave tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testweave)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testweave()) M 644 inline weave.py data 13866 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy and comparison methods of Weave instances? class VerInfo(object): """Information about a version in a Weave.""" included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. text Sequence of lines to be added in the new version.""" self._check_versions(parents) self._check_lines(text) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: i = i1 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._v.append(VerInfo()) return idx def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = False lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': if istack and (istack[-1] >= v): raise WeaveFormatError("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WeaveFormatError("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WeaveFormatError("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WeaveFormatError("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WeaveFormatError("version %d deletes own text on line %d" % (v, lineno)) dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WeaveFormatError("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WeaveFormatError("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WeaveFormatError("literal at top level on line %d" % lineno) isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WeaveFormatError("unclosed insertion blocks at end of weave", istack) if dset: raise WeaveFormatError("unclosed deletion blocks at end of weave", dset) def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise WeaveFormatError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise WeaveFormatError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) # basis a list of (origin, lineno, line) basis = list(self._extract(included)) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (origin, lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((None, len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][1] real_i2 = basis[i2][1] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] commit refs/heads/tmp mark :909 committer Martin Pool 1119967814 +1000 data 182 Fix bug in an update edit that both deletes and inserts -- previously the insert was clobbered by our own delete. Add some tests that do such updates. Clean up imports of pformat. from :908 M 644 inline testweave.py data 14263 #! /usr/bin/python2.4 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test suite for weave algorithm""" from testsweet import TestBase from weave import Weave, VerInfo, WeaveFormatError from pprint import pformat # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Weave() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Weave() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class DeltaAdd(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): k = Weave() k.add([], ['line 1']) self.assertEqual(k._l, [('{', 0), 'line 1', ('}', 0), ]) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # currently there are 3 lines in the weave, and we insert after them self.assertEquals(changes, [(3, 3, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(1, 1, ['top line'])]) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Weave() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): k = Weave() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) text3 = ['line 1', 'middle line', 'line 2'] k.add([0, 1], text3) self.log("changes to text3: " + pformat(list(k._delta(set([0, 1]), text3)))) self.log("k._l=" + pformat(k._l)) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) # now multiple insertions at different places k.add([0, 1, 3], ['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc']) self.assertEqual(k.annotate(4), [(0, 'line 1'), (4, 'aaa'), (3, 'middle line'), (4, 'bbb'), (1, 'line 2'), (4, 'ccc')]) class DeleteLines(TestBase): """Deletion of lines from existing text. Try various texts all based on a common ancestor.""" def runTest(self): k = Weave() base_text = ['one', 'two', 'three', 'four'] k.add([], base_text) texts = [['one', 'two', 'three'], ['two', 'three', 'four'], ['one', 'four'], ['one', 'two', 'three', 'four'], ] for t in texts: ver = k.add([0], t) self.log('final weave:') self.log('k._l=' + pformat(k._l)) for i in range(len(texts)): self.assertEqual(k.get(i+1), texts[i]) class SuicideDelete(TestBase): """Invalid weave which tries to add and delete simultaneously.""" def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = [('{', 0), 'first line', ('[', 0), 'deleted in 0', (']', 0), ('}', 0), ] self.assertRaises(WeaveFormatError, k.get, 0) class CannedDelete(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'last line', ]) class CannedReplacement(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), ('{', 1), 'replacement line', ('}', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'replacement line', 'last line', ]) class BadWeave(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = ['bad line', ('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) class BadInsert(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 1), ' more in 1', ('}', 1), ('}', 1), ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) self.assertRaises(WeaveFormatError, k.get, 1) class InsertNested(TestBase): """Insertion with nested instructions.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertEqual(k.get(0), ['foo {', '}']) self.assertEqual(k.get(1), ['foo {', ' added in version 1', ' also from v1', '}']) self.assertEqual(k.get(2), ['foo {', ' added in v2', '}']) self.assertEqual(k.get(3), ['foo {', ' added in version 1', ' added in v2', ' also from v1', '}']) class DeleteLines2(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Weave() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) self.assertEqual(k.annotate(1), [(0, "line the first"), (0, "fine")]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a weave with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1)] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Weave with two diverged texts based on version 0. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1), ('{', 2), "alternative second line", ('}', 2), ] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) class ReplaceLine(TestBase): def runTest(self): k = Weave() text0 = ['cheddar', 'stilton', 'gruyere'] text1 = ['cheddar', 'blue vein', 'neufchatel', 'chevre'] k.add([], text0) k.add([0], text1) self.log('k._l=' + pformat(k._l)) self.assertEqual(k.get(1), text1) class Khayyam(TestBase): def runTest(self): rawtexts = [ """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, -- and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise enow!""", """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, -- and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now!""", ] texts = [[l.strip() for l in t.split('\n')] for t in rawtexts] k = Weave() parents = set() for t in texts: ver = k.add(parents, t) parents.add(ver) for i, t in enumerate(texts): self.assertEqual(k.get(i), t) def testweave(): import testsweet from unittest import TestSuite, TestLoader import testweave tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testweave)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testweave()) M 644 inline weave.py data 14058 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy and comparison methods of Weave instances? class VerInfo(object): """Information about a version in a Weave.""" included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. text Sequence of lines to be added in the new version.""" self._check_versions(parents) self._check_lines(text) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._v.append(VerInfo()) return idx def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = False lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': if istack and (istack[-1] >= v): raise WeaveFormatError("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WeaveFormatError("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WeaveFormatError("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WeaveFormatError("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WeaveFormatError("version %d deletes own text on line %d" % (v, lineno)) dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WeaveFormatError("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WeaveFormatError("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WeaveFormatError("literal at top level on line %d" % lineno) isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WeaveFormatError("unclosed insertion blocks at end of weave", istack) if dset: raise WeaveFormatError("unclosed deletion blocks at end of weave", dset) def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise WeaveFormatError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise WeaveFormatError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) # basis a list of (origin, lineno, line) basis = list(self._extract(included)) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (origin, lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((None, len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][1] real_i2 = basis[i2][1] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] commit refs/heads/tmp mark :910 committer Martin Pool 1119968170 +1000 data 3 doc from :909 M 644 inline weave.py data 14208 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy and comparison methods of Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. class VerInfo(object): """Information about a version in a Weave.""" included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. text Sequence of lines to be added in the new version.""" self._check_versions(parents) self._check_lines(text) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._v.append(VerInfo()) return idx def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = False lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': if istack and (istack[-1] >= v): raise WeaveFormatError("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WeaveFormatError("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WeaveFormatError("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WeaveFormatError("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WeaveFormatError("version %d deletes own text on line %d" % (v, lineno)) dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WeaveFormatError("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WeaveFormatError("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WeaveFormatError("literal at top level on line %d" % lineno) isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WeaveFormatError("unclosed insertion blocks at end of weave", istack) if dset: raise WeaveFormatError("unclosed deletion blocks at end of weave", dset) def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise WeaveFormatError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise WeaveFormatError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) # basis a list of (origin, lineno, line) basis = list(self._extract(included)) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (origin, lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((None, len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][1] real_i2 = basis[i2][1] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] commit refs/heads/tmp mark :911 committer Martin Pool 1119968177 +1000 data 23 More modification tests from :910 M 644 inline testweave.py data 14828 #! /usr/bin/python2.4 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test suite for weave algorithm""" from testsweet import TestBase from weave import Weave, VerInfo, WeaveFormatError from pprint import pformat # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Weave() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Weave() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class DeltaAdd(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): k = Weave() k.add([], ['line 1']) self.assertEqual(k._l, [('{', 0), 'line 1', ('}', 0), ]) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # currently there are 3 lines in the weave, and we insert after them self.assertEquals(changes, [(3, 3, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(1, 1, ['top line'])]) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Weave() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): k = Weave() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) text3 = ['line 1', 'middle line', 'line 2'] k.add([0, 1], text3) self.log("changes to text3: " + pformat(list(k._delta(set([0, 1]), text3)))) self.log("k._l=" + pformat(k._l)) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) # now multiple insertions at different places k.add([0, 1, 3], ['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc']) self.assertEqual(k.annotate(4), [(0, 'line 1'), (4, 'aaa'), (3, 'middle line'), (4, 'bbb'), (1, 'line 2'), (4, 'ccc')]) class DeleteLines(TestBase): """Deletion of lines from existing text. Try various texts all based on a common ancestor.""" def runTest(self): k = Weave() base_text = ['one', 'two', 'three', 'four'] k.add([], base_text) texts = [['one', 'two', 'three'], ['two', 'three', 'four'], ['one', 'four'], ['one', 'two', 'three', 'four'], ] for t in texts: ver = k.add([0], t) self.log('final weave:') self.log('k._l=' + pformat(k._l)) for i in range(len(texts)): self.assertEqual(k.get(i+1), texts[i]) class SuicideDelete(TestBase): """Invalid weave which tries to add and delete simultaneously.""" def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = [('{', 0), 'first line', ('[', 0), 'deleted in 0', (']', 0), ('}', 0), ] self.assertRaises(WeaveFormatError, k.get, 0) class CannedDelete(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'last line', ]) class CannedReplacement(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), ('{', 1), 'replacement line', ('}', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'replacement line', 'last line', ]) class BadWeave(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = ['bad line', ('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) class BadInsert(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 1), ' more in 1', ('}', 1), ('}', 1), ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) self.assertRaises(WeaveFormatError, k.get, 1) class InsertNested(TestBase): """Insertion with nested instructions.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertEqual(k.get(0), ['foo {', '}']) self.assertEqual(k.get(1), ['foo {', ' added in version 1', ' also from v1', '}']) self.assertEqual(k.get(2), ['foo {', ' added in v2', '}']) self.assertEqual(k.get(3), ['foo {', ' added in version 1', ' added in v2', ' also from v1', '}']) class DeleteLines2(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Weave() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) self.assertEqual(k.annotate(1), [(0, "line the first"), (0, "fine")]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a weave with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1)] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Weave with two diverged texts based on version 0. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1), ('{', 2), "alternative second line", ('}', 2), ] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) class ReplaceLine(TestBase): def runTest(self): k = Weave() text0 = ['cheddar', 'stilton', 'gruyere'] text1 = ['cheddar', 'blue vein', 'neufchatel', 'chevre'] k.add([], text0) k.add([0], text1) self.log('k._l=' + pformat(k._l)) self.assertEqual(k.get(0), text0) self.assertEqual(k.get(1), text1) class Khayyam(TestBase): def runTest(self): rawtexts = [ """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, -- and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise enow!""", """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, -- and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now!""", """A Book of poems underneath the tree, A Jug of Wine, a Loaf of Bread, and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now! -- O. Khayyam""", """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now! """, ] texts = [[l.strip() for l in t.split('\n')] for t in rawtexts] k = Weave() parents = set() for t in texts: ver = k.add(parents, t) parents.add(ver) self.log("k._l=" + pformat(k._l)) for i, t in enumerate(texts): self.assertEqual(k.get(i), t) def testweave(): import testsweet from unittest import TestSuite, TestLoader import testweave tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testweave)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testweave()) commit refs/heads/tmp mark :912 committer Martin Pool 1119968252 +1000 data 23 Weave eq and ne methods from :911 M 644 inline weave.py data 14455 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy and comparison methods of Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. class VerInfo(object): """Information about a version in a Weave.""" included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. text Sequence of lines to be added in the new version.""" self._check_versions(parents) self._check_lines(text) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._v.append(VerInfo()) return idx def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = False lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': if istack and (istack[-1] >= v): raise WeaveFormatError("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WeaveFormatError("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WeaveFormatError("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WeaveFormatError("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WeaveFormatError("version %d deletes own text on line %d" % (v, lineno)) dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WeaveFormatError("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WeaveFormatError("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WeaveFormatError("literal at top level on line %d" % lineno) isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WeaveFormatError("unclosed insertion blocks at end of weave", istack) if dset: raise WeaveFormatError("unclosed deletion blocks at end of weave", dset) def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise WeaveFormatError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise WeaveFormatError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) # basis a list of (origin, lineno, line) basis = list(self._extract(included)) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (origin, lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((None, len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][1] real_i2 = basis[i2][1] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] commit refs/heads/tmp mark :913 committer Martin Pool 1119968274 +1000 data 3 doc from :912 M 644 inline weave.py data 14440 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. class VerInfo(object): """Information about a version in a Weave.""" included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. text Sequence of lines to be added in the new version.""" self._check_versions(parents) self._check_lines(text) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._v.append(VerInfo()) return idx def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = False lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': if istack and (istack[-1] >= v): raise WeaveFormatError("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WeaveFormatError("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WeaveFormatError("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WeaveFormatError("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WeaveFormatError("version %d deletes own text on line %d" % (v, lineno)) dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WeaveFormatError("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WeaveFormatError("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WeaveFormatError("literal at top level on line %d" % lineno) isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WeaveFormatError("unclosed insertion blocks at end of weave", istack) if dset: raise WeaveFormatError("unclosed deletion blocks at end of weave", dset) def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise WeaveFormatError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise WeaveFormatError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) # basis a list of (origin, lineno, line) basis = list(self._extract(included)) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (origin, lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((None, len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][1] real_i2 = basis[i2][1] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] commit refs/heads/tmp mark :914 committer Martin Pool 1119969525 +1000 data 63 Lame command-line client for reading and writing weaves. Doc. from :913 M 644 inline testweave.py data 14679 #! /usr/bin/python2.4 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test suite for weave algorithm""" from testsweet import TestBase from weave import Weave, VerInfo, WeaveFormatError from pprint import pformat # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Weave() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Weave() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class DeltaAdd(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): k = Weave() k.add([], ['line 1']) self.assertEqual(k._l, [('{', 0), 'line 1', ('}', 0), ]) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # currently there are 3 lines in the weave, and we insert after them self.assertEquals(changes, [(3, 3, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(1, 1, ['top line'])]) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Weave() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): k = Weave() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) text3 = ['line 1', 'middle line', 'line 2'] k.add([0, 1], text3) self.log("changes to text3: " + pformat(list(k._delta(set([0, 1]), text3)))) self.log("k._l=" + pformat(k._l)) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) # now multiple insertions at different places k.add([0, 1, 3], ['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc']) self.assertEqual(k.annotate(4), [(0, 'line 1'), (4, 'aaa'), (3, 'middle line'), (4, 'bbb'), (1, 'line 2'), (4, 'ccc')]) class DeleteLines(TestBase): """Deletion of lines from existing text. Try various texts all based on a common ancestor.""" def runTest(self): k = Weave() base_text = ['one', 'two', 'three', 'four'] k.add([], base_text) texts = [['one', 'two', 'three'], ['two', 'three', 'four'], ['one', 'four'], ['one', 'two', 'three', 'four'], ] for t in texts: ver = k.add([0], t) self.log('final weave:') self.log('k._l=' + pformat(k._l)) for i in range(len(texts)): self.assertEqual(k.get(i+1), texts[i]) class SuicideDelete(TestBase): """Invalid weave which tries to add and delete simultaneously.""" def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = [('{', 0), 'first line', ('[', 0), 'deleted in 0', (']', 0), ('}', 0), ] self.assertRaises(WeaveFormatError, k.get, 0) class CannedDelete(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'last line', ]) class CannedReplacement(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), ('{', 1), 'replacement line', ('}', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'replacement line', 'last line', ]) class BadWeave(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = ['bad line', ('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) class BadInsert(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 1), ' more in 1', ('}', 1), ('}', 1), ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) self.assertRaises(WeaveFormatError, k.get, 1) class InsertNested(TestBase): """Insertion with nested instructions.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertEqual(k.get(0), ['foo {', '}']) self.assertEqual(k.get(1), ['foo {', ' added in version 1', ' also from v1', '}']) self.assertEqual(k.get(2), ['foo {', ' added in v2', '}']) self.assertEqual(k.get(3), ['foo {', ' added in version 1', ' added in v2', ' also from v1', '}']) class DeleteLines2(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Weave() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) self.assertEqual(k.annotate(1), [(0, "line the first"), (0, "fine")]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a weave with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1)] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Weave with two diverged texts based on version 0. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1), ('{', 2), "alternative second line", ('}', 2), ] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) class ReplaceLine(TestBase): def runTest(self): k = Weave() text0 = ['cheddar', 'stilton', 'gruyere'] text1 = ['cheddar', 'blue vein', 'neufchatel', 'chevre'] k.add([], text0) k.add([0], text1) self.log('k._l=' + pformat(k._l)) self.assertEqual(k.get(0), text0) self.assertEqual(k.get(1), text1) class Khayyam(TestBase): def runTest(self): rawtexts = [ """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, -- and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise enow!""", """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, -- and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now!""", """A Book of poems underneath the tree, A Jug of Wine, a Loaf of Bread, and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now! -- O. Khayyam""", """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now! """, ] texts = [[l.strip() for l in t.split('\n')] for t in rawtexts] k = Weave() parents = set() for t in texts: ver = k.add(parents, t) parents.add(ver) self.log("k._l=" + pformat(k._l)) for i, t in enumerate(texts): self.assertEqual(k.get(i), t) def testweave(): import testsweet from unittest import TestSuite, TestLoader import testweave tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testweave)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testweave()) M 644 inline weave.py data 16307 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? class VerInfo(object): """Information about a version in a Weave.""" included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. text Sequence of lines to be added in the new version.""" self._check_versions(parents) self._check_lines(text) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._v.append(VerInfo()) return idx def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = False lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': if istack and (istack[-1] >= v): raise WeaveFormatError("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WeaveFormatError("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WeaveFormatError("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WeaveFormatError("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WeaveFormatError("version %d deletes own text on line %d" % (v, lineno)) dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WeaveFormatError("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WeaveFormatError("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WeaveFormatError("literal at top level on line %d" % lineno) isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WeaveFormatError("unclosed insertion blocks at end of weave", istack) if dset: raise WeaveFormatError("unclosed deletion blocks at end of weave", dset) def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise WeaveFormatError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise WeaveFormatError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) # basis a list of (origin, lineno, line) basis = list(self._extract(included)) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (origin, lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((None, len(self._l), None)) # XXX: which line of the weave should we really consider matches the end of the file? # the current code says it's the last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][1] real_i2 = basis[i2][1] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def main(argv): import sys import os from cPickle import dump, load cmd = argv[1] if cmd == 'add': w = load(file(argv[2], 'rb')) # at the moment, based on everything in the file parents = set(range(len(w._v))) ver = w.add(parents, sys.stdin.readlines()) dump(w, file(argv[2], 'wb')) print 'added %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() dump(w, file(fn, 'wb')) elif cmd == 'get': w = load(file(argv[2], 'rb')) sys.stdout.writelines(w.get(int(argv[3]))) elif cmd == 'annotate': w = load(file(argv[2], 'rb')) # assumes lines are ended lasto = None for origin, text in w.annotate(int(argv[3])): if text[-1] == '\n': text = text[:-1] if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) commit refs/heads/tmp mark :915 committer Martin Pool 1119969768 +1000 data 45 Abbreviate WeaveFormatError in some code Doc from :914 M 644 inline weave.py data 16116 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? class VerInfo(object): """Information about a version in a Weave.""" included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. text Sequence of lines to be added in the new version.""" self._check_versions(parents) self._check_lines(text) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._v.append(VerInfo()) return idx def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = False lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. WFE = WeaveFormatError for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': if istack and (istack[-1] >= v): raise WFE("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WFE("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WFE("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WFE("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WFE("version %d deletes own text on line %d" % (v, lineno)) dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WFE("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WFE("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WFE("literal at top level on line %d" % lineno) isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WFE("unclosed insertion blocks at end of weave", istack) if dset: raise WFE("unclosed deletion blocks at end of weave", dset) def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise WeaveFormatError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise WeaveFormatError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) # basis a list of (origin, lineno, line) basis = list(self._extract(included)) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (origin, lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((None, len(self._l), None)) # XXX: which line of the weave should we really consider # matches the end of the file? the current code says it's the # last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][1] real_i2 = basis[i2][1] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def main(argv): import sys import os from cPickle import dump, load cmd = argv[1] if cmd == 'add': w = load(file(argv[2], 'rb')) # at the moment, based on everything in the file parents = set(range(len(w._v))) ver = w.add(parents, sys.stdin.readlines()) dump(w, file(argv[2], 'wb')) print 'added %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() dump(w, file(fn, 'wb')) elif cmd == 'get': w = load(file(argv[2], 'rb')) sys.stdout.writelines(w.get(int(argv[3]))) elif cmd == 'annotate': w = load(file(argv[2], 'rb')) # assumes lines are ended lasto = None for origin, text in w.annotate(int(argv[3])): if text[-1] == '\n': text = text[:-1] if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) commit refs/heads/tmp mark :916 committer Martin Pool 1119970155 +1000 data 29 Add test for merging versions from :915 M 644 inline testweave.py data 15514 #! /usr/bin/python2.4 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test suite for weave algorithm""" from testsweet import TestBase from weave import Weave, VerInfo, WeaveFormatError from pprint import pformat # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Weave() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Weave() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class DeltaAdd(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): k = Weave() k.add([], ['line 1']) self.assertEqual(k._l, [('{', 0), 'line 1', ('}', 0), ]) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # currently there are 3 lines in the weave, and we insert after them self.assertEquals(changes, [(3, 3, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(1, 1, ['top line'])]) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Weave() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): k = Weave() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) text3 = ['line 1', 'middle line', 'line 2'] k.add([0, 1], text3) self.log("changes to text3: " + pformat(list(k._delta(set([0, 1]), text3)))) self.log("k._l=" + pformat(k._l)) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) # now multiple insertions at different places k.add([0, 1, 3], ['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc']) self.assertEqual(k.annotate(4), [(0, 'line 1'), (4, 'aaa'), (3, 'middle line'), (4, 'bbb'), (1, 'line 2'), (4, 'ccc')]) class DeleteLines(TestBase): """Deletion of lines from existing text. Try various texts all based on a common ancestor.""" def runTest(self): k = Weave() base_text = ['one', 'two', 'three', 'four'] k.add([], base_text) texts = [['one', 'two', 'three'], ['two', 'three', 'four'], ['one', 'four'], ['one', 'two', 'three', 'four'], ] for t in texts: ver = k.add([0], t) self.log('final weave:') self.log('k._l=' + pformat(k._l)) for i in range(len(texts)): self.assertEqual(k.get(i+1), texts[i]) class SuicideDelete(TestBase): """Invalid weave which tries to add and delete simultaneously.""" def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = [('{', 0), 'first line', ('[', 0), 'deleted in 0', (']', 0), ('}', 0), ] self.assertRaises(WeaveFormatError, k.get, 0) class CannedDelete(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'last line', ]) class CannedReplacement(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), ('{', 1), 'replacement line', ('}', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'replacement line', 'last line', ]) class BadWeave(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = ['bad line', ('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) class BadInsert(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 1), ' more in 1', ('}', 1), ('}', 1), ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) self.assertRaises(WeaveFormatError, k.get, 1) class InsertNested(TestBase): """Insertion with nested instructions.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertEqual(k.get(0), ['foo {', '}']) self.assertEqual(k.get(1), ['foo {', ' added in version 1', ' also from v1', '}']) self.assertEqual(k.get(2), ['foo {', ' added in v2', '}']) self.assertEqual(k.get(3), ['foo {', ' added in version 1', ' added in v2', ' also from v1', '}']) class DeleteLines2(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Weave() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) self.assertEqual(k.annotate(1), [(0, "line the first"), (0, "fine")]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a weave with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1)] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Weave with two diverged texts based on version 0. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1), ('{', 2), "alternative second line", ('}', 2), ] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) class ReplaceLine(TestBase): def runTest(self): k = Weave() text0 = ['cheddar', 'stilton', 'gruyere'] text1 = ['cheddar', 'blue vein', 'neufchatel', 'chevre'] k.add([], text0) k.add([0], text1) self.log('k._l=' + pformat(k._l)) self.assertEqual(k.get(0), text0) self.assertEqual(k.get(1), text1) class Merge(TestBase): def runTest(self): k = Weave() texts = [['header'], ['header', '', 'line from 1'], ['header', '', 'line from 2', 'more from 2'], ['header', '', 'line from 1', 'fixup line', 'line from 2'], ] k.add([], texts[0]) k.add([0], texts[1]) k.add([0], texts[2]) k.add([0, 1, 2], texts[3]) for i, t in enumerate(texts): self.assertEqual(k.get(i), t) self.assertEqual(k.annotate(3), [(0, 'header'), (1, ''), (1, 'line from 1'), (3, 'fixup line'), (2, 'line from 2'), ]) self.log('k._l=' + pformat(k._l)) class Khayyam(TestBase): def runTest(self): rawtexts = [ """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, -- and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise enow!""", """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, -- and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now!""", """A Book of poems underneath the tree, A Jug of Wine, a Loaf of Bread, and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now! -- O. Khayyam""", """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now! """, ] texts = [[l.strip() for l in t.split('\n')] for t in rawtexts] k = Weave() parents = set() for t in texts: ver = k.add(parents, t) parents.add(ver) self.log("k._l=" + pformat(k._l)) for i, t in enumerate(texts): self.assertEqual(k.get(i), t) def testweave(): import testsweet from unittest import TestSuite, TestLoader import testweave tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testweave)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testweave()) M 644 inline weave.py data 16224 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? class VerInfo(object): """Information about a version in a Weave.""" included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. This must normally include the parents and the parent's parents, or wierd things might happen. text Sequence of lines to be added in the new version.""" self._check_versions(parents) self._check_lines(text) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._v.append(VerInfo()) return idx def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = False lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. WFE = WeaveFormatError for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': if istack and (istack[-1] >= v): raise WFE("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WFE("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WFE("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WFE("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WFE("version %d deletes own text on line %d" % (v, lineno)) dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WFE("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WFE("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WFE("literal at top level on line %d" % lineno) isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WFE("unclosed insertion blocks at end of weave", istack) if dset: raise WFE("unclosed deletion blocks at end of weave", dset) def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise WeaveFormatError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise WeaveFormatError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) # basis a list of (origin, lineno, line) basis = list(self._extract(included)) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (origin, lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((None, len(self._l), None)) # XXX: which line of the weave should we really consider # matches the end of the file? the current code says it's the # last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][1] real_i2 = basis[i2][1] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def main(argv): import sys import os from cPickle import dump, load cmd = argv[1] if cmd == 'add': w = load(file(argv[2], 'rb')) # at the moment, based on everything in the file parents = set(range(len(w._v))) ver = w.add(parents, sys.stdin.readlines()) dump(w, file(argv[2], 'wb')) print 'added %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() dump(w, file(fn, 'wb')) elif cmd == 'get': w = load(file(argv[2], 'rb')) sys.stdout.writelines(w.get(int(argv[3]))) elif cmd == 'annotate': w = load(file(argv[2], 'rb')) # assumes lines are ended lasto = None for origin, text in w.annotate(int(argv[3])): if text[-1] == '\n': text = text[:-1] if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) commit refs/heads/tmp mark :917 committer Martin Pool 1119970499 +1000 data 69 Add Weave.merge_iter to get automerged lines Add AutoMerge test case from :916 M 644 inline testweave.py data 16203 #! /usr/bin/python2.4 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test suite for weave algorithm""" from testsweet import TestBase from weave import Weave, VerInfo, WeaveFormatError from pprint import pformat # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Weave() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Weave() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class DeltaAdd(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): k = Weave() k.add([], ['line 1']) self.assertEqual(k._l, [('{', 0), 'line 1', ('}', 0), ]) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # currently there are 3 lines in the weave, and we insert after them self.assertEquals(changes, [(3, 3, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(1, 1, ['top line'])]) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Weave() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): k = Weave() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) text3 = ['line 1', 'middle line', 'line 2'] k.add([0, 1], text3) self.log("changes to text3: " + pformat(list(k._delta(set([0, 1]), text3)))) self.log("k._l=" + pformat(k._l)) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) # now multiple insertions at different places k.add([0, 1, 3], ['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc']) self.assertEqual(k.annotate(4), [(0, 'line 1'), (4, 'aaa'), (3, 'middle line'), (4, 'bbb'), (1, 'line 2'), (4, 'ccc')]) class DeleteLines(TestBase): """Deletion of lines from existing text. Try various texts all based on a common ancestor.""" def runTest(self): k = Weave() base_text = ['one', 'two', 'three', 'four'] k.add([], base_text) texts = [['one', 'two', 'three'], ['two', 'three', 'four'], ['one', 'four'], ['one', 'two', 'three', 'four'], ] for t in texts: ver = k.add([0], t) self.log('final weave:') self.log('k._l=' + pformat(k._l)) for i in range(len(texts)): self.assertEqual(k.get(i+1), texts[i]) class SuicideDelete(TestBase): """Invalid weave which tries to add and delete simultaneously.""" def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = [('{', 0), 'first line', ('[', 0), 'deleted in 0', (']', 0), ('}', 0), ] self.assertRaises(WeaveFormatError, k.get, 0) class CannedDelete(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'last line', ]) class CannedReplacement(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), ('{', 1), 'replacement line', ('}', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'replacement line', 'last line', ]) class BadWeave(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = ['bad line', ('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) class BadInsert(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 1), ' more in 1', ('}', 1), ('}', 1), ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) self.assertRaises(WeaveFormatError, k.get, 1) class InsertNested(TestBase): """Insertion with nested instructions.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertEqual(k.get(0), ['foo {', '}']) self.assertEqual(k.get(1), ['foo {', ' added in version 1', ' also from v1', '}']) self.assertEqual(k.get(2), ['foo {', ' added in v2', '}']) self.assertEqual(k.get(3), ['foo {', ' added in version 1', ' added in v2', ' also from v1', '}']) class DeleteLines2(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Weave() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) self.assertEqual(k.annotate(1), [(0, "line the first"), (0, "fine")]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a weave with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1)] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Weave with two diverged texts based on version 0. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1), ('{', 2), "alternative second line", ('}', 2), ] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) class ReplaceLine(TestBase): def runTest(self): k = Weave() text0 = ['cheddar', 'stilton', 'gruyere'] text1 = ['cheddar', 'blue vein', 'neufchatel', 'chevre'] k.add([], text0) k.add([0], text1) self.log('k._l=' + pformat(k._l)) self.assertEqual(k.get(0), text0) self.assertEqual(k.get(1), text1) class Merge(TestBase): """Versions that merge diverged parents""" def runTest(self): k = Weave() texts = [['header'], ['header', '', 'line from 1'], ['header', '', 'line from 2', 'more from 2'], ['header', '', 'line from 1', 'fixup line', 'line from 2'], ] k.add([], texts[0]) k.add([0], texts[1]) k.add([0], texts[2]) k.add([0, 1, 2], texts[3]) for i, t in enumerate(texts): self.assertEqual(k.get(i), t) self.assertEqual(k.annotate(3), [(0, 'header'), (1, ''), (1, 'line from 1'), (3, 'fixup line'), (2, 'line from 2'), ]) self.log('k._l=' + pformat(k._l)) class AutoMerge(TestBase): def runTest(self): k = Weave() texts = [['header', 'aaa', 'bbb'], ['header', 'aaa', 'line from 1', 'bbb'], ['header', 'aaa', 'bbb', 'line from 2', 'more from 2'], ] k.add([], texts[0]) k.add([0], texts[1]) k.add([0], texts[2]) self.log('k._l=' + pformat(k._l)) m = list(k.merge_iter([0, 1, 2])) self.assertEqual(m, ['header', 'aaa', 'line from 1', 'bbb', 'line from 2', 'more from 2']) class Khayyam(TestBase): def runTest(self): rawtexts = [ """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, -- and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise enow!""", """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, -- and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now!""", """A Book of poems underneath the tree, A Jug of Wine, a Loaf of Bread, and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now! -- O. Khayyam""", """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now! """, ] texts = [[l.strip() for l in t.split('\n')] for t in rawtexts] k = Weave() parents = set() for t in texts: ver = k.add(parents, t) parents.add(ver) self.log("k._l=" + pformat(k._l)) for i, t in enumerate(texts): self.assertEqual(k.get(i), t) def testweave(): import testsweet from unittest import TestSuite, TestLoader import testweave tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testweave)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testweave()) M 644 inline weave.py data 16454 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? class VerInfo(object): """Information about a version in a Weave.""" included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. This must normally include the parents and the parent's parents, or wierd things might happen. text Sequence of lines to be added in the new version.""" self._check_versions(parents) self._check_lines(text) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._v.append(VerInfo()) return idx def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = False lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. WFE = WeaveFormatError for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': if istack and (istack[-1] >= v): raise WFE("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WFE("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WFE("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WFE("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WFE("version %d deletes own text on line %d" % (v, lineno)) dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WFE("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WFE("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WFE("literal at top level on line %d" % lineno) isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WFE("unclosed insertion blocks at end of weave", istack) if dset: raise WFE("unclosed deletion blocks at end of weave", dset) def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def merge_iter(self, included): """Return composed version of multiple included versions.""" included = frozenset(included) for origin, lineno, text in self._extract(included): yield text def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise WeaveFormatError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise WeaveFormatError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) # basis a list of (origin, lineno, line) basis = list(self._extract(included)) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (origin, lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((None, len(self._l), None)) # XXX: which line of the weave should we really consider # matches the end of the file? the current code says it's the # last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][1] real_i2 = basis[i2][1] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def main(argv): import sys import os from cPickle import dump, load cmd = argv[1] if cmd == 'add': w = load(file(argv[2], 'rb')) # at the moment, based on everything in the file parents = set(range(len(w._v))) ver = w.add(parents, sys.stdin.readlines()) dump(w, file(argv[2], 'wb')) print 'added %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() dump(w, file(fn, 'wb')) elif cmd == 'get': w = load(file(argv[2], 'rb')) sys.stdout.writelines(w.get(int(argv[3]))) elif cmd == 'annotate': w = load(file(argv[2], 'rb')) # assumes lines are ended lasto = None for origin, text in w.annotate(int(argv[3])): if text[-1] == '\n': text = text[:-1] if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) commit refs/heads/tmp mark :918 committer Martin Pool 1119970615 +1000 data 34 Cope without set/frozenset classes from :917 M 644 inline testweave.py data 16357 #! /usr/bin/python2.4 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test suite for weave algorithm""" from testsweet import TestBase from weave import Weave, VerInfo, WeaveFormatError from pprint import pformat try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, FrozenSet # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Weave() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Weave() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class DeltaAdd(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): k = Weave() k.add([], ['line 1']) self.assertEqual(k._l, [('{', 0), 'line 1', ('}', 0), ]) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # currently there are 3 lines in the weave, and we insert after them self.assertEquals(changes, [(3, 3, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(1, 1, ['top line'])]) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Weave() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): k = Weave() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) text3 = ['line 1', 'middle line', 'line 2'] k.add([0, 1], text3) self.log("changes to text3: " + pformat(list(k._delta(set([0, 1]), text3)))) self.log("k._l=" + pformat(k._l)) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) # now multiple insertions at different places k.add([0, 1, 3], ['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc']) self.assertEqual(k.annotate(4), [(0, 'line 1'), (4, 'aaa'), (3, 'middle line'), (4, 'bbb'), (1, 'line 2'), (4, 'ccc')]) class DeleteLines(TestBase): """Deletion of lines from existing text. Try various texts all based on a common ancestor.""" def runTest(self): k = Weave() base_text = ['one', 'two', 'three', 'four'] k.add([], base_text) texts = [['one', 'two', 'three'], ['two', 'three', 'four'], ['one', 'four'], ['one', 'two', 'three', 'four'], ] for t in texts: ver = k.add([0], t) self.log('final weave:') self.log('k._l=' + pformat(k._l)) for i in range(len(texts)): self.assertEqual(k.get(i+1), texts[i]) class SuicideDelete(TestBase): """Invalid weave which tries to add and delete simultaneously.""" def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = [('{', 0), 'first line', ('[', 0), 'deleted in 0', (']', 0), ('}', 0), ] self.assertRaises(WeaveFormatError, k.get, 0) class CannedDelete(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'last line', ]) class CannedReplacement(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), ('{', 1), 'replacement line', ('}', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'replacement line', 'last line', ]) class BadWeave(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = ['bad line', ('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) class BadInsert(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 1), ' more in 1', ('}', 1), ('}', 1), ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) self.assertRaises(WeaveFormatError, k.get, 1) class InsertNested(TestBase): """Insertion with nested instructions.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertEqual(k.get(0), ['foo {', '}']) self.assertEqual(k.get(1), ['foo {', ' added in version 1', ' also from v1', '}']) self.assertEqual(k.get(2), ['foo {', ' added in v2', '}']) self.assertEqual(k.get(3), ['foo {', ' added in version 1', ' added in v2', ' also from v1', '}']) class DeleteLines2(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Weave() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) self.assertEqual(k.annotate(1), [(0, "line the first"), (0, "fine")]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a weave with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1)] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Weave with two diverged texts based on version 0. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1), ('{', 2), "alternative second line", ('}', 2), ] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) class ReplaceLine(TestBase): def runTest(self): k = Weave() text0 = ['cheddar', 'stilton', 'gruyere'] text1 = ['cheddar', 'blue vein', 'neufchatel', 'chevre'] k.add([], text0) k.add([0], text1) self.log('k._l=' + pformat(k._l)) self.assertEqual(k.get(0), text0) self.assertEqual(k.get(1), text1) class Merge(TestBase): """Versions that merge diverged parents""" def runTest(self): k = Weave() texts = [['header'], ['header', '', 'line from 1'], ['header', '', 'line from 2', 'more from 2'], ['header', '', 'line from 1', 'fixup line', 'line from 2'], ] k.add([], texts[0]) k.add([0], texts[1]) k.add([0], texts[2]) k.add([0, 1, 2], texts[3]) for i, t in enumerate(texts): self.assertEqual(k.get(i), t) self.assertEqual(k.annotate(3), [(0, 'header'), (1, ''), (1, 'line from 1'), (3, 'fixup line'), (2, 'line from 2'), ]) self.log('k._l=' + pformat(k._l)) class AutoMerge(TestBase): def runTest(self): k = Weave() texts = [['header', 'aaa', 'bbb'], ['header', 'aaa', 'line from 1', 'bbb'], ['header', 'aaa', 'bbb', 'line from 2', 'more from 2'], ] k.add([], texts[0]) k.add([0], texts[1]) k.add([0], texts[2]) self.log('k._l=' + pformat(k._l)) m = list(k.merge_iter([0, 1, 2])) self.assertEqual(m, ['header', 'aaa', 'line from 1', 'bbb', 'line from 2', 'more from 2']) class Khayyam(TestBase): def runTest(self): rawtexts = [ """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, -- and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise enow!""", """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, -- and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now!""", """A Book of poems underneath the tree, A Jug of Wine, a Loaf of Bread, and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now! -- O. Khayyam""", """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now! """, ] texts = [[l.strip() for l in t.split('\n')] for t in rawtexts] k = Weave() parents = set() for t in texts: ver = k.add(parents, t) parents.add(ver) self.log("k._l=" + pformat(k._l)) for i, t in enumerate(texts): self.assertEqual(k.get(i), t) def testweave(): import testsweet from unittest import TestSuite, TestLoader import testweave tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testweave)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testweave()) M 644 inline weave.py data 16606 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, FrozenSet class VerInfo(object): """Information about a version in a Weave.""" included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. This must normally include the parents and the parent's parents, or wierd things might happen. text Sequence of lines to be added in the new version.""" self._check_versions(parents) self._check_lines(text) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._v.append(VerInfo()) return idx def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = False lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. WFE = WeaveFormatError for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': if istack and (istack[-1] >= v): raise WFE("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WFE("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WFE("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WFE("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WFE("version %d deletes own text on line %d" % (v, lineno)) dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WFE("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WFE("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WFE("literal at top level on line %d" % lineno) isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WFE("unclosed insertion blocks at end of weave", istack) if dset: raise WFE("unclosed deletion blocks at end of weave", dset) def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def merge_iter(self, included): """Return composed version of multiple included versions.""" included = frozenset(included) for origin, lineno, text in self._extract(included): yield text def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise WeaveFormatError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise WeaveFormatError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) # basis a list of (origin, lineno, line) basis = list(self._extract(included)) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (origin, lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((None, len(self._l), None)) # XXX: which line of the weave should we really consider # matches the end of the file? the current code says it's the # last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][1] real_i2 = basis[i2][1] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def main(argv): import sys import os from cPickle import dump, load cmd = argv[1] if cmd == 'add': w = load(file(argv[2], 'rb')) # at the moment, based on everything in the file parents = set(range(len(w._v))) ver = w.add(parents, sys.stdin.readlines()) dump(w, file(argv[2], 'wb')) print 'added %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() dump(w, file(fn, 'wb')) elif cmd == 'get': w = load(file(argv[2], 'rb')) sys.stdout.writelines(w.get(int(argv[3]))) elif cmd == 'annotate': w = load(file(argv[2], 'rb')) # assumes lines are ended lasto = None for origin, text in w.annotate(int(argv[3])): if text[-1] == '\n': text = text[:-1] if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) commit refs/heads/tmp mark :919 committer Martin Pool 1119970770 +1000 data 37 More fixes to try to run on python2.3 from :918 M 644 inline testsweet.py data 9473 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase def _need_subprocess(): sys.stderr.write("sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") class CommandFailed(Exception): pass class TestSkipped(Exception): """Indicates that a test was intentionally skipped, rather than failing.""" # XXX: Not used yet class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr, so that we can run it # through a specified Python intepreter OVERRIDE_PYTHON = None # to run with alternative python 'python' BZRPATH = 'bzr' _log_buf = "" def setUp(self): super(TestBase, self).setUp() self.log("%s setup" % self.id()) def tearDown(self): super(TestBase, self).tearDown() self.log("%s teardown" % self.id()) self.log('') def formcmd(self, cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = self.BZRPATH if self.OVERRIDE_PYTHON: cmd.insert(0, self.OVERRIDE_PYTHON) self.log('$ %r' % cmd) return cmd def runcmd(self, cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" try: from subprocess import call, Popen, PIPE except ImportError, e: _need_subprocess raise cmd = self.formcmd(cmd) self.log('$ ' + ' '.join(cmd)) actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(self, cmd, retcode=0): """Run a command and return its output""" try: from subprocess import call, Popen, PIPE except ImportError, e: _need_subprocess() raise cmd = self.formcmd(cmd) child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG) outd, errd = child.communicate() self.log(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def build_tree(self, shape): """Build a test tree according to a pattern. shape is a sequence of file specifications. If the final character is '/', a directory is created. This doesn't add anything to a branch. """ # XXX: It's OK to just create them using forward slashes on windows? import os for name in shape: assert isinstance(name, basestring) if name[-1] == '/': os.mkdir(name[:-1]) else: f = file(name, 'wt') print >>f, "contents of", name f.close() def log(self, msg): """Log a message to a progress file""" self._log_buf = self._log_buf + str(msg) + '\n' print >>self.TEST_LOG, msg def check_inventory_shape(self, inv, shape): """ Compare an inventory to a list of expected names. Fail if they are not precisely equal. """ extras = [] shape = list(shape) # copy for path, ie in inv.entries(): name = path.replace('\\', '/') if ie.kind == 'dir': name = name + '/' if name in shape: shape.remove(name) else: extras.append(name) if shape: self.fail("expected paths not found in inventory: %r" % shape) if extras: self.fail("unexpected paths found in inventory: %r" % extras) def check_file_contents(self, filename, expect): self.log("check contents of file %s" % filename) contents = file(filename, 'r').read() if contents != expect: self.log("expected: %r" % expected) self.log("actually: %r" % contents) self.fail("contents of %s not as expected") class InTempDir(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.test_dir = os.path.join(self.TEST_ROOT, self.__class__.__name__) os.mkdir(self.test_dir) os.chdir(self.test_dir) def tearDown(self): import os os.chdir(self.TEST_ROOT) class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ def __init__(self, out): self.out = out TestResult.__init__(self) def startTest(self, test): # TODO: Maybe show test.shortDescription somewhere? print >>self.out, '%-60.60s' % test.id(), self.out.flush() TestResult.startTest(self, test) def stopTest(self, test): # print TestResult.stopTest(self, test) def addError(self, test, err): print >>self.out, 'ERROR' TestResult.addError(self, test, err) _show_test_failure('error', test, err, self.out) def addFailure(self, test, err): print >>self.out, 'FAILURE' TestResult.addFailure(self, test, err) _show_test_failure('failure', test, err, self.out) def addSuccess(self, test): print >>self.out, 'OK' TestResult.addSuccess(self, test) def selftest(): from unittest import TestLoader, TestSuite import bzrlib import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning from doctest import DocTestSuite import os import shutil import time import sys suite = TestSuite() tl = TestLoader() for m in bzrlib.selftest.whitebox, \ bzrlib.selftest.versioning: suite.addTest(tl.loadTestsFromModule(m)) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands: suite.addTest(DocTestSuite(m)) suite.addTest(bzrlib.selftest.blackbox.suite()) return run_suite(suite) def run_suite(suite): import os import shutil import time import sys _setup_test_log() _setup_test_dir() print # save stdout & stderr so there's no leakage from code-under-test real_stdout = sys.stdout real_stderr = sys.stderr sys.stdout = sys.stderr = TestBase.TEST_LOG try: result = _MyResult(real_stdout) suite.run(result) finally: sys.stdout = real_stdout sys.stderr = real_stderr _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os log_filename = os.path.abspath('test.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_ROOT = os.path.abspath("test.tmp") print '%-30s %s' % ('running tests in', TestBase.TEST_ROOT) if os.path.exists(TestBase.TEST_ROOT): shutil.rmtree(TestBase.TEST_ROOT) os.mkdir(TestBase.TEST_ROOT) os.chdir(TestBase.TEST_ROOT) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_ROOT, '.bzr')) def _show_results(result): print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, exc_info, out): from traceback import print_exception print >>out, '-' * 60 print >>out, case desc = case.shortDescription() if desc: print >>out, ' (%s)' % desc print_exception(exc_info[0], exc_info[1], exc_info[2], None, out) if isinstance(case, TestBase): print >>out print >>out, 'log from this test:' print >>out, case._log_buf print >>out, '-' * 60 M 644 inline testweave.py data 16360 #! /usr/bin/python2.4 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test suite for weave algorithm""" from testsweet import TestBase from weave import Weave, VerInfo, WeaveFormatError from pprint import pformat try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Weave() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Weave() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class DeltaAdd(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): k = Weave() k.add([], ['line 1']) self.assertEqual(k._l, [('{', 0), 'line 1', ('}', 0), ]) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # currently there are 3 lines in the weave, and we insert after them self.assertEquals(changes, [(3, 3, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(1, 1, ['top line'])]) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Weave() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): k = Weave() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) text3 = ['line 1', 'middle line', 'line 2'] k.add([0, 1], text3) self.log("changes to text3: " + pformat(list(k._delta(set([0, 1]), text3)))) self.log("k._l=" + pformat(k._l)) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) # now multiple insertions at different places k.add([0, 1, 3], ['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc']) self.assertEqual(k.annotate(4), [(0, 'line 1'), (4, 'aaa'), (3, 'middle line'), (4, 'bbb'), (1, 'line 2'), (4, 'ccc')]) class DeleteLines(TestBase): """Deletion of lines from existing text. Try various texts all based on a common ancestor.""" def runTest(self): k = Weave() base_text = ['one', 'two', 'three', 'four'] k.add([], base_text) texts = [['one', 'two', 'three'], ['two', 'three', 'four'], ['one', 'four'], ['one', 'two', 'three', 'four'], ] for t in texts: ver = k.add([0], t) self.log('final weave:') self.log('k._l=' + pformat(k._l)) for i in range(len(texts)): self.assertEqual(k.get(i+1), texts[i]) class SuicideDelete(TestBase): """Invalid weave which tries to add and delete simultaneously.""" def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = [('{', 0), 'first line', ('[', 0), 'deleted in 0', (']', 0), ('}', 0), ] self.assertRaises(WeaveFormatError, k.get, 0) class CannedDelete(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'last line', ]) class CannedReplacement(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), ('{', 1), 'replacement line', ('}', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'replacement line', 'last line', ]) class BadWeave(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = ['bad line', ('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) class BadInsert(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 1), ' more in 1', ('}', 1), ('}', 1), ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) self.assertRaises(WeaveFormatError, k.get, 1) class InsertNested(TestBase): """Insertion with nested instructions.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertEqual(k.get(0), ['foo {', '}']) self.assertEqual(k.get(1), ['foo {', ' added in version 1', ' also from v1', '}']) self.assertEqual(k.get(2), ['foo {', ' added in v2', '}']) self.assertEqual(k.get(3), ['foo {', ' added in version 1', ' added in v2', ' also from v1', '}']) class DeleteLines2(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Weave() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) self.assertEqual(k.annotate(1), [(0, "line the first"), (0, "fine")]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a weave with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1)] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Weave with two diverged texts based on version 0. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1), ('{', 2), "alternative second line", ('}', 2), ] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) class ReplaceLine(TestBase): def runTest(self): k = Weave() text0 = ['cheddar', 'stilton', 'gruyere'] text1 = ['cheddar', 'blue vein', 'neufchatel', 'chevre'] k.add([], text0) k.add([0], text1) self.log('k._l=' + pformat(k._l)) self.assertEqual(k.get(0), text0) self.assertEqual(k.get(1), text1) class Merge(TestBase): """Versions that merge diverged parents""" def runTest(self): k = Weave() texts = [['header'], ['header', '', 'line from 1'], ['header', '', 'line from 2', 'more from 2'], ['header', '', 'line from 1', 'fixup line', 'line from 2'], ] k.add([], texts[0]) k.add([0], texts[1]) k.add([0], texts[2]) k.add([0, 1, 2], texts[3]) for i, t in enumerate(texts): self.assertEqual(k.get(i), t) self.assertEqual(k.annotate(3), [(0, 'header'), (1, ''), (1, 'line from 1'), (3, 'fixup line'), (2, 'line from 2'), ]) self.log('k._l=' + pformat(k._l)) class AutoMerge(TestBase): def runTest(self): k = Weave() texts = [['header', 'aaa', 'bbb'], ['header', 'aaa', 'line from 1', 'bbb'], ['header', 'aaa', 'bbb', 'line from 2', 'more from 2'], ] k.add([], texts[0]) k.add([0], texts[1]) k.add([0], texts[2]) self.log('k._l=' + pformat(k._l)) m = list(k.merge_iter([0, 1, 2])) self.assertEqual(m, ['header', 'aaa', 'line from 1', 'bbb', 'line from 2', 'more from 2']) class Khayyam(TestBase): def runTest(self): rawtexts = [ """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, -- and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise enow!""", """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, -- and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now!""", """A Book of poems underneath the tree, A Jug of Wine, a Loaf of Bread, and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now! -- O. Khayyam""", """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now! """, ] texts = [[l.strip() for l in t.split('\n')] for t in rawtexts] k = Weave() parents = set() for t in texts: ver = k.add(parents, t) parents.add(ver) self.log("k._l=" + pformat(k._l)) for i, t in enumerate(texts): self.assertEqual(k.get(i), t) def testweave(): import testsweet from unittest import TestSuite, TestLoader import testweave tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testweave)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testweave()) M 644 inline weave.py data 16609 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet class VerInfo(object): """Information about a version in a Weave.""" included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. This must normally include the parents and the parent's parents, or wierd things might happen. text Sequence of lines to be added in the new version.""" self._check_versions(parents) self._check_lines(text) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._v.append(VerInfo()) return idx def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = False lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. WFE = WeaveFormatError for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': if istack and (istack[-1] >= v): raise WFE("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WFE("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WFE("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WFE("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WFE("version %d deletes own text on line %d" % (v, lineno)) dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WFE("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WFE("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WFE("literal at top level on line %d" % lineno) isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WFE("unclosed insertion blocks at end of weave", istack) if dset: raise WFE("unclosed deletion blocks at end of weave", dset) def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def merge_iter(self, included): """Return composed version of multiple included versions.""" included = frozenset(included) for origin, lineno, text in self._extract(included): yield text def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise WeaveFormatError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise WeaveFormatError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) # basis a list of (origin, lineno, line) basis = list(self._extract(included)) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (origin, lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((None, len(self._l), None)) # XXX: which line of the weave should we really consider # matches the end of the file? the current code says it's the # last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][1] real_i2 = basis[i2][1] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def main(argv): import sys import os from cPickle import dump, load cmd = argv[1] if cmd == 'add': w = load(file(argv[2], 'rb')) # at the moment, based on everything in the file parents = set(range(len(w._v))) ver = w.add(parents, sys.stdin.readlines()) dump(w, file(argv[2], 'wb')) print 'added %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() dump(w, file(fn, 'wb')) elif cmd == 'get': w = load(file(argv[2], 'rb')) sys.stdout.writelines(w.get(int(argv[3]))) elif cmd == 'annotate': w = load(file(argv[2], 'rb')) # assumes lines are ended lasto = None for origin, text in w.annotate(int(argv[3])): if text[-1] == '\n': text = text[:-1] if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) commit refs/heads/tmp mark :920 committer Martin Pool 1120043120 +1000 data 3 doc from :919 M 644 inline weave.py data 16667 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? # TODO: Separate out some code to read and write weaves. try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet class VerInfo(object): """Information about a version in a Weave.""" included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. This must normally include the parents and the parent's parents, or wierd things might happen. text Sequence of lines to be added in the new version.""" self._check_versions(parents) self._check_lines(text) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._v.append(VerInfo()) return idx def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = False lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. WFE = WeaveFormatError for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': if istack and (istack[-1] >= v): raise WFE("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WFE("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WFE("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WFE("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WFE("version %d deletes own text on line %d" % (v, lineno)) dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WFE("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WFE("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WFE("literal at top level on line %d" % lineno) isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WFE("unclosed insertion blocks at end of weave", istack) if dset: raise WFE("unclosed deletion blocks at end of weave", dset) def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def merge_iter(self, included): """Return composed version of multiple included versions.""" included = frozenset(included) for origin, lineno, text in self._extract(included): yield text def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise WeaveFormatError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise WeaveFormatError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) # basis a list of (origin, lineno, line) basis = list(self._extract(included)) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (origin, lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((None, len(self._l), None)) # XXX: which line of the weave should we really consider # matches the end of the file? the current code says it's the # last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][1] real_i2 = basis[i2][1] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def main(argv): import sys import os from cPickle import dump, load cmd = argv[1] if cmd == 'add': w = load(file(argv[2], 'rb')) # at the moment, based on everything in the file parents = set(range(len(w._v))) ver = w.add(parents, sys.stdin.readlines()) dump(w, file(argv[2], 'wb')) print 'added %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() dump(w, file(fn, 'wb')) elif cmd == 'get': w = load(file(argv[2], 'rb')) sys.stdout.writelines(w.get(int(argv[3]))) elif cmd == 'annotate': w = load(file(argv[2], 'rb')) # assumes lines are ended lasto = None for origin, text in w.annotate(int(argv[3])): if text[-1] == '\n': text = text[:-1] if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) commit refs/heads/tmp mark :921 committer Martin Pool 1120053496 +1000 data 74 Simple text-based format for storing weaves, cleaner than Python pickles. from :920 M 644 inline weavefile.py data 2546 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Store and retrieve weaves in files. """ # TODO: When extracting a single version it'd be enough to just pass # an iterator returning the weave lines... FORMAT_1 = '# bzr weave file v1' def write_weave_v1(weave, f): """Write weave to file f.""" print >>f, FORMAT_1 print >>f for version, verinfo in enumerate(weave._v): print >>f, 'v', version included = list(verinfo.included) included.sort() print >>f, 'i', for i in included: print >>f, i, print >>f print >>f print >>f, 'w' for l in weave._l: if isinstance(l, tuple): assert len(l) == 2 assert l[0] in '{}[]' print >>f, '%s %d' % l else: assert '\n' not in l print >>f, '.', l print >>f, 'W' def read_weave_v1(f): from weave import Weave, VerInfo w = Weave() assert f.readline() == FORMAT_1+'\n' assert f.readline() == '\n' while True: l = f.readline() if l[0] == 'v': l = f.readline()[:-1] if l[0] != 'i': raise Exception(`l`) if len(l) > 2: included = map(int, l[2:].split(' ')) w._v.append(VerInfo(included)) else: w._v.append(VerInfo()) assert f.readline() == '\n' elif l[0] == 'w': break else: assert 0, l while True: l = f.readline() if l == 'W\n': break elif l[:2] == '. ': w._l.append(l[2:-1]) else: assert l[0] in '{}[]', l assert l[1] == ' ', l w._l.append((l[0], int(l[2:]))) return w M 644 inline weave.py data 16901 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? # TODO: Separate out some code to read and write weaves. # TODO: End marker for each version? # TODO: Check that no insertion occurs inside a deletion that was # active in the version of the insertion. try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet class VerInfo(object): """Information about a version in a Weave.""" included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. This must normally include the parents and the parent's parents, or wierd things might happen. text Sequence of lines to be added in the new version.""" self._check_versions(parents) self._check_lines(text) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._v.append(VerInfo()) return idx def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = False lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. WFE = WeaveFormatError for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': if istack and (istack[-1] >= v): raise WFE("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WFE("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WFE("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WFE("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WFE("version %d deletes own text on line %d" % (v, lineno)) dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WFE("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WFE("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WFE("literal at top level on line %d" % lineno) isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WFE("unclosed insertion blocks at end of weave", istack) if dset: raise WFE("unclosed deletion blocks at end of weave", dset) def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def merge_iter(self, included): """Return composed version of multiple included versions.""" included = frozenset(included) for origin, lineno, text in self._extract(included): yield text def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise WeaveFormatError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise WeaveFormatError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) # basis a list of (origin, lineno, line) basis = list(self._extract(included)) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (origin, lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((None, len(self._l), None)) # XXX: which line of the weave should we really consider # matches the end of the file? the current code says it's the # last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][1] real_i2 = basis[i2][1] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def main(argv): import sys import os from weavefile import write_weave_v1, read_weave_v1 cmd = argv[1] if cmd == 'add': w = read_weave_v1(file(argv[2], 'rb')) # at the moment, based on everything in the file parents = set(range(len(w._v))) lines = [x.rstrip('\n') for x in sys.stdin.xreadlines()] ver = w.add(parents, lines) write_weave_v1(w, file(argv[2], 'wb')) print 'added %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() write_weave_v1(w, file(fn, 'wb')) elif cmd == 'get': w = read_weave_v1(file(argv[2], 'rb')) sys.stdout.writelines(w.get(int(argv[3]))) elif cmd == 'annotate': w = read_weave_v1(file(argv[2], 'rb')) # assumes lines are ended lasto = None for origin, text in w.annotate(int(argv[3])): assert '\n' not in text if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) commit refs/heads/tmp mark :922 committer Martin Pool 1120114299 +1000 data 18 Ignore test.weave from :921 M 644 inline .bzrignore data 29 test.log test.tmp test.weave commit refs/heads/tmp mark :923 committer Martin Pool 1120114351 +1000 data 42 Log externalized weave from one test case from :922 M 644 inline testweave.py data 16473 #! /usr/bin/python2.4 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test suite for weave algorithm""" from testsweet import TestBase from weave import Weave, VerInfo, WeaveFormatError from pprint import pformat try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class Easy(TestBase): def runTest(self): k = Weave() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Weave() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class DeltaAdd(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): k = Weave() k.add([], ['line 1']) self.assertEqual(k._l, [('{', 0), 'line 1', ('}', 0), ]) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # currently there are 3 lines in the weave, and we insert after them self.assertEquals(changes, [(3, 3, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(1, 1, ['top line'])]) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Weave() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): k = Weave() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) text3 = ['line 1', 'middle line', 'line 2'] k.add([0, 1], text3) self.log("changes to text3: " + pformat(list(k._delta(set([0, 1]), text3)))) self.log("k._l=" + pformat(k._l)) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) # now multiple insertions at different places k.add([0, 1, 3], ['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc']) self.assertEqual(k.annotate(4), [(0, 'line 1'), (4, 'aaa'), (3, 'middle line'), (4, 'bbb'), (1, 'line 2'), (4, 'ccc')]) class DeleteLines(TestBase): """Deletion of lines from existing text. Try various texts all based on a common ancestor.""" def runTest(self): k = Weave() base_text = ['one', 'two', 'three', 'four'] k.add([], base_text) texts = [['one', 'two', 'three'], ['two', 'three', 'four'], ['one', 'four'], ['one', 'two', 'three', 'four'], ] for t in texts: ver = k.add([0], t) self.log('final weave:') self.log('k._l=' + pformat(k._l)) for i in range(len(texts)): self.assertEqual(k.get(i+1), texts[i]) class SuicideDelete(TestBase): """Invalid weave which tries to add and delete simultaneously.""" def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = [('{', 0), 'first line', ('[', 0), 'deleted in 0', (']', 0), ('}', 0), ] self.assertRaises(WeaveFormatError, k.get, 0) class CannedDelete(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'last line', ]) class CannedReplacement(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), ('{', 1), 'replacement line', ('}', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'replacement line', 'last line', ]) class BadWeave(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), ] k._l = ['bad line', ('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) class BadInsert(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 1), ' more in 1', ('}', 1), ('}', 1), ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) self.assertRaises(WeaveFormatError, k.get, 1) class InsertNested(TestBase): """Insertion with nested instructions.""" def runTest(self): k = Weave() k._v = [VerInfo([]), VerInfo([0]), VerInfo([0]), VerInfo([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertEqual(k.get(0), ['foo {', '}']) self.assertEqual(k.get(1), ['foo {', ' added in version 1', ' also from v1', '}']) self.assertEqual(k.get(2), ['foo {', ' added in v2', '}']) self.assertEqual(k.get(3), ['foo {', ' added in version 1', ' added in v2', ' also from v1', '}']) class DeleteLines2(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Weave() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) self.assertEqual(k.annotate(1), [(0, "line the first"), (0, "fine")]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a weave with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0])] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1)] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Weave with two diverged texts based on version 0. """ def runTest(self): k = Weave() k._v = [VerInfo(), VerInfo(included=[0]), VerInfo(included=[0]), ] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1), ('{', 2), "alternative second line", ('}', 2), ] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) class ReplaceLine(TestBase): def runTest(self): k = Weave() text0 = ['cheddar', 'stilton', 'gruyere'] text1 = ['cheddar', 'blue vein', 'neufchatel', 'chevre'] k.add([], text0) k.add([0], text1) self.log('k._l=' + pformat(k._l)) self.assertEqual(k.get(0), text0) self.assertEqual(k.get(1), text1) class Merge(TestBase): """Versions that merge diverged parents""" def runTest(self): k = Weave() texts = [['header'], ['header', '', 'line from 1'], ['header', '', 'line from 2', 'more from 2'], ['header', '', 'line from 1', 'fixup line', 'line from 2'], ] k.add([], texts[0]) k.add([0], texts[1]) k.add([0], texts[2]) k.add([0, 1, 2], texts[3]) for i, t in enumerate(texts): self.assertEqual(k.get(i), t) self.assertEqual(k.annotate(3), [(0, 'header'), (1, ''), (1, 'line from 1'), (3, 'fixup line'), (2, 'line from 2'), ]) self.log('k._l=' + pformat(k._l)) from weavefile import write_weave_v1 self.log('weave:') write_weave_v1(k, self.TEST_LOG) class AutoMerge(TestBase): def runTest(self): k = Weave() texts = [['header', 'aaa', 'bbb'], ['header', 'aaa', 'line from 1', 'bbb'], ['header', 'aaa', 'bbb', 'line from 2', 'more from 2'], ] k.add([], texts[0]) k.add([0], texts[1]) k.add([0], texts[2]) self.log('k._l=' + pformat(k._l)) m = list(k.merge_iter([0, 1, 2])) self.assertEqual(m, ['header', 'aaa', 'line from 1', 'bbb', 'line from 2', 'more from 2']) class Khayyam(TestBase): def runTest(self): rawtexts = [ """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, -- and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise enow!""", """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, -- and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now!""", """A Book of poems underneath the tree, A Jug of Wine, a Loaf of Bread, and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now! -- O. Khayyam""", """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now! """, ] texts = [[l.strip() for l in t.split('\n')] for t in rawtexts] k = Weave() parents = set() for t in texts: ver = k.add(parents, t) parents.add(ver) self.log("k._l=" + pformat(k._l)) for i, t in enumerate(texts): self.assertEqual(k.get(i), t) def testweave(): import testsweet from unittest import TestSuite, TestLoader import testweave tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testweave)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testweave()) commit refs/heads/tmp mark :924 committer Martin Pool 1120114485 +1000 data 140 Go back to weave lines normally having newlines at the end. Lines without final newlines are now serialized as ',' lines rather than '.'. from :923 M 644 inline weave.py data 17213 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? # TODO: Separate out some code to read and write weaves. # TODO: End marker for each version? # TODO: Check that no insertion occurs inside a deletion that was # active in the version of the insertion. try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet class VerInfo(object): """Information about a version in a Weave.""" included = frozenset() def __init__(self, included=None): if included: self.included = frozenset(included) def __repr__(self): s = self.__class__.__name__ + '(' if self.included: s += 'included=%r' % (list(self.included)) s += ')' return s class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. To clients the "lines" of the file are represented as a list of strings. These strings will typically have terminal newline characters, but this is not required. In particular files commonly do not have a newline at the end of the file. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. This must normally include the parents and the parent's parents, or wierd things might happen. text Sequence of lines to be added in the new version.""" self._check_versions(parents) self._check_lines(text) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._v.append(VerInfo(parents)) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._v.append(VerInfo()) return idx def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi.included) included.add(index) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = False lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. WFE = WeaveFormatError for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': if istack and (istack[-1] >= v): raise WFE("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WFE("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WFE("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WFE("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WFE("version %d deletes own text on line %d" % (v, lineno)) dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WFE("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WFE("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WFE("literal at top level on line %d" % lineno) isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WFE("unclosed insertion blocks at end of weave", istack) if dset: raise WFE("unclosed deletion blocks at end of weave", dset) def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def merge_iter(self, included): """Return composed version of multiple included versions.""" included = frozenset(included) for origin, lineno, text in self._extract(included): yield text def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise WeaveFormatError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise WeaveFormatError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) # basis a list of (origin, lineno, line) basis = list(self._extract(included)) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (origin, lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((None, len(self._l), None)) # XXX: which line of the weave should we really consider # matches the end of the file? the current code says it's the # last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][1] real_i2 = basis[i2][1] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def main(argv): import sys import os from weavefile import write_weave_v1, read_weave_v1 cmd = argv[1] if cmd == 'add': w = read_weave_v1(file(argv[2], 'rb')) # at the moment, based on everything in the file parents = set(range(len(w._v))) lines = sys.stdin.readlines() ver = w.add(parents, lines) write_weave_v1(w, file(argv[2], 'wb')) print 'added %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() write_weave_v1(w, file(fn, 'wb')) elif cmd == 'get': w = read_weave_v1(file(argv[2], 'rb')) sys.stdout.writelines(w.getiter(int(argv[3]))) elif cmd == 'annotate': w = read_weave_v1(file(argv[2], 'rb')) # newline is added to all lines regardless; too hard to get # reasonable formatting otherwise lasto = None for origin, text in w.annotate(int(argv[3])): text = text.rstrip('\r\n') if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) M 644 inline weavefile.py data 3387 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Store and retrieve weaves in files. There is one format marker followed by a blank line, followed by a series of version headers, followed by the weave itself. Each version marker has 'v' and the version, then 'i' and the included previous versions. The weave is bracketed by 'w' and 'W' lines, and includes the '{}[]' processing instructions. Lines of text are prefixed by '.' if the line contains a newline, or ',' if not. """ # TODO: When extracting a single version it'd be enough to just pass # an iterator returning the weave lines... FORMAT_1 = '# bzr weave file v1' def write_weave_v1(weave, f): """Write weave to file f.""" print >>f, FORMAT_1 print >>f for version, verinfo in enumerate(weave._v): print >>f, 'v', version if verinfo.included: included = list(verinfo.included) included.sort() assert included[0] >= 0 assert included[-1] < version print >>f, 'i', for i in included: print >>f, i, print >>f else: print >>f, 'i' print >>f print >>f, 'w' for l in weave._l: if isinstance(l, tuple): assert len(l) == 2 assert l[0] in '{}[]' print >>f, '%s %d' % l else: # text line if not l: print >>f, ', ' elif l[-1] == '\n': assert '\n' not in l[:-1] print >>f, '.', l, else: print >>f, ',', l print >>f, 'W' def read_weave_v1(f): from weave import Weave, VerInfo w = Weave() assert f.readline() == FORMAT_1+'\n' assert f.readline() == '\n' while True: l = f.readline() if l[0] == 'v': l = f.readline()[:-1] if l[0] != 'i': raise Exception(`l`) if len(l) > 2: included = map(int, l[2:].split(' ')) w._v.append(VerInfo(included)) else: w._v.append(VerInfo()) assert f.readline() == '\n' elif l[0] == 'w': break else: assert 0, l while True: l = f.readline() if l == 'W\n': break elif l[:2] == '. ': w._l.append(l[2:]) # include newline elif l[:2] == ', ': w._l.append(l[2:-1]) # exclude newline else: assert l[0] in '{}[]', l assert l[1] == ' ', l w._l.append((l[0], int(l[2:]))) return w commit refs/heads/tmp mark :925 committer Martin Pool 1120116043 +1000 data 33 Clean up assertions for weavefile from :924 M 644 inline weavefile.py data 3790 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Store and retrieve weaves in files. There is one format marker followed by a blank line, followed by a series of version headers, followed by the weave itself. Each version marker has 'v' and the version, then 'i' and the included previous versions. The weave is bracketed by 'w' and 'W' lines, and includes the '{}[]' processing instructions. Lines of text are prefixed by '.' if the line contains a newline, or ',' if not. """ # TODO: When extracting a single version it'd be enough to just pass # an iterator returning the weave lines... FORMAT_1 = '# bzr weave file v1\n' def write_weave_v1(weave, f): """Write weave to file f.""" print >>f, FORMAT_1, for version, verinfo in enumerate(weave._v): print >>f, 'v', version if verinfo.included: included = list(verinfo.included) included.sort() assert included[0] >= 0 assert included[-1] < version print >>f, 'i', for i in included: print >>f, i, print >>f else: print >>f, 'i' print >>f print >>f, 'w' for l in weave._l: if isinstance(l, tuple): assert l[0] in '{}[]' print >>f, '%s %d' % l else: # text line if not l: print >>f, ', ' elif l[-1] == '\n': assert l.find('\n', 0, -1) == -1 print >>f, '.', l, else: assert l.find('\n') == -1 print >>f, ',', l print >>f, 'W' def read_weave_v1(f): from weave import Weave, VerInfo, WeaveFormatError w = Weave() wfe = WeaveFormatError l = f.readline() if l != FORMAT_1: raise WeaveFormatError('invalid weave file header: %r' % l) v_cnt = 0 while True: l = f.readline() if l.startswith('v '): ver = int(l[2:]) if ver != v_cnt: raise WeaveFormatError('version %d!=%d out of order' % (ver, v_cnt)) v_cnt += 1 l = f.readline()[:-1] if l[0] != 'i': raise WeaveFormatError('unexpected line %r' % l) if len(l) > 2: included = map(int, l[2:].split(' ')) w._v.append(VerInfo(included)) else: w._v.append(VerInfo()) assert f.readline() == '\n' elif l == 'w\n': break else: raise WeaveFormatError('unexpected line %r' % l) while True: l = f.readline() if l == 'W\n': break elif l.startswith('. '): w._l.append(l[2:]) # include newline elif l.startswith(', '): w._l.append(l[2:-1]) # exclude newline else: assert l[0] in '{}[]', l assert l[1] == ' ', l w._l.append((l[0], int(l[2:]))) return w commit refs/heads/tmp mark :926 committer Martin Pool 1120117790 +1000 data 35 Add format-hidden readwrite methods from :925 M 644 inline weavefile.py data 4030 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Store and retrieve weaves in files. There is one format marker followed by a blank line, followed by a series of version headers, followed by the weave itself. Each version marker has 'v' and the version, then 'i' and the included previous versions. The weave is bracketed by 'w' and 'W' lines, and includes the '{}[]' processing instructions. Lines of text are prefixed by '.' if the line contains a newline, or ',' if not. """ # TODO: When extracting a single version it'd be enough to just pass # an iterator returning the weave lines... FORMAT_1 = '# bzr weave file v1\n' def write_weave(weave, f, format=None): if format == None or format == 1: return write_weave_v1(weave, f) else: raise ValueError("unknown weave format %r" % format) def write_weave_v1(weave, f): """Write weave to file f.""" print >>f, FORMAT_1, for version, verinfo in enumerate(weave._v): print >>f, 'v', version if verinfo.included: included = list(verinfo.included) included.sort() assert included[0] >= 0 assert included[-1] < version print >>f, 'i', for i in included: print >>f, i, print >>f else: print >>f, 'i' print >>f print >>f, 'w' for l in weave._l: if isinstance(l, tuple): assert l[0] in '{}[]' print >>f, '%s %d' % l else: # text line if not l: print >>f, ', ' elif l[-1] == '\n': assert l.find('\n', 0, -1) == -1 print >>f, '.', l, else: assert l.find('\n') == -1 print >>f, ',', l print >>f, 'W' def read_weave(f): return read_weave_v1(f) def read_weave_v1(f): from weave import Weave, VerInfo, WeaveFormatError w = Weave() wfe = WeaveFormatError l = f.readline() if l != FORMAT_1: raise WeaveFormatError('invalid weave file header: %r' % l) v_cnt = 0 while True: l = f.readline() if l.startswith('v '): ver = int(l[2:]) if ver != v_cnt: raise WeaveFormatError('version %d!=%d out of order' % (ver, v_cnt)) v_cnt += 1 l = f.readline()[:-1] if l[0] != 'i': raise WeaveFormatError('unexpected line %r' % l) if len(l) > 2: included = map(int, l[2:].split(' ')) w._v.append(VerInfo(included)) else: w._v.append(VerInfo()) assert f.readline() == '\n' elif l == 'w\n': break else: raise WeaveFormatError('unexpected line %r' % l) while True: l = f.readline() if l == 'W\n': break elif l.startswith('. '): w._l.append(l[2:]) # include newline elif l.startswith(', '): w._l.append(l[2:-1]) # exclude newline else: assert l[0] in '{}[]', l assert l[1] == ' ', l w._l.append((l[0], int(l[2:]))) return w commit refs/heads/tmp mark :927 committer Martin Pool 1120118280 +1000 data 111 Remove VerInfo class; just store sets directly in the list of versions. Add tests for serialize/deserialize. from :926 M 644 inline testweave.py data 16940 #! /usr/bin/python2.4 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test suite for weave algorithm""" import testsweet from weave import Weave, WeaveFormatError from pprint import pformat try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class TestBase(testsweet.TestBase): def check_read_write(self, k): """Check the weave k can be written & re-read.""" from tempfile import TemporaryFile from weavefile import write_weave, read_weave tf = TemporaryFile() write_weave(k, tf) tf.seek(0) k2 = read_weave(tf) if k != k2: tf.seek(0) self.log('serialized weave:') self.log(tf.read()) self.fail('read/write check failed') class Easy(TestBase): def runTest(self): k = Weave() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Weave() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class DeltaAdd(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): k = Weave() k.add([], ['line 1']) self.assertEqual(k._l, [('{', 0), 'line 1', ('}', 0), ]) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # currently there are 3 lines in the weave, and we insert after them self.assertEquals(changes, [(3, 3, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(1, 1, ['top line'])]) self.check_read_write(k) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Weave() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): k = Weave() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) text3 = ['line 1', 'middle line', 'line 2'] k.add([0, 1], text3) self.log("changes to text3: " + pformat(list(k._delta(set([0, 1]), text3)))) self.log("k._l=" + pformat(k._l)) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) # now multiple insertions at different places k.add([0, 1, 3], ['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc']) self.assertEqual(k.annotate(4), [(0, 'line 1'), (4, 'aaa'), (3, 'middle line'), (4, 'bbb'), (1, 'line 2'), (4, 'ccc')]) class DeleteLines(TestBase): """Deletion of lines from existing text. Try various texts all based on a common ancestor.""" def runTest(self): k = Weave() base_text = ['one', 'two', 'three', 'four'] k.add([], base_text) texts = [['one', 'two', 'three'], ['two', 'three', 'four'], ['one', 'four'], ['one', 'two', 'three', 'four'], ] for t in texts: ver = k.add([0], t) self.log('final weave:') self.log('k._l=' + pformat(k._l)) for i in range(len(texts)): self.assertEqual(k.get(i+1), texts[i]) class SuicideDelete(TestBase): """Invalid weave which tries to add and delete simultaneously.""" def runTest(self): k = Weave() k._v = [(), ] k._l = [('{', 0), 'first line', ('[', 0), 'deleted in 0', (']', 0), ('}', 0), ] self.assertRaises(WeaveFormatError, k.get, 0) class CannedDelete(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [(), frozenset([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'last line', ]) class CannedReplacement(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), ('{', 1), 'replacement line', ('}', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'replacement line', 'last line', ]) class BadWeave(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [frozenset(), ] k._l = ['bad line', ('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) class BadInsert(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0]), frozenset([0]), frozenset([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 1), ' more in 1', ('}', 1), ('}', 1), ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) self.assertRaises(WeaveFormatError, k.get, 1) class InsertNested(TestBase): """Insertion with nested instructions.""" def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0]), frozenset([0]), frozenset([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertEqual(k.get(0), ['foo {', '}']) self.assertEqual(k.get(1), ['foo {', ' added in version 1', ' also from v1', '}']) self.assertEqual(k.get(2), ['foo {', ' added in v2', '}']) self.assertEqual(k.get(3), ['foo {', ' added in version 1', ' added in v2', ' also from v1', '}']) class DeleteLines2(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Weave() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) self.assertEqual(k.annotate(1), [(0, "line the first"), (0, "fine")]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a weave with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0])] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1)] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Weave with two diverged texts based on version 0. """ def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0]), frozenset([0]), ] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1), ('{', 2), "alternative second line", ('}', 2), ] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) class ReplaceLine(TestBase): def runTest(self): k = Weave() text0 = ['cheddar', 'stilton', 'gruyere'] text1 = ['cheddar', 'blue vein', 'neufchatel', 'chevre'] k.add([], text0) k.add([0], text1) self.log('k._l=' + pformat(k._l)) self.assertEqual(k.get(0), text0) self.assertEqual(k.get(1), text1) class Merge(TestBase): """Versions that merge diverged parents""" def runTest(self): k = Weave() texts = [['header'], ['header', '', 'line from 1'], ['header', '', 'line from 2', 'more from 2'], ['header', '', 'line from 1', 'fixup line', 'line from 2'], ] k.add([], texts[0]) k.add([0], texts[1]) k.add([0], texts[2]) k.add([0, 1, 2], texts[3]) for i, t in enumerate(texts): self.assertEqual(k.get(i), t) self.assertEqual(k.annotate(3), [(0, 'header'), (1, ''), (1, 'line from 1'), (3, 'fixup line'), (2, 'line from 2'), ]) self.log('k._l=' + pformat(k._l)) self.check_read_write(k) class AutoMerge(TestBase): def runTest(self): k = Weave() texts = [['header', 'aaa', 'bbb'], ['header', 'aaa', 'line from 1', 'bbb'], ['header', 'aaa', 'bbb', 'line from 2', 'more from 2'], ] k.add([], texts[0]) k.add([0], texts[1]) k.add([0], texts[2]) self.log('k._l=' + pformat(k._l)) m = list(k.merge_iter([0, 1, 2])) self.assertEqual(m, ['header', 'aaa', 'line from 1', 'bbb', 'line from 2', 'more from 2']) class Khayyam(TestBase): """Test changes to multi-line texts, and read/write""" def runTest(self): rawtexts = [ """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, -- and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise enow!""", """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, -- and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now!""", """A Book of poems underneath the tree, A Jug of Wine, a Loaf of Bread, and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now! -- O. Khayyam""", """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now!""", ] texts = [[l.strip() for l in t.split('\n')] for t in rawtexts] k = Weave() parents = set() for t in texts: ver = k.add(parents, t) parents.add(ver) self.log("k._l=" + pformat(k._l)) for i, t in enumerate(texts): self.assertEqual(k.get(i), t) self.check_read_write(k) def testweave(): import testsweet from unittest import TestSuite, TestLoader import testweave tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testweave)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testweave()) M 644 inline weave.py data 16961 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? # TODO: Separate out some code to read and write weaves. # TODO: End marker for each version? # TODO: Check that no insertion occurs inside a deletion that was # active in the version of the insertion. try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. To clients the "lines" of the file are represented as a list of strings. These strings will typically have terminal newline characters, but this is not required. In particular files commonly do not have a newline at the end of the file. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the tuple (included_versions), which lists the previous versions also considered active. """ def __init__(self): self._l = [] self._v = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. This must normally include the parents and the parent's parents, or wierd things might happen. text Sequence of lines to be added in the new version.""" self._check_versions(parents) self._check_lines(text) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._addversion(parents) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._addversion(None) return idx def _addversion(self, parents): if parents: self._v.append(frozenset(parents)) else: self._v.append(frozenset()) def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi) included.add(index) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = False lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. WFE = WeaveFormatError for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': if istack and (istack[-1] >= v): raise WFE("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WFE("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WFE("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WFE("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WFE("version %d deletes own text on line %d" % (v, lineno)) dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WFE("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WFE("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WFE("literal at top level on line %d" % lineno) isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WFE("unclosed insertion blocks at end of weave", istack) if dset: raise WFE("unclosed deletion blocks at end of weave", dset) def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def merge_iter(self, included): """Return composed version of multiple included versions.""" included = frozenset(included) for origin, lineno, text in self._extract(included): yield text def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise WeaveFormatError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise WeaveFormatError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) # basis a list of (origin, lineno, line) basis = list(self._extract(included)) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (origin, lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((None, len(self._l), None)) # XXX: which line of the weave should we really consider # matches the end of the file? the current code says it's the # last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][1] real_i2 = basis[i2][1] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def main(argv): import sys import os from weavefile import write_weave_v1, read_weave_v1 cmd = argv[1] if cmd == 'add': w = read_weave_v1(file(argv[2], 'rb')) # at the moment, based on everything in the file parents = set(range(len(w._v))) lines = sys.stdin.readlines() ver = w.add(parents, lines) write_weave_v1(w, file(argv[2], 'wb')) print 'added %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() write_weave_v1(w, file(fn, 'wb')) elif cmd == 'get': w = read_weave_v1(file(argv[2], 'rb')) sys.stdout.writelines(w.getiter(int(argv[3]))) elif cmd == 'annotate': w = read_weave_v1(file(argv[2], 'rb')) # newline is added to all lines regardless; too hard to get # reasonable formatting otherwise lasto = None for origin, text in w.annotate(int(argv[3])): text = text.rstrip('\r\n') if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) M 644 inline weavefile.py data 3996 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Store and retrieve weaves in files. There is one format marker followed by a blank line, followed by a series of version headers, followed by the weave itself. Each version marker has 'v' and the version, then 'i' and the included previous versions. The weave is bracketed by 'w' and 'W' lines, and includes the '{}[]' processing instructions. Lines of text are prefixed by '.' if the line contains a newline, or ',' if not. """ # TODO: When extracting a single version it'd be enough to just pass # an iterator returning the weave lines... FORMAT_1 = '# bzr weave file v1\n' def write_weave(weave, f, format=None): if format == None or format == 1: return write_weave_v1(weave, f) else: raise ValueError("unknown weave format %r" % format) def write_weave_v1(weave, f): """Write weave to file f.""" print >>f, FORMAT_1, for version, included in enumerate(weave._v): print >>f, 'v', version if included: included = list(included) included.sort() assert included[0] >= 0 assert included[-1] < version print >>f, 'i', for i in included: print >>f, i, print >>f else: print >>f, 'i' print >>f print >>f, 'w' for l in weave._l: if isinstance(l, tuple): assert l[0] in '{}[]' print >>f, '%s %d' % l else: # text line if not l: print >>f, ', ' elif l[-1] == '\n': assert l.find('\n', 0, -1) == -1 print >>f, '.', l, else: assert l.find('\n') == -1 print >>f, ',', l print >>f, 'W' def read_weave(f): return read_weave_v1(f) def read_weave_v1(f): from weave import Weave, WeaveFormatError w = Weave() wfe = WeaveFormatError l = f.readline() if l != FORMAT_1: raise WeaveFormatError('invalid weave file header: %r' % l) v_cnt = 0 while True: l = f.readline() if l.startswith('v '): ver = int(l[2:]) if ver != v_cnt: raise WeaveFormatError('version %d!=%d out of order' % (ver, v_cnt)) v_cnt += 1 l = f.readline()[:-1] if l[0] != 'i': raise WeaveFormatError('unexpected line %r' % l) if len(l) > 2: included = map(int, l[2:].split(' ')) w._addversion(included) else: w._addversion(None) assert f.readline() == '\n' elif l == 'w\n': break else: raise WeaveFormatError('unexpected line %r' % l) while True: l = f.readline() if l == 'W\n': break elif l.startswith('. '): w._l.append(l[2:]) # include newline elif l.startswith(', '): w._l.append(l[2:-1]) # exclude newline else: assert l[0] in '{}[]', l assert l[1] == ' ', l w._l.append((l[0], int(l[2:]))) return w commit refs/heads/tmp mark :928 committer Martin Pool 1120119071 +1000 data 63 Script that tries conversion from bzr inventory into weave file from :927 M 644 inline tryconvert.py data 1258 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Experiment in converting existing bzr branches to weaves.""" import bzrlib.branch from weave import Weave from weavefile import write_weave WEAVE_NAME = "inventory.weave" wf = Weave() b = bzrlib.branch.find_branch('.') print 'converting...' parents = set() revno = 1 for rev_id in b.revision_history(): print revno inv_xml = b.inventory_store[rev_id].readlines() weave_id = wf.add(parents, inv_xml) parents.add(weave_id) revno += 1 write_weave(wf, file(WEAVE_NAME, 'wb')) commit refs/heads/tmp mark :929 committer Martin Pool 1120119438 +1000 data 50 New Weave.get_included() does transitive expansion from :928 M 644 inline testweave.py data 17112 #! /usr/bin/python2.4 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test suite for weave algorithm""" import testsweet from weave import Weave, WeaveFormatError from pprint import pformat try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class TestBase(testsweet.TestBase): def check_read_write(self, k): """Check the weave k can be written & re-read.""" from tempfile import TemporaryFile from weavefile import write_weave, read_weave tf = TemporaryFile() write_weave(k, tf) tf.seek(0) k2 = read_weave(tf) if k != k2: tf.seek(0) self.log('serialized weave:') self.log(tf.read()) self.fail('read/write check failed') class Easy(TestBase): def runTest(self): k = Weave() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Weave() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class DeltaAdd(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): k = Weave() k.add([], ['line 1']) self.assertEqual(k._l, [('{', 0), 'line 1', ('}', 0), ]) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # currently there are 3 lines in the weave, and we insert after them self.assertEquals(changes, [(3, 3, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(1, 1, ['top line'])]) self.check_read_write(k) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Weave() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): k = Weave() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) text3 = ['line 1', 'middle line', 'line 2'] k.add([0, 1], text3) self.log("changes to text3: " + pformat(list(k._delta(set([0, 1]), text3)))) self.log("k._l=" + pformat(k._l)) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) # now multiple insertions at different places k.add([0, 1, 3], ['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc']) self.assertEqual(k.annotate(4), [(0, 'line 1'), (4, 'aaa'), (3, 'middle line'), (4, 'bbb'), (1, 'line 2'), (4, 'ccc')]) class DeleteLines(TestBase): """Deletion of lines from existing text. Try various texts all based on a common ancestor.""" def runTest(self): k = Weave() base_text = ['one', 'two', 'three', 'four'] k.add([], base_text) texts = [['one', 'two', 'three'], ['two', 'three', 'four'], ['one', 'four'], ['one', 'two', 'three', 'four'], ] for t in texts: ver = k.add([0], t) self.log('final weave:') self.log('k._l=' + pformat(k._l)) for i in range(len(texts)): self.assertEqual(k.get(i+1), texts[i]) class SuicideDelete(TestBase): """Invalid weave which tries to add and delete simultaneously.""" def runTest(self): k = Weave() k._v = [(), ] k._l = [('{', 0), 'first line', ('[', 0), 'deleted in 0', (']', 0), ('}', 0), ] self.assertRaises(WeaveFormatError, k.get, 0) class CannedDelete(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [(), frozenset([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'last line', ]) class CannedReplacement(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), ('{', 1), 'replacement line', ('}', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'replacement line', 'last line', ]) class BadWeave(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [frozenset(), ] k._l = ['bad line', ('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) class BadInsert(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0]), frozenset([0]), frozenset([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 1), ' more in 1', ('}', 1), ('}', 1), ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) self.assertRaises(WeaveFormatError, k.get, 1) class InsertNested(TestBase): """Insertion with nested instructions.""" def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0]), frozenset([0]), frozenset([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertEqual(k.get(0), ['foo {', '}']) self.assertEqual(k.get(1), ['foo {', ' added in version 1', ' also from v1', '}']) self.assertEqual(k.get(2), ['foo {', ' added in v2', '}']) self.assertEqual(k.get(3), ['foo {', ' added in version 1', ' added in v2', ' also from v1', '}']) class DeleteLines2(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Weave() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) self.assertEqual(k.annotate(1), [(0, "line the first"), (0, "fine")]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a weave with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0])] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1)] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Weave with two diverged texts based on version 0. """ def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0]), frozenset([0]), ] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1), ('{', 2), "alternative second line", ('}', 2), ] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) self.assertEqual(k.get_included(2), set([0, 2])) class ReplaceLine(TestBase): def runTest(self): k = Weave() text0 = ['cheddar', 'stilton', 'gruyere'] text1 = ['cheddar', 'blue vein', 'neufchatel', 'chevre'] k.add([], text0) k.add([0], text1) self.log('k._l=' + pformat(k._l)) self.assertEqual(k.get(0), text0) self.assertEqual(k.get(1), text1) class Merge(TestBase): """Versions that merge diverged parents""" def runTest(self): k = Weave() texts = [['header'], ['header', '', 'line from 1'], ['header', '', 'line from 2', 'more from 2'], ['header', '', 'line from 1', 'fixup line', 'line from 2'], ] k.add([], texts[0]) k.add([0], texts[1]) k.add([0], texts[2]) k.add([0, 1, 2], texts[3]) for i, t in enumerate(texts): self.assertEqual(k.get(i), t) self.assertEqual(k.annotate(3), [(0, 'header'), (1, ''), (1, 'line from 1'), (3, 'fixup line'), (2, 'line from 2'), ]) self.assertEqual(k.get_included(3), set([0, 1, 2, 3])) self.log('k._l=' + pformat(k._l)) self.check_read_write(k) class AutoMerge(TestBase): def runTest(self): k = Weave() texts = [['header', 'aaa', 'bbb'], ['header', 'aaa', 'line from 1', 'bbb'], ['header', 'aaa', 'bbb', 'line from 2', 'more from 2'], ] k.add([], texts[0]) k.add([0], texts[1]) k.add([0], texts[2]) self.log('k._l=' + pformat(k._l)) m = list(k.merge_iter([0, 1, 2])) self.assertEqual(m, ['header', 'aaa', 'line from 1', 'bbb', 'line from 2', 'more from 2']) class Khayyam(TestBase): """Test changes to multi-line texts, and read/write""" def runTest(self): rawtexts = [ """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, -- and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise enow!""", """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, -- and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now!""", """A Book of poems underneath the tree, A Jug of Wine, a Loaf of Bread, and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now! -- O. Khayyam""", """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now!""", ] texts = [[l.strip() for l in t.split('\n')] for t in rawtexts] k = Weave() parents = set() for t in texts: ver = k.add(parents, t) parents.add(ver) self.log("k._l=" + pformat(k._l)) for i, t in enumerate(texts): self.assertEqual(k.get(i), t) self.check_read_write(k) def testweave(): import testsweet from unittest import TestSuite, TestLoader import testweave tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testweave)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testweave()) M 644 inline weave.py data 17481 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? # TODO: Separate out some code to read and write weaves. # TODO: End marker for each version? # TODO: Check that no insertion occurs inside a deletion that was # active in the version of the insertion. try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. To clients the "lines" of the file are represented as a list of strings. These strings will typically have terminal newline characters, but this is not required. In particular files commonly do not have a newline at the end of the file. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the set (included_versions), which lists the previous versions also considered active; the versions included in those versions are included transitively. So new versions created from nothing list []; most versions have a single entry; some have more. """ def __init__(self): self._l = [] self._v = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. This must normally include the parents and the parent's parents, or wierd things might happen. text Sequence of lines to be added in the new version.""" self._check_versions(parents) self._check_lines(text) idx = len(self._v) if parents: parents = frozenset(parents) delta = self._delta(parents, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._addversion(parents) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._addversion(None) return idx def get_included(self, version): """Return a set with all included versions for version.""" i = set() x = [version] while x: v = x.pop() if v in i: continue i.add(v) for u in self._v[v]: x.append(u) return i def _addversion(self, parents): if parents: self._v.append(frozenset(parents)) else: self._v.append(frozenset()) def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, index): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" try: vi = self._v[index] except IndexError: raise IndexError('version index %d out of range' % index) included = set(vi) included.add(index) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = False lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. WFE = WeaveFormatError for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': if istack and (istack[-1] >= v): raise WFE("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WFE("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WFE("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WFE("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WFE("version %d deletes own text on line %d" % (v, lineno)) dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WFE("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WFE("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WFE("literal at top level on line %d" % lineno) isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WFE("unclosed insertion blocks at end of weave", istack) if dset: raise WFE("unclosed deletion blocks at end of weave", dset) def getiter(self, index): """Yield lines for the specified version.""" for origin, line in self.annotate_iter(index): yield line def get(self, index): return list(self.getiter(index)) def merge_iter(self, included): """Return composed version of multiple included versions.""" included = frozenset(included) for origin, lineno, text in self._extract(included): yield text def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise WeaveFormatError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise WeaveFormatError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) # basis a list of (origin, lineno, line) basis = list(self._extract(included)) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (origin, lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((None, len(self._l), None)) # XXX: which line of the weave should we really consider # matches the end of the file? the current code says it's the # last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][1] real_i2 = basis[i2][1] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def main(argv): import sys import os from weavefile import write_weave_v1, read_weave_v1 cmd = argv[1] if cmd == 'add': w = read_weave_v1(file(argv[2], 'rb')) # at the moment, based on everything in the file parents = set(range(len(w._v))) lines = sys.stdin.readlines() ver = w.add(parents, lines) write_weave_v1(w, file(argv[2], 'wb')) print 'added %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() write_weave_v1(w, file(fn, 'wb')) elif cmd == 'get': w = read_weave_v1(file(argv[2], 'rb')) sys.stdout.writelines(w.getiter(int(argv[3]))) elif cmd == 'annotate': w = read_weave_v1(file(argv[2], 'rb')) # newline is added to all lines regardless; too hard to get # reasonable formatting otherwise lasto = None for origin, text in w.annotate(int(argv[3])): text = text.rstrip('\r\n') if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) commit refs/heads/tmp mark :930 committer Martin Pool 1120120859 +1000 data 91 Rename Weave.get_included to inclusions and getiter to get_iter Refactor annotate() code from :929 M 644 inline testweave.py data 17118 #! /usr/bin/python2.4 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test suite for weave algorithm""" import testsweet from weave import Weave, WeaveFormatError from pprint import pformat try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class TestBase(testsweet.TestBase): def check_read_write(self, k): """Check the weave k can be written & re-read.""" from tempfile import TemporaryFile from weavefile import write_weave, read_weave tf = TemporaryFile() write_weave(k, tf) tf.seek(0) k2 = read_weave(tf) if k != k2: tf.seek(0) self.log('serialized weave:') self.log(tf.read()) self.fail('read/write check failed') class Easy(TestBase): def runTest(self): k = Weave() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Weave() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class DeltaAdd(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): k = Weave() k.add([], ['line 1']) self.assertEqual(k._l, [('{', 0), 'line 1', ('}', 0), ]) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # currently there are 3 lines in the weave, and we insert after them self.assertEquals(changes, [(3, 3, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(1, 1, ['top line'])]) self.check_read_write(k) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Weave() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): k = Weave() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) text3 = ['line 1', 'middle line', 'line 2'] k.add([0, 1], text3) self.log("changes to text3: " + pformat(list(k._delta(set([0, 1]), text3)))) self.log("k._l=" + pformat(k._l)) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) # now multiple insertions at different places k.add([0, 1, 3], ['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc']) self.assertEqual(k.annotate(4), [(0, 'line 1'), (4, 'aaa'), (3, 'middle line'), (4, 'bbb'), (1, 'line 2'), (4, 'ccc')]) class DeleteLines(TestBase): """Deletion of lines from existing text. Try various texts all based on a common ancestor.""" def runTest(self): k = Weave() base_text = ['one', 'two', 'three', 'four'] k.add([], base_text) texts = [['one', 'two', 'three'], ['two', 'three', 'four'], ['one', 'four'], ['one', 'two', 'three', 'four'], ] for t in texts: ver = k.add([0], t) self.log('final weave:') self.log('k._l=' + pformat(k._l)) for i in range(len(texts)): self.assertEqual(k.get(i+1), texts[i]) class SuicideDelete(TestBase): """Invalid weave which tries to add and delete simultaneously.""" def runTest(self): k = Weave() k._v = [(), ] k._l = [('{', 0), 'first line', ('[', 0), 'deleted in 0', (']', 0), ('}', 0), ] self.assertRaises(WeaveFormatError, k.get, 0) class CannedDelete(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [(), frozenset([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'last line', ]) class CannedReplacement(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), ('{', 1), 'replacement line', ('}', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'replacement line', 'last line', ]) class BadWeave(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [frozenset(), ] k._l = ['bad line', ('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) class BadInsert(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0]), frozenset([0]), frozenset([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 1), ' more in 1', ('}', 1), ('}', 1), ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) self.assertRaises(WeaveFormatError, k.get, 1) class InsertNested(TestBase): """Insertion with nested instructions.""" def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0]), frozenset([0]), frozenset([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertEqual(k.get(0), ['foo {', '}']) self.assertEqual(k.get(1), ['foo {', ' added in version 1', ' also from v1', '}']) self.assertEqual(k.get(2), ['foo {', ' added in v2', '}']) self.assertEqual(k.get(3), ['foo {', ' added in version 1', ' added in v2', ' also from v1', '}']) class DeleteLines2(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Weave() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) self.assertEqual(k.annotate(1), [(0, "line the first"), (0, "fine")]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a weave with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0])] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1)] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Weave with two diverged texts based on version 0. """ def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0]), frozenset([0]), ] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1), ('{', 2), "alternative second line", ('}', 2), ] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) self.assertEqual(k.inclusions([2]), set([0, 2])) class ReplaceLine(TestBase): def runTest(self): k = Weave() text0 = ['cheddar', 'stilton', 'gruyere'] text1 = ['cheddar', 'blue vein', 'neufchatel', 'chevre'] k.add([], text0) k.add([0], text1) self.log('k._l=' + pformat(k._l)) self.assertEqual(k.get(0), text0) self.assertEqual(k.get(1), text1) class Merge(TestBase): """Versions that merge diverged parents""" def runTest(self): k = Weave() texts = [['header'], ['header', '', 'line from 1'], ['header', '', 'line from 2', 'more from 2'], ['header', '', 'line from 1', 'fixup line', 'line from 2'], ] k.add([], texts[0]) k.add([0], texts[1]) k.add([0], texts[2]) k.add([0, 1, 2], texts[3]) for i, t in enumerate(texts): self.assertEqual(k.get(i), t) self.assertEqual(k.annotate(3), [(0, 'header'), (1, ''), (1, 'line from 1'), (3, 'fixup line'), (2, 'line from 2'), ]) self.assertEqual(k.inclusions([3]), set([0, 1, 2, 3])) self.log('k._l=' + pformat(k._l)) self.check_read_write(k) class AutoMerge(TestBase): def runTest(self): k = Weave() texts = [['header', 'aaa', 'bbb'], ['header', 'aaa', 'line from 1', 'bbb'], ['header', 'aaa', 'bbb', 'line from 2', 'more from 2'], ] k.add([], texts[0]) k.add([0], texts[1]) k.add([0], texts[2]) self.log('k._l=' + pformat(k._l)) m = list(k.merge_iter([0, 1, 2])) self.assertEqual(m, ['header', 'aaa', 'line from 1', 'bbb', 'line from 2', 'more from 2']) class Khayyam(TestBase): """Test changes to multi-line texts, and read/write""" def runTest(self): rawtexts = [ """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, -- and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise enow!""", """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, -- and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now!""", """A Book of poems underneath the tree, A Jug of Wine, a Loaf of Bread, and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now! -- O. Khayyam""", """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now!""", ] texts = [[l.strip() for l in t.split('\n')] for t in rawtexts] k = Weave() parents = set() for t in texts: ver = k.add(list(parents), t) parents.add(ver) self.log("k._l=" + pformat(k._l)) for i, t in enumerate(texts): self.assertEqual(k.get(i), t) self.check_read_write(k) def testweave(): import testsweet from unittest import TestSuite, TestLoader import testweave tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testweave)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testweave()) M 644 inline weave.py data 17315 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? # TODO: Separate out some code to read and write weaves. # TODO: End marker for each version? # TODO: Check that no insertion occurs inside a deletion that was # active in the version of the insertion. try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. To clients the "lines" of the file are represented as a list of strings. These strings will typically have terminal newline characters, but this is not required. In particular files commonly do not have a newline at the end of the file. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the set (included_versions), which lists the previous versions also considered active; the versions included in those versions are included transitively. So new versions created from nothing list []; most versions have a single entry; some have more. """ def __init__(self): self._l = [] self._v = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. This must normally include the parents and the parent's parents, or wierd things might happen. text Sequence of lines to be added in the new version.""" self._check_versions(parents) self._check_lines(text) idx = len(self._v) if parents: delta = self._delta(self.inclusions(parents), text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) # TODO: Could eliminate any parents that are implied by # the others self._addversion(parents) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._addversion(None) return idx def inclusions(self, versions): """Expand out everything included by versions.""" i = set(versions) for v in versions: i.update(self._v[v]) return i def _addversion(self, parents): if parents: self._v.append(frozenset(parents)) else: self._v.append(frozenset()) def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, version): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" included = self.inclusions([version]) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = False lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. WFE = WeaveFormatError for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': if istack and (istack[-1] >= v): raise WFE("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WFE("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WFE("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WFE("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WFE("version %d deletes own text on line %d" % (v, lineno)) dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WFE("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WFE("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WFE("literal at top level on line %d" % lineno) isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WFE("unclosed insertion blocks at end of weave", istack) if dset: raise WFE("unclosed deletion blocks at end of weave", dset) def get_iter(self, version): """Yield lines for the specified version.""" for origin, lineno, line in self._extract(self.inclusions([version])): yield line def get(self, index): return list(self.get_iter(index)) def merge_iter(self, included): """Return composed version of multiple included versions.""" included = frozenset(included) for origin, lineno, text in self._extract(included): yield text def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise WeaveFormatError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise WeaveFormatError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) # basis a list of (origin, lineno, line) basis = list(self._extract(included)) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (origin, lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((None, len(self._l), None)) # XXX: which line of the weave should we really consider # matches the end of the file? the current code says it's the # last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][1] real_i2 = basis[i2][1] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def main(argv): import sys import os from weavefile import write_weave_v1, read_weave_v1 cmd = argv[1] if cmd == 'add': w = read_weave_v1(file(argv[2], 'rb')) # at the moment, based on everything in the file parents = set(range(len(w._v))) lines = sys.stdin.readlines() ver = w.add(parents, lines) write_weave_v1(w, file(argv[2], 'wb')) print 'added %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() write_weave_v1(w, file(fn, 'wb')) elif cmd == 'get': w = read_weave_v1(file(argv[2], 'rb')) sys.stdout.writelines(w.getiter(int(argv[3]))) elif cmd == 'annotate': w = read_weave_v1(file(argv[2], 'rb')) # newline is added to all lines regardless; too hard to get # reasonable formatting otherwise lasto = None for origin, text in w.annotate(int(argv[3])): text = text.rstrip('\r\n') if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) commit refs/heads/tmp mark :931 committer Martin Pool 1120121558 +1000 data 88 In the weavefile, store only the minimum revisions added, not the full list of parents. from :930 M 644 inline weavefile.py data 4502 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Store and retrieve weaves in files. There is one format marker followed by a blank line, followed by a series of version headers, followed by the weave itself. Each version marker has 'v' and the version, then 'i' and the included previous versions. The inclusions do not need to list versions included by a parent. The weave is bracketed by 'w' and 'W' lines, and includes the '{}[]' processing instructions. Lines of text are prefixed by '.' if the line contains a newline, or ',' if not. """ # TODO: When extracting a single version it'd be enough to just pass # an iterator returning the weave lines... FORMAT_1 = '# bzr weave file v1\n' def write_weave(weave, f, format=None): if format == None or format == 1: return write_weave_v1(weave, f) else: raise ValueError("unknown weave format %r" % format) def write_weave_v1(weave, f): """Write weave to file f.""" print >>f, FORMAT_1, for version, included in enumerate(weave._v): print >>f, 'v', version if included: # find a minimal expression of it; bias towards using # later revisions li = list(included) li.sort() li.reverse() mininc = [] gotit = set() for pv in li: if pv not in gotit: mininc.append(pv) gotit.update(weave._v[pv]) assert mininc[0] >= 0 assert mininc[-1] < version print >>f, 'i', for i in mininc: print >>f, i, print >>f else: print >>f, 'i' print >>f print >>f, 'w' for l in weave._l: if isinstance(l, tuple): assert l[0] in '{}[]' print >>f, '%s %d' % l else: # text line if not l: print >>f, ', ' elif l[-1] == '\n': assert l.find('\n', 0, -1) == -1 print >>f, '.', l, else: assert l.find('\n') == -1 print >>f, ',', l print >>f, 'W' def read_weave(f): return read_weave_v1(f) def read_weave_v1(f): from weave import Weave, WeaveFormatError w = Weave() wfe = WeaveFormatError l = f.readline() if l != FORMAT_1: raise WeaveFormatError('invalid weave file header: %r' % l) v_cnt = 0 while True: l = f.readline() if l.startswith('v '): ver = int(l[2:]) if ver != v_cnt: raise WeaveFormatError('version %d!=%d out of order' % (ver, v_cnt)) v_cnt += 1 l = f.readline()[:-1] if l[0] != 'i': raise WeaveFormatError('unexpected line %r' % l) if len(l) > 2: included = map(int, l[2:].split(' ')) full = set() for pv in included: full.add(pv) full.update(w._v[pv]) w._addversion(full) else: w._addversion(None) assert f.readline() == '\n' elif l == 'w\n': break else: raise WeaveFormatError('unexpected line %r' % l) while True: l = f.readline() if l == 'W\n': break elif l.startswith('. '): w._l.append(l[2:]) # include newline elif l.startswith(', '): w._l.append(l[2:-1]) # exclude newline else: assert l[0] in '{}[]', l assert l[1] == ' ', l w._l.append((l[0], int(l[2:]))) return w commit refs/heads/tmp mark :932 committer Martin Pool 1120123570 +1000 data 52 Add test code to convert from old storage to a weave from :931 R tryconvert.py convertinv.py M 644 inline convertfile.py data 1973 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Experiment in converting existing bzr branches to weaves.""" import sys import bzrlib.branch from weave import Weave from weavefile import write_weave import hotshot import tempfile def convert(): WEAVE_NAME = "test.weave" wf = Weave() b = bzrlib.branch.find_branch('.') print 'converting...' fid = b.read_working_inventory().path2id(sys.argv[1]) parents = set() revno = 1 for rev_id in b.revision_history(): print revno tree = b.revision_tree(rev_id) inv = tree.inventory if fid not in tree: print ' (not present)' continue text = tree.get_file(fid).readlines() weave_id = wf.add(parents, text) parents.add(weave_id) revno += 1 print ' %4d lines' % len(text) write_weave(wf, file(WEAVE_NAME, 'wb')) prof_f = tempfile.NamedTemporaryFile() prof = hotshot.Profile(prof_f.name) prof.runcall(convert) prof.close() import hotshot.stats stats = hotshot.stats.load(prof_f.name) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) commit refs/heads/tmp mark :933 committer Martin Pool 1120124294 +1000 data 85 Avoid re-encoding versions which have not changed when converting from old bzr stores from :932 M 644 inline convertfile.py data 2083 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Experiment in converting existing bzr branches to weaves.""" import sys import bzrlib.branch from weave import Weave from weavefile import write_weave import hotshot import tempfile def convert(): WEAVE_NAME = "test.weave" wf = Weave() b = bzrlib.branch.find_branch('.') print 'converting...' fid = b.read_working_inventory().path2id(sys.argv[1]) last_lines = None parents = set() revno = 0 for rev_id in b.revision_history(): revno += 1 print revno tree = b.revision_tree(rev_id) inv = tree.inventory if fid not in tree: print ' (not present)' continue text = tree.get_file(fid).readlines() if text == last_lines: continue last_lines = text weave_id = wf.add(parents, text) parents.add(weave_id) print ' %4d lines' % len(text) write_weave(wf, file(WEAVE_NAME, 'wb')) prof_f = tempfile.NamedTemporaryFile() prof = hotshot.Profile(prof_f.name) prof.runcall(convert) prof.close() import hotshot.stats stats = hotshot.stats.load(prof_f.name) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) commit refs/heads/tmp mark :934 committer Martin Pool 1120124338 +1000 data 25 Small weave optimizations from :933 M 644 inline weave.py data 17422 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? # TODO: Separate out some code to read and write weaves. # TODO: End marker for each version? # TODO: Check that no insertion occurs inside a deletion that was # active in the version of the insertion. try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. To clients the "lines" of the file are represented as a list of strings. These strings will typically have terminal newline characters, but this is not required. In particular files commonly do not have a newline at the end of the file. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the set (included_versions), which lists the previous versions also considered active; the versions included in those versions are included transitively. So new versions created from nothing list []; most versions have a single entry; some have more. """ def __init__(self): self._l = [] self._v = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. This must normally include the parents and the parent's parents, or wierd things might happen. text Sequence of lines to be added in the new version.""" ## self._check_versions(parents) ## self._check_lines(text) idx = len(self._v) if parents: delta = self._delta(self.inclusions(parents), text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) # TODO: Could eliminate any parents that are implied by # the others self._addversion(parents) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._addversion(None) return idx def inclusions(self, versions): """Expand out everything included by versions.""" i = set(versions) for v in versions: i.update(self._v[v]) return i def _addversion(self, parents): if parents: self._v.append(frozenset(parents)) else: self._v.append(frozenset()) def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, version): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" included = self.inclusions([version]) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = None lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. WFE = WeaveFormatError for l in self._l: if isinstance(l, tuple): isactive = None # recalculate c, v = l if c == '{': if istack and (istack[-1] >= v): raise WFE("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WFE("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WFE("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WFE("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WFE("version %d deletes own text on line %d" % (v, lineno)) dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WFE("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WFE("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WFE("literal at top level on line %d" % lineno) if isactive == None: isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WFE("unclosed insertion blocks at end of weave", istack) if dset: raise WFE("unclosed deletion blocks at end of weave", dset) def get_iter(self, version): """Yield lines for the specified version.""" for origin, lineno, line in self._extract(self.inclusions([version])): yield line def get(self, index): return list(self.get_iter(index)) def merge_iter(self, included): """Return composed version of multiple included versions.""" included = frozenset(included) for origin, lineno, text in self._extract(included): yield text def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise WeaveFormatError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise WeaveFormatError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ ## self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) # basis a list of (origin, lineno, line) basis = list(self._extract(included)) # now make a parallel list with only the text, to pass to the differ basis_lines = [line for (origin, lineno, line) in basis] # add a sentinal, because we can also match against the final line basis.append((None, len(self._l), None)) # XXX: which line of the weave should we really consider # matches the end of the file? the current code says it's the # last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][1] real_i2 = basis[i2][1] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def main(argv): import sys import os from weavefile import write_weave_v1, read_weave_v1 cmd = argv[1] if cmd == 'add': w = read_weave_v1(file(argv[2], 'rb')) # at the moment, based on everything in the file parents = set(range(len(w._v))) lines = sys.stdin.readlines() ver = w.add(parents, lines) write_weave_v1(w, file(argv[2], 'wb')) print 'added %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() write_weave_v1(w, file(fn, 'wb')) elif cmd == 'get': w = read_weave_v1(file(argv[2], 'rb')) sys.stdout.writelines(w.getiter(int(argv[3]))) elif cmd == 'annotate': w = read_weave_v1(file(argv[2], 'rb')) # newline is added to all lines regardless; too hard to get # reasonable formatting otherwise lasto = None for origin, text in w.annotate(int(argv[3])): text = text.rstrip('\r\n') if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) commit refs/heads/tmp mark :935 committer Martin Pool 1120124441 +1000 data 30 Better delta basis calculation from :934 M 644 inline weave.py data 17384 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? # TODO: Separate out some code to read and write weaves. # TODO: End marker for each version? # TODO: Check that no insertion occurs inside a deletion that was # active in the version of the insertion. try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. To clients the "lines" of the file are represented as a list of strings. These strings will typically have terminal newline characters, but this is not required. In particular files commonly do not have a newline at the end of the file. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the set (included_versions), which lists the previous versions also considered active; the versions included in those versions are included transitively. So new versions created from nothing list []; most versions have a single entry; some have more. """ def __init__(self): self._l = [] self._v = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. This must normally include the parents and the parent's parents, or wierd things might happen. text Sequence of lines to be added in the new version.""" ## self._check_versions(parents) ## self._check_lines(text) idx = len(self._v) if parents: delta = self._delta(self.inclusions(parents), text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) # TODO: Could eliminate any parents that are implied by # the others self._addversion(parents) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._addversion(None) return idx def inclusions(self, versions): """Expand out everything included by versions.""" i = set(versions) for v in versions: i.update(self._v[v]) return i def _addversion(self, parents): if parents: self._v.append(frozenset(parents)) else: self._v.append(frozenset()) def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, version): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" included = self.inclusions([version]) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = None lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. WFE = WeaveFormatError for l in self._l: if isinstance(l, tuple): isactive = None # recalculate c, v = l if c == '{': if istack and (istack[-1] >= v): raise WFE("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WFE("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WFE("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WFE("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WFE("version %d deletes own text on line %d" % (v, lineno)) dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WFE("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WFE("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WFE("literal at top level on line %d" % lineno) if isactive == None: isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WFE("unclosed insertion blocks at end of weave", istack) if dset: raise WFE("unclosed deletion blocks at end of weave", dset) def get_iter(self, version): """Yield lines for the specified version.""" for origin, lineno, line in self._extract(self.inclusions([version])): yield line def get(self, index): return list(self.get_iter(index)) def merge_iter(self, included): """Return composed version of multiple included versions.""" included = frozenset(included) for origin, lineno, text in self._extract(included): yield text def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise WeaveFormatError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise WeaveFormatError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ ## self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) # basis a list of (origin, lineno, line) basis = [] basis_lines = [] for t in self._extract(included): basis.append(t) basis_lines.append(t[2]) # add a sentinal, because we can also match against the final line basis.append((None, len(self._l), None)) # XXX: which line of the weave should we really consider # matches the end of the file? the current code says it's the # last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) ##print 'basis sequence:' ##pprint(basis) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis[i1][1] real_i2 = basis[i2][1] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def main(argv): import sys import os from weavefile import write_weave_v1, read_weave_v1 cmd = argv[1] if cmd == 'add': w = read_weave_v1(file(argv[2], 'rb')) # at the moment, based on everything in the file parents = set(range(len(w._v))) lines = sys.stdin.readlines() ver = w.add(parents, lines) write_weave_v1(w, file(argv[2], 'wb')) print 'added %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() write_weave_v1(w, file(fn, 'wb')) elif cmd == 'get': w = read_weave_v1(file(argv[2], 'rb')) sys.stdout.writelines(w.getiter(int(argv[3]))) elif cmd == 'annotate': w = read_weave_v1(file(argv[2], 'rb')) # newline is added to all lines regardless; too hard to get # reasonable formatting otherwise lasto = None for origin, text in w.annotate(int(argv[3])): text = text.rstrip('\r\n') if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) commit refs/heads/tmp mark :936 committer Martin Pool 1120124588 +1000 data 58 Refactor Weave._delta to calculate less unused information from :935 M 644 inline weave.py data 17364 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? # TODO: Separate out some code to read and write weaves. # TODO: End marker for each version? # TODO: Check that no insertion occurs inside a deletion that was # active in the version of the insertion. try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. To clients the "lines" of the file are represented as a list of strings. These strings will typically have terminal newline characters, but this is not required. In particular files commonly do not have a newline at the end of the file. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the set (included_versions), which lists the previous versions also considered active; the versions included in those versions are included transitively. So new versions created from nothing list []; most versions have a single entry; some have more. """ def __init__(self): self._l = [] self._v = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. This must normally include the parents and the parent's parents, or wierd things might happen. text Sequence of lines to be added in the new version.""" ## self._check_versions(parents) ## self._check_lines(text) idx = len(self._v) if parents: delta = self._delta(self.inclusions(parents), text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) # TODO: Could eliminate any parents that are implied by # the others self._addversion(parents) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._addversion(None) return idx def inclusions(self, versions): """Expand out everything included by versions.""" i = set(versions) for v in versions: i.update(self._v[v]) return i def _addversion(self, parents): if parents: self._v.append(frozenset(parents)) else: self._v.append(frozenset()) def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, version): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" included = self.inclusions([version]) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = None lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. WFE = WeaveFormatError for l in self._l: if isinstance(l, tuple): isactive = None # recalculate c, v = l if c == '{': if istack and (istack[-1] >= v): raise WFE("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WFE("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WFE("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WFE("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WFE("version %d deletes own text on line %d" % (v, lineno)) dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WFE("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WFE("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WFE("literal at top level on line %d" % lineno) if isactive == None: isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WFE("unclosed insertion blocks at end of weave", istack) if dset: raise WFE("unclosed deletion blocks at end of weave", dset) def get_iter(self, version): """Yield lines for the specified version.""" for origin, lineno, line in self._extract(self.inclusions([version])): yield line def get(self, index): return list(self.get_iter(index)) def merge_iter(self, included): """Return composed version of multiple included versions.""" included = frozenset(included) for origin, lineno, text in self._extract(included): yield text def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise WeaveFormatError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise WeaveFormatError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ ## self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) # basis a list of (origin, lineno, line) basis_lineno = [] basis_lines = [] for origin, lineno, line in self._extract(included): basis_lineno.append(lineno) basis_lines.append(line) # add a sentinal, because we can also match against the final line basis_lineno.append(len(self._l)) # XXX: which line of the weave should we really consider # matches the end of the file? the current code says it's the # last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis_lineno[i1] real_i2 = basis_lineno[i2] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def main(argv): import sys import os from weavefile import write_weave_v1, read_weave_v1 cmd = argv[1] if cmd == 'add': w = read_weave_v1(file(argv[2], 'rb')) # at the moment, based on everything in the file parents = set(range(len(w._v))) lines = sys.stdin.readlines() ver = w.add(parents, lines) write_weave_v1(w, file(argv[2], 'wb')) print 'added %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() write_weave_v1(w, file(fn, 'wb')) elif cmd == 'get': w = read_weave_v1(file(argv[2], 'rb')) sys.stdout.writelines(w.getiter(int(argv[3]))) elif cmd == 'annotate': w = read_weave_v1(file(argv[2], 'rb')) # newline is added to all lines regardless; too hard to get # reasonable formatting otherwise lasto = None for origin, text in w.annotate(int(argv[3])): text = text.rstrip('\r\n') if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) commit refs/heads/tmp mark :937 committer Martin Pool 1120125657 +1000 data 3 doc from :936 M 644 inline weave.py data 17583 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? # TODO: Separate out some code to read and write weaves. # TODO: End marker for each version so we can stop reading? # TODO: Check that no insertion occurs inside a deletion that was # active in the version of the insertion. # TODO: Perhaps a special slower check() method that verifies more # nesting constraints and the MD5 of each version? try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. To clients the "lines" of the file are represented as a list of strings. These strings will typically have terminal newline characters, but this is not required. In particular files commonly do not have a newline at the end of the file. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the set (included_versions), which lists the previous versions also considered active; the versions included in those versions are included transitively. So new versions created from nothing list []; most versions have a single entry; some have more. """ def __init__(self): self._l = [] self._v = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. This must normally include the parents and the parent's parents, or wierd things might happen. text Sequence of lines to be added in the new version.""" ## self._check_versions(parents) ## self._check_lines(text) idx = len(self._v) if parents: delta = self._delta(self.inclusions(parents), text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._addversion(parents) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._addversion(None) return idx def inclusions(self, versions): """Expand out everything included by versions.""" i = set(versions) for v in versions: i.update(self._v[v]) return i def _addversion(self, parents): if parents: self._v.append(frozenset(parents)) else: self._v.append(frozenset()) def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, version): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" included = self.inclusions([version]) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = None lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. # TODO: In fact, I think we only need to store the *count* of # active insertions and deletions, and we can maintain that by # just by just counting as we go along. WFE = WeaveFormatError for l in self._l: if isinstance(l, tuple): isactive = None # recalculate c, v = l if c == '{': if istack and (istack[-1] >= v): raise WFE("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WFE("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WFE("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WFE("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WFE("version %d deletes own text on line %d" % (v, lineno)) dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WFE("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WFE("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WFE("literal at top level on line %d" % lineno) if isactive == None: isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WFE("unclosed insertion blocks at end of weave", istack) if dset: raise WFE("unclosed deletion blocks at end of weave", dset) def get_iter(self, version): """Yield lines for the specified version.""" for origin, lineno, line in self._extract(self.inclusions([version])): yield line def get(self, index): return list(self.get_iter(index)) def merge_iter(self, included): """Return composed version of multiple included versions.""" included = frozenset(included) for origin, lineno, text in self._extract(included): yield text def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise WeaveFormatError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise WeaveFormatError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ ## self._check_versions(included) ##from pprint import pprint # first get basis for comparison # basis holds (lineno, origin, line) basis = [] ##print 'my lines:' ##pprint(self._l) # basis a list of (origin, lineno, line) basis_lineno = [] basis_lines = [] for origin, lineno, line in self._extract(included): basis_lineno.append(lineno) basis_lines.append(line) # add a sentinal, because we can also match against the final line basis_lineno.append(len(self._l)) # XXX: which line of the weave should we really consider # matches the end of the file? the current code says it's the # last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis_lineno[i1] real_i2 = basis_lineno[i2] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def main(argv): import sys import os from weavefile import write_weave_v1, read_weave_v1 cmd = argv[1] if cmd == 'add': w = read_weave_v1(file(argv[2], 'rb')) # at the moment, based on everything in the file parents = set(range(len(w._v))) lines = sys.stdin.readlines() ver = w.add(parents, lines) write_weave_v1(w, file(argv[2], 'wb')) print 'added %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() write_weave_v1(w, file(fn, 'wb')) elif cmd == 'get': w = read_weave_v1(file(argv[2], 'rb')) sys.stdout.writelines(w.getiter(int(argv[3]))) elif cmd == 'annotate': w = read_weave_v1(file(argv[2], 'rb')) # newline is added to all lines regardless; too hard to get # reasonable formatting otherwise lasto = None for origin, text in w.annotate(int(argv[3])): text = text.rstrip('\r\n') if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) commit refs/heads/tmp mark :938 committer Martin Pool 1120125793 +1000 data 16 Remove dead code from :937 M 644 inline weave.py data 17341 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? # TODO: Separate out some code to read and write weaves. # TODO: End marker for each version so we can stop reading? # TODO: Check that no insertion occurs inside a deletion that was # active in the version of the insertion. # TODO: Perhaps a special slower check() method that verifies more # nesting constraints and the MD5 of each version? try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. To clients the "lines" of the file are represented as a list of strings. These strings will typically have terminal newline characters, but this is not required. In particular files commonly do not have a newline at the end of the file. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the set (included_versions), which lists the previous versions also considered active; the versions included in those versions are included transitively. So new versions created from nothing list []; most versions have a single entry; some have more. """ def __init__(self): self._l = [] self._v = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. This must normally include the parents and the parent's parents, or wierd things might happen. text Sequence of lines to be added in the new version.""" ## self._check_versions(parents) ## self._check_lines(text) idx = len(self._v) if parents: delta = self._delta(self.inclusions(parents), text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._addversion(parents) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._addversion(None) return idx def inclusions(self, versions): """Expand out everything included by versions.""" i = set(versions) for v in versions: i.update(self._v[v]) return i def _addversion(self, parents): if parents: self._v.append(frozenset(parents)) else: self._v.append(frozenset()) def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, version): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" included = self.inclusions([version]) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = None lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. # TODO: In fact, I think we only need to store the *count* of # active insertions and deletions, and we can maintain that by # just by just counting as we go along. WFE = WeaveFormatError for l in self._l: if isinstance(l, tuple): isactive = None # recalculate c, v = l if c == '{': if istack and (istack[-1] >= v): raise WFE("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WFE("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WFE("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WFE("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WFE("version %d deletes own text on line %d" % (v, lineno)) dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WFE("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WFE("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WFE("literal at top level on line %d" % lineno) if isactive == None: isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WFE("unclosed insertion blocks at end of weave", istack) if dset: raise WFE("unclosed deletion blocks at end of weave", dset) def get_iter(self, version): """Yield lines for the specified version.""" for origin, lineno, line in self._extract(self.inclusions([version])): yield line def get(self, index): return list(self.get_iter(index)) def merge_iter(self, included): """Return composed version of multiple included versions.""" included = frozenset(included) for origin, lineno, text in self._extract(included): yield text def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise WeaveFormatError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise WeaveFormatError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ # basis a list of (origin, lineno, line) basis_lineno = [] basis_lines = [] for origin, lineno, line in self._extract(included): basis_lineno.append(lineno) basis_lines.append(line) # add a sentinal, because we can also match against the final line basis_lineno.append(len(self._l)) # XXX: which line of the weave should we really consider # matches the end of the file? the current code says it's the # last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis_lineno[i1] real_i2 = basis_lineno[i2] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def main(argv): import sys import os from weavefile import write_weave_v1, read_weave_v1 cmd = argv[1] if cmd == 'add': w = read_weave_v1(file(argv[2], 'rb')) # at the moment, based on everything in the file parents = set(range(len(w._v))) lines = sys.stdin.readlines() ver = w.add(parents, lines) write_weave_v1(w, file(argv[2], 'wb')) print 'added %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() write_weave_v1(w, file(fn, 'wb')) elif cmd == 'get': w = read_weave_v1(file(argv[2], 'rb')) sys.stdout.writelines(w.getiter(int(argv[3]))) elif cmd == 'annotate': w = read_weave_v1(file(argv[2], 'rb')) # newline is added to all lines regardless; too hard to get # reasonable formatting otherwise lasto = None for origin, text in w.annotate(int(argv[3])): text = text.rstrip('\r\n') if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) commit refs/heads/tmp mark :939 committer Martin Pool 1120185387 +1000 data 44 Show profile when converting inventory too. from :938 M 644 inline convertinv.py data 1725 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Experiment in converting existing bzr branches to weaves.""" import bzrlib.branch from weave import Weave from weavefile import write_weave import tempfile import hotshot def convert(): WEAVE_NAME = "inventory.weave" wf = Weave() b = bzrlib.branch.find_branch('.') print 'converting...' parents = set() revno = 1 for rev_id in b.revision_history(): print revno inv_xml = b.inventory_store[rev_id].readlines() weave_id = wf.add(parents, inv_xml) parents.add(weave_id) revno += 1 write_weave(wf, file(WEAVE_NAME, 'wb')) prof_f = tempfile.NamedTemporaryFile() prof = hotshot.Profile(prof_f.name) prof.runcall(convert) prof.close() import hotshot.stats stats = hotshot.stats.load(prof_f.name) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) commit refs/heads/tmp mark :940 committer Martin Pool 1120187324 +1000 data 24 Add weave info command. from :939 M 644 inline weave.py data 18201 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? # TODO: Separate out some code to read and write weaves. # TODO: End marker for each version so we can stop reading? # TODO: Check that no insertion occurs inside a deletion that was # active in the version of the insertion. # TODO: Perhaps a special slower check() method that verifies more # nesting constraints and the MD5 of each version? try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. To clients the "lines" of the file are represented as a list of strings. These strings will typically have terminal newline characters, but this is not required. In particular files commonly do not have a newline at the end of the file. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the set (included_versions), which lists the previous versions also considered active; the versions included in those versions are included transitively. So new versions created from nothing list []; most versions have a single entry; some have more. """ def __init__(self): self._l = [] self._v = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. This must normally include the parents and the parent's parents, or wierd things might happen. text Sequence of lines to be added in the new version.""" ## self._check_versions(parents) ## self._check_lines(text) idx = len(self._v) if parents: delta = self._delta(self.inclusions(parents), text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._addversion(parents) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._addversion(None) return idx def inclusions(self, versions): """Expand out everything included by versions.""" i = set(versions) for v in versions: i.update(self._v[v]) return i def _addversion(self, parents): if parents: self._v.append(frozenset(parents)) else: self._v.append(frozenset()) def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, version): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" included = self.inclusions([version]) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = None lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. # TODO: In fact, I think we only need to store the *count* of # active insertions and deletions, and we can maintain that by # just by just counting as we go along. WFE = WeaveFormatError for l in self._l: if isinstance(l, tuple): isactive = None # recalculate c, v = l if c == '{': if istack and (istack[-1] >= v): raise WFE("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WFE("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WFE("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WFE("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WFE("version %d deletes own text on line %d" % (v, lineno)) dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WFE("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WFE("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WFE("literal at top level on line %d" % lineno) if isactive == None: isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WFE("unclosed insertion blocks at end of weave", istack) if dset: raise WFE("unclosed deletion blocks at end of weave", dset) def get_iter(self, version): """Yield lines for the specified version.""" for origin, lineno, line in self._extract(self.inclusions([version])): yield line def get(self, index): return list(self.get_iter(index)) def merge_iter(self, included): """Return composed version of multiple included versions.""" included = frozenset(included) for origin, lineno, text in self._extract(included): yield text def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise WeaveFormatError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise WeaveFormatError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ # basis a list of (origin, lineno, line) basis_lineno = [] basis_lines = [] for origin, lineno, line in self._extract(included): basis_lineno.append(lineno) basis_lines.append(line) # add a sentinal, because we can also match against the final line basis_lineno.append(len(self._l)) # XXX: which line of the weave should we really consider # matches the end of the file? the current code says it's the # last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis_lineno[i1] real_i2 = basis_lineno[i2] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def weave_info(filename, out): """Show some text information about the weave.""" from weavefile import read_weave wf = file(filename, 'rb') w = read_weave(wf) # FIXME: doesn't work on pipes weave_size = wf.tell() print >>out, "weave file size %d bytes" % weave_size print >>out, "weave contains %d versions" % len(w._v) total = 0 print ' %8s %8s %8s' % ('version', 'lines', 'bytes') print ' -------- -------- --------' for i in range(len(w._v)): text = w.get(i) lines = len(text) bytes = sum((len(a) for a in text)) print ' %8d %8d %8d' % (i, lines, bytes) total += bytes print >>out, "versions total %d bytes" % total print >>out, "compression ratio %.3f" % (float(total)/float(weave_size)) def main(argv): import sys import os from weavefile import write_weave_v1, read_weave_v1 cmd = argv[1] if cmd == 'add': w = read_weave_v1(file(argv[2], 'rb')) # at the moment, based on everything in the file parents = set(range(len(w._v))) lines = sys.stdin.readlines() ver = w.add(parents, lines) write_weave_v1(w, file(argv[2], 'wb')) print 'added %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() write_weave_v1(w, file(fn, 'wb')) elif cmd == 'get': w = read_weave_v1(file(argv[2], 'rb')) sys.stdout.writelines(w.getiter(int(argv[3]))) elif cmd == 'annotate': w = read_weave_v1(file(argv[2], 'rb')) # newline is added to all lines regardless; too hard to get # reasonable formatting otherwise lasto = None for origin, text in w.annotate(int(argv[3])): text = text.rstrip('\r\n') if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin elif cmd == 'info': weave_info(argv[2], sys.stdout) else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) commit refs/heads/tmp mark :941 committer Martin Pool 1120188054 +1000 data 47 Store SHA1 in weave file for later verification from :940 M 644 inline weave.py data 18470 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? # TODO: Separate out some code to read and write weaves. # TODO: End marker for each version so we can stop reading? # TODO: Check that no insertion occurs inside a deletion that was # active in the version of the insertion. # TODO: Perhaps a special slower check() method that verifies more # nesting constraints and the MD5 of each version? try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. To clients the "lines" of the file are represented as a list of strings. These strings will typically have terminal newline characters, but this is not required. In particular files commonly do not have a newline at the end of the file. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the set (included_versions), which lists the previous versions also considered active; the versions included in those versions are included transitively. So new versions created from nothing list []; most versions have a single entry; some have more. _sha1s List of hex SHA-1 of each version, or None if not recorded. """ def __init__(self): self._l = [] self._v = [] self._sha1s = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. This must normally include the parents and the parent's parents, or wierd things might happen. text Sequence of lines to be added in the new version.""" ## self._check_versions(parents) ## self._check_lines(text) idx = len(self._v) import sha s = sha.new() for l in text: s.update(l) sha1 = s.hexdigest() del s if parents: delta = self._delta(self.inclusions(parents), text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._addversion(parents) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._addversion(None) self._sha1s.append(sha1) return idx def inclusions(self, versions): """Expand out everything included by versions.""" i = set(versions) for v in versions: i.update(self._v[v]) return i def _addversion(self, parents): if parents: self._v.append(frozenset(parents)) else: self._v.append(frozenset()) def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, version): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" included = self.inclusions([version]) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = None lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. # TODO: In fact, I think we only need to store the *count* of # active insertions and deletions, and we can maintain that by # just by just counting as we go along. WFE = WeaveFormatError for l in self._l: if isinstance(l, tuple): isactive = None # recalculate c, v = l if c == '{': if istack and (istack[-1] >= v): raise WFE("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WFE("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WFE("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WFE("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WFE("version %d deletes own text on line %d" % (v, lineno)) dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WFE("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WFE("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WFE("literal at top level on line %d" % lineno) if isactive == None: isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WFE("unclosed insertion blocks at end of weave", istack) if dset: raise WFE("unclosed deletion blocks at end of weave", dset) def get_iter(self, version): """Yield lines for the specified version.""" for origin, lineno, line in self._extract(self.inclusions([version])): yield line def get(self, index): return list(self.get_iter(index)) def merge_iter(self, included): """Return composed version of multiple included versions.""" included = frozenset(included) for origin, lineno, text in self._extract(included): yield text def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def check(self): for vers_info in self._v: included = set() for vi in vers_info[0]: if vi < 0 or vi >= index: raise WeaveFormatError("invalid included version %d for index %d" % (vi, index)) if vi in included: raise WeaveFormatError("repeated included version %d for index %d" % (vi, index)) included.add(vi) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ # basis a list of (origin, lineno, line) basis_lineno = [] basis_lines = [] for origin, lineno, line in self._extract(included): basis_lineno.append(lineno) basis_lines.append(line) # add a sentinal, because we can also match against the final line basis_lineno.append(len(self._l)) # XXX: which line of the weave should we really consider # matches the end of the file? the current code says it's the # last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis_lineno[i1] real_i2 = basis_lineno[i2] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def weave_info(filename, out): """Show some text information about the weave.""" from weavefile import read_weave wf = file(filename, 'rb') w = read_weave(wf) # FIXME: doesn't work on pipes weave_size = wf.tell() print >>out, "weave file size %d bytes" % weave_size print >>out, "weave contains %d versions" % len(w._v) total = 0 print ' %8s %8s %8s' % ('version', 'lines', 'bytes') print ' -------- -------- --------' for i in range(len(w._v)): text = w.get(i) lines = len(text) bytes = sum((len(a) for a in text)) print ' %8d %8d %8d' % (i, lines, bytes) total += bytes print >>out, "versions total %d bytes" % total print >>out, "compression ratio %.3f" % (float(total)/float(weave_size)) def main(argv): import sys import os from weavefile import write_weave_v1, read_weave_v1 cmd = argv[1] if cmd == 'add': w = read_weave_v1(file(argv[2], 'rb')) # at the moment, based on everything in the file parents = set(range(len(w._v))) lines = sys.stdin.readlines() ver = w.add(parents, lines) write_weave_v1(w, file(argv[2], 'wb')) print 'added %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() write_weave_v1(w, file(fn, 'wb')) elif cmd == 'get': w = read_weave_v1(file(argv[2], 'rb')) sys.stdout.writelines(w.getiter(int(argv[3]))) elif cmd == 'annotate': w = read_weave_v1(file(argv[2], 'rb')) # newline is added to all lines regardless; too hard to get # reasonable formatting otherwise lasto = None for origin, text in w.annotate(int(argv[3])): text = text.rstrip('\r\n') if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin elif cmd == 'info': weave_info(argv[2], sys.stdout) else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) M 644 inline weavefile.py data 4718 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Store and retrieve weaves in files. There is one format marker followed by a blank line, followed by a series of version headers, followed by the weave itself. Each version marker has 'v' and the version, then 'i' and the included previous versions, then '1' and the SHA-1 of the text, if known. The inclusions do not need to list versions included by a parent. The weave is bracketed by 'w' and 'W' lines, and includes the '{}[]' processing instructions. Lines of text are prefixed by '.' if the line contains a newline, or ',' if not. """ # TODO: When extracting a single version it'd be enough to just pass # an iterator returning the weave lines... FORMAT_1 = '# bzr weave file v2\n' def write_weave(weave, f, format=None): if format == None or format == 1: return write_weave_v1(weave, f) else: raise ValueError("unknown weave format %r" % format) def write_weave_v1(weave, f): """Write weave to file f.""" print >>f, FORMAT_1, for version, included in enumerate(weave._v): print >>f, 'v', version if included: # find a minimal expression of it; bias towards using # later revisions li = list(included) li.sort() li.reverse() mininc = [] gotit = set() for pv in li: if pv not in gotit: mininc.append(pv) gotit.update(weave._v[pv]) assert mininc[0] >= 0 assert mininc[-1] < version print >>f, 'i', for i in mininc: print >>f, i, print >>f else: print >>f, 'i' print >>f, '1', weave._sha1s[version] print >>f print >>f, 'w' for l in weave._l: if isinstance(l, tuple): assert l[0] in '{}[]' print >>f, '%s %d' % l else: # text line if not l: print >>f, ', ' elif l[-1] == '\n': assert l.find('\n', 0, -1) == -1 print >>f, '.', l, else: assert l.find('\n') == -1 print >>f, ',', l print >>f, 'W' def read_weave(f): return read_weave_v1(f) def read_weave_v1(f): from weave import Weave, WeaveFormatError w = Weave() wfe = WeaveFormatError l = f.readline() if l != FORMAT_1: raise WeaveFormatError('invalid weave file header: %r' % l) v_cnt = 0 while True: l = f.readline() if l.startswith('v '): ver = int(l[2:]) if ver != v_cnt: raise WeaveFormatError('version %d!=%d out of order' % (ver, v_cnt)) v_cnt += 1 l = f.readline()[:-1] if l[0] != 'i': raise WeaveFormatError('unexpected line %r' % l) if len(l) > 2: included = map(int, l[2:].split(' ')) full = set() for pv in included: full.add(pv) full.update(w._v[pv]) w._addversion(full) else: w._addversion(None) l = f.readline()[:-1] assert l.startswith('1 ') w._sha1s.append(l[2:]) assert f.readline() == '\n' elif l == 'w\n': break else: raise WeaveFormatError('unexpected line %r' % l) while True: l = f.readline() if l == 'W\n': break elif l.startswith('. '): w._l.append(l[2:]) # include newline elif l.startswith(', '): w._l.append(l[2:-1]) # exclude newline else: assert l[0] in '{}[]', l assert l[1] == ' ', l w._l.append((l[0], int(l[2:]))) return w commit refs/heads/tmp mark :942 committer Martin Pool 1120188263 +1000 data 42 Remove redundant 'v' lines from weave file from :941 M 644 inline weavefile.py data 4328 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Store and retrieve weaves in files. There is one format marker followed by a blank line, followed by a series of version headers, followed by the weave itself. Each version marker has 'i' and the included previous versions, then '1' and the SHA-1 of the text, if known. The inclusions do not need to list versions included by a parent. The weave is bracketed by 'w' and 'W' lines, and includes the '{}[]' processing instructions. Lines of text are prefixed by '.' if the line contains a newline, or ',' if not. """ # TODO: When extracting a single version it'd be enough to just pass # an iterator returning the weave lines... FORMAT_1 = '# bzr weave file v3\n' def write_weave(weave, f, format=None): if format == None or format == 1: return write_weave_v1(weave, f) else: raise ValueError("unknown weave format %r" % format) def write_weave_v1(weave, f): """Write weave to file f.""" print >>f, FORMAT_1, for version, included in enumerate(weave._v): if included: # find a minimal expression of it; bias towards using # later revisions li = list(included) li.sort() li.reverse() mininc = [] gotit = set() for pv in li: if pv not in gotit: mininc.append(pv) gotit.update(weave._v[pv]) assert mininc[0] >= 0 assert mininc[-1] < version print >>f, 'i', for i in mininc: print >>f, i, print >>f else: print >>f, 'i' print >>f, '1', weave._sha1s[version] print >>f print >>f, 'w' for l in weave._l: if isinstance(l, tuple): assert l[0] in '{}[]' print >>f, '%s %d' % l else: # text line if not l: print >>f, ', ' elif l[-1] == '\n': assert l.find('\n', 0, -1) == -1 print >>f, '.', l, else: assert l.find('\n') == -1 print >>f, ',', l print >>f, 'W' def read_weave(f): return read_weave_v1(f) def read_weave_v1(f): from weave import Weave, WeaveFormatError w = Weave() wfe = WeaveFormatError l = f.readline() if l != FORMAT_1: raise WeaveFormatError('invalid weave file header: %r' % l) ver = 0 while True: l = f.readline() if l[0] == 'i': ver += 1 if len(l) > 2: included = map(int, l[2:].split(' ')) full = set() for pv in included: full.add(pv) full.update(w._v[pv]) w._addversion(full) else: w._addversion(None) l = f.readline()[:-1] assert l.startswith('1 ') w._sha1s.append(l[2:]) assert f.readline() == '\n' elif l == 'w\n': break else: raise WeaveFormatError('unexpected line %r' % l) while True: l = f.readline() if l == 'W\n': break elif l.startswith('. '): w._l.append(l[2:]) # include newline elif l.startswith(', '): w._l.append(l[2:-1]) # exclude newline else: assert l[0] in '{}[]', l assert l[1] == ' ', l w._l.append((l[0], int(l[2:]))) return w commit refs/heads/tmp mark :943 committer Martin Pool 1120188759 +1000 data 98 Update Weave.check Add new external check command that invokes it Check sha1 sums for all versions from :942 M 644 inline weave.py data 19202 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? # TODO: Separate out some code to read and write weaves. # TODO: End marker for each version so we can stop reading? # TODO: Check that no insertion occurs inside a deletion that was # active in the version of the insertion. # TODO: Perhaps a special slower check() method that verifies more # nesting constraints and the MD5 of each version? try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. To clients the "lines" of the file are represented as a list of strings. These strings will typically have terminal newline characters, but this is not required. In particular files commonly do not have a newline at the end of the file. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the set (included_versions), which lists the previous versions also considered active; the versions included in those versions are included transitively. So new versions created from nothing list []; most versions have a single entry; some have more. _sha1s List of hex SHA-1 of each version, or None if not recorded. """ def __init__(self): self._l = [] self._v = [] self._sha1s = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. This must normally include the parents and the parent's parents, or wierd things might happen. text Sequence of lines to be added in the new version.""" ## self._check_versions(parents) ## self._check_lines(text) idx = len(self._v) import sha s = sha.new() for l in text: s.update(l) sha1 = s.hexdigest() del s if parents: delta = self._delta(self.inclusions(parents), text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._addversion(parents) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._addversion(None) self._sha1s.append(sha1) return idx def inclusions(self, versions): """Expand out everything included by versions.""" i = set(versions) for v in versions: i.update(self._v[v]) return i def _addversion(self, parents): if parents: self._v.append(frozenset(parents)) else: self._v.append(frozenset()) def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, version): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" included = self.inclusions([version]) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = None lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. # TODO: In fact, I think we only need to store the *count* of # active insertions and deletions, and we can maintain that by # just by just counting as we go along. WFE = WeaveFormatError for l in self._l: if isinstance(l, tuple): isactive = None # recalculate c, v = l if c == '{': if istack and (istack[-1] >= v): raise WFE("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WFE("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WFE("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WFE("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WFE("version %d deletes own text on line %d" % (v, lineno)) dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WFE("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WFE("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WFE("literal at top level on line %d" % lineno) if isactive == None: isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WFE("unclosed insertion blocks at end of weave", istack) if dset: raise WFE("unclosed deletion blocks at end of weave", dset) def get_iter(self, version): """Yield lines for the specified version.""" for origin, lineno, line in self._extract(self.inclusions([version])): yield line def get(self, index): return list(self.get_iter(index)) def merge_iter(self, included): """Return composed version of multiple included versions.""" included = frozenset(included) for origin, lineno, text in self._extract(included): yield text def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def numversions(self): l = len(self._v) assert l == len(self._sha1s) return l def check(self): # check no circular inclusions for version in range(self.numversions()): inclusions = list(self._v[version]) if inclusions: inclusions.sort() if inclusions[-1] >= version: raise WeaveFormatError("invalid included version %d for index %d" % (inclusions[-1], version)) # try extracting all versions; this is a bit slow and parallel # extraction could be used import sha for version in range(self.numversions()): s = sha.new() for l in self.get_iter(version): s.update(l) hd = s.hexdigest() expected = self._sha1s[version] if hd != expected: raise WeaveError("mismatched sha1 for version %d; " "got %s, expected %s" % (version, hd, expected)) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ # basis a list of (origin, lineno, line) basis_lineno = [] basis_lines = [] for origin, lineno, line in self._extract(included): basis_lineno.append(lineno) basis_lines.append(line) # add a sentinal, because we can also match against the final line basis_lineno.append(len(self._l)) # XXX: which line of the weave should we really consider # matches the end of the file? the current code says it's the # last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis_lineno[i1] real_i2 = basis_lineno[i2] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def weave_info(filename, out): """Show some text information about the weave.""" from weavefile import read_weave wf = file(filename, 'rb') w = read_weave(wf) # FIXME: doesn't work on pipes weave_size = wf.tell() print >>out, "weave file size %d bytes" % weave_size print >>out, "weave contains %d versions" % len(w._v) total = 0 print ' %8s %8s %8s %s' % ('version', 'lines', 'bytes', 'sha1') print ' -------- -------- -------- ----------------------------------------' for i in range(len(w._v)): text = w.get(i) lines = len(text) bytes = sum((len(a) for a in text)) sha1 = w._sha1s[i] print ' %8d %8d %8d %s' % (i, lines, bytes, sha1) total += bytes print >>out, "versions total %d bytes" % total print >>out, "compression ratio %.3f" % (float(total)/float(weave_size)) def main(argv): import sys import os from weavefile import write_weave_v1, read_weave cmd = argv[1] if cmd == 'add': w = read_weave(file(argv[2], 'rb')) # at the moment, based on everything in the file parents = set(range(len(w._v))) lines = sys.stdin.readlines() ver = w.add(parents, lines) write_weave_v1(w, file(argv[2], 'wb')) print 'added %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() write_weave_v1(w, file(fn, 'wb')) elif cmd == 'get': w = read_weave(file(argv[2], 'rb')) sys.stdout.writelines(w.getiter(int(argv[3]))) elif cmd == 'annotate': w = read_weave(file(argv[2], 'rb')) # newline is added to all lines regardless; too hard to get # reasonable formatting otherwise lasto = None for origin, text in w.annotate(int(argv[3])): text = text.rstrip('\r\n') if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin elif cmd == 'info': weave_info(argv[2], sys.stdout) elif cmd == 'check': w = read_weave(file(argv[2], 'rb')) w.check() else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) commit refs/heads/tmp mark :944 committer Martin Pool 1120189035 +1000 data 56 Use proper relative path when converting a file from bzr from :943 M 644 inline convertfile.py data 2143 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Experiment in converting existing bzr branches to weaves.""" import sys import bzrlib.branch from weave import Weave from weavefile import write_weave import hotshot import tempfile def convert(): WEAVE_NAME = "test.weave" wf = Weave() toconvert = sys.argv[1] b = bzrlib.branch.find_branch(toconvert) rp = b.relpath(toconvert) print 'converting...' fid = b.read_working_inventory().path2id(rp) last_lines = None parents = set() revno = 0 for rev_id in b.revision_history(): revno += 1 print revno tree = b.revision_tree(rev_id) inv = tree.inventory if fid not in tree: print ' (not present)' continue text = tree.get_file(fid).readlines() if text == last_lines: continue last_lines = text weave_id = wf.add(parents, text) parents.add(weave_id) print ' %4d lines' % len(text) write_weave(wf, file(WEAVE_NAME, 'wb')) prof_f = tempfile.NamedTemporaryFile() prof = hotshot.Profile(prof_f.name) prof.runcall(convert) prof.close() import hotshot.stats stats = hotshot.stats.load(prof_f.name) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) commit refs/heads/tmp mark :945 committer Martin Pool 1120189174 +1000 data 31 Fix assertion with side effects from :944 M 644 inline weavefile.py data 4346 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Store and retrieve weaves in files. There is one format marker followed by a blank line, followed by a series of version headers, followed by the weave itself. Each version marker has 'i' and the included previous versions, then '1' and the SHA-1 of the text, if known. The inclusions do not need to list versions included by a parent. The weave is bracketed by 'w' and 'W' lines, and includes the '{}[]' processing instructions. Lines of text are prefixed by '.' if the line contains a newline, or ',' if not. """ # TODO: When extracting a single version it'd be enough to just pass # an iterator returning the weave lines... FORMAT_1 = '# bzr weave file v3\n' def write_weave(weave, f, format=None): if format == None or format == 1: return write_weave_v1(weave, f) else: raise ValueError("unknown weave format %r" % format) def write_weave_v1(weave, f): """Write weave to file f.""" print >>f, FORMAT_1, for version, included in enumerate(weave._v): if included: # find a minimal expression of it; bias towards using # later revisions li = list(included) li.sort() li.reverse() mininc = [] gotit = set() for pv in li: if pv not in gotit: mininc.append(pv) gotit.update(weave._v[pv]) assert mininc[0] >= 0 assert mininc[-1] < version print >>f, 'i', for i in mininc: print >>f, i, print >>f else: print >>f, 'i' print >>f, '1', weave._sha1s[version] print >>f print >>f, 'w' for l in weave._l: if isinstance(l, tuple): assert l[0] in '{}[]' print >>f, '%s %d' % l else: # text line if not l: print >>f, ', ' elif l[-1] == '\n': assert l.find('\n', 0, -1) == -1 print >>f, '.', l, else: assert l.find('\n') == -1 print >>f, ',', l print >>f, 'W' def read_weave(f): return read_weave_v1(f) def read_weave_v1(f): from weave import Weave, WeaveFormatError w = Weave() wfe = WeaveFormatError l = f.readline() if l != FORMAT_1: raise WeaveFormatError('invalid weave file header: %r' % l) ver = 0 while True: l = f.readline() if l[0] == 'i': ver += 1 if len(l) > 2: included = map(int, l[2:].split(' ')) full = set() for pv in included: full.add(pv) full.update(w._v[pv]) w._addversion(full) else: w._addversion(None) l = f.readline()[:-1] assert l.startswith('1 ') w._sha1s.append(l[2:]) l = f.readline() assert l == '\n' elif l == 'w\n': break else: raise WeaveFormatError('unexpected line %r' % l) while True: l = f.readline() if l == 'W\n': break elif l.startswith('. '): w._l.append(l[2:]) # include newline elif l.startswith(', '): w._l.append(l[2:-1]) # exclude newline else: assert l[0] in '{}[]', l assert l[1] == ' ', l w._l.append((l[0], int(l[2:]))) return w commit refs/heads/tmp mark :946 committer Martin Pool 1120190869 +1000 data 17 Fix get_iter call from :945 M 644 inline weave.py data 19203 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? # TODO: Separate out some code to read and write weaves. # TODO: End marker for each version so we can stop reading? # TODO: Check that no insertion occurs inside a deletion that was # active in the version of the insertion. # TODO: Perhaps a special slower check() method that verifies more # nesting constraints and the MD5 of each version? try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. To clients the "lines" of the file are represented as a list of strings. These strings will typically have terminal newline characters, but this is not required. In particular files commonly do not have a newline at the end of the file. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the set (included_versions), which lists the previous versions also considered active; the versions included in those versions are included transitively. So new versions created from nothing list []; most versions have a single entry; some have more. _sha1s List of hex SHA-1 of each version, or None if not recorded. """ def __init__(self): self._l = [] self._v = [] self._sha1s = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. This must normally include the parents and the parent's parents, or wierd things might happen. text Sequence of lines to be added in the new version.""" ## self._check_versions(parents) ## self._check_lines(text) idx = len(self._v) import sha s = sha.new() for l in text: s.update(l) sha1 = s.hexdigest() del s if parents: delta = self._delta(self.inclusions(parents), text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._addversion(parents) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._addversion(None) self._sha1s.append(sha1) return idx def inclusions(self, versions): """Expand out everything included by versions.""" i = set(versions) for v in versions: i.update(self._v[v]) return i def _addversion(self, parents): if parents: self._v.append(frozenset(parents)) else: self._v.append(frozenset()) def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, version): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" included = self.inclusions([version]) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = None lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. # TODO: In fact, I think we only need to store the *count* of # active insertions and deletions, and we can maintain that by # just by just counting as we go along. WFE = WeaveFormatError for l in self._l: if isinstance(l, tuple): isactive = None # recalculate c, v = l if c == '{': if istack and (istack[-1] >= v): raise WFE("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WFE("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WFE("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WFE("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WFE("version %d deletes own text on line %d" % (v, lineno)) dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WFE("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WFE("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WFE("literal at top level on line %d" % lineno) if isactive == None: isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WFE("unclosed insertion blocks at end of weave", istack) if dset: raise WFE("unclosed deletion blocks at end of weave", dset) def get_iter(self, version): """Yield lines for the specified version.""" for origin, lineno, line in self._extract(self.inclusions([version])): yield line def get(self, index): return list(self.get_iter(index)) def merge_iter(self, included): """Return composed version of multiple included versions.""" included = frozenset(included) for origin, lineno, text in self._extract(included): yield text def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def numversions(self): l = len(self._v) assert l == len(self._sha1s) return l def check(self): # check no circular inclusions for version in range(self.numversions()): inclusions = list(self._v[version]) if inclusions: inclusions.sort() if inclusions[-1] >= version: raise WeaveFormatError("invalid included version %d for index %d" % (inclusions[-1], version)) # try extracting all versions; this is a bit slow and parallel # extraction could be used import sha for version in range(self.numversions()): s = sha.new() for l in self.get_iter(version): s.update(l) hd = s.hexdigest() expected = self._sha1s[version] if hd != expected: raise WeaveError("mismatched sha1 for version %d; " "got %s, expected %s" % (version, hd, expected)) def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ # basis a list of (origin, lineno, line) basis_lineno = [] basis_lines = [] for origin, lineno, line in self._extract(included): basis_lineno.append(lineno) basis_lines.append(line) # add a sentinal, because we can also match against the final line basis_lineno.append(len(self._l)) # XXX: which line of the weave should we really consider # matches the end of the file? the current code says it's the # last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis_lineno[i1] real_i2 = basis_lineno[i2] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def weave_info(filename, out): """Show some text information about the weave.""" from weavefile import read_weave wf = file(filename, 'rb') w = read_weave(wf) # FIXME: doesn't work on pipes weave_size = wf.tell() print >>out, "weave file size %d bytes" % weave_size print >>out, "weave contains %d versions" % len(w._v) total = 0 print ' %8s %8s %8s %s' % ('version', 'lines', 'bytes', 'sha1') print ' -------- -------- -------- ----------------------------------------' for i in range(len(w._v)): text = w.get(i) lines = len(text) bytes = sum((len(a) for a in text)) sha1 = w._sha1s[i] print ' %8d %8d %8d %s' % (i, lines, bytes, sha1) total += bytes print >>out, "versions total %d bytes" % total print >>out, "compression ratio %.3f" % (float(total)/float(weave_size)) def main(argv): import sys import os from weavefile import write_weave_v1, read_weave cmd = argv[1] if cmd == 'add': w = read_weave(file(argv[2], 'rb')) # at the moment, based on everything in the file parents = set(range(len(w._v))) lines = sys.stdin.readlines() ver = w.add(parents, lines) write_weave_v1(w, file(argv[2], 'wb')) print 'added %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() write_weave_v1(w, file(fn, 'wb')) elif cmd == 'get': w = read_weave(file(argv[2], 'rb')) sys.stdout.writelines(w.get_iter(int(argv[3]))) elif cmd == 'annotate': w = read_weave(file(argv[2], 'rb')) # newline is added to all lines regardless; too hard to get # reasonable formatting otherwise lasto = None for origin, text in w.annotate(int(argv[3])): text = text.rstrip('\r\n') if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin elif cmd == 'info': weave_info(argv[2], sys.stdout) elif cmd == 'check': w = read_weave(file(argv[2], 'rb')) w.check() else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) commit refs/heads/tmp mark :947 committer Martin Pool 1120723032 +1000 data 38 - preliminary merge conflict detection from :946 M 644 inline testweave.py data 18090 #! /usr/bin/python2.4 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test suite for weave algorithm""" import testsweet from weave import Weave, WeaveFormatError from pprint import pformat try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class TestBase(testsweet.TestBase): def check_read_write(self, k): """Check the weave k can be written & re-read.""" from tempfile import TemporaryFile from weavefile import write_weave, read_weave tf = TemporaryFile() write_weave(k, tf) tf.seek(0) k2 = read_weave(tf) if k != k2: tf.seek(0) self.log('serialized weave:') self.log(tf.read()) self.fail('read/write check failed') class Easy(TestBase): def runTest(self): k = Weave() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Weave() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class DeltaAdd(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): k = Weave() k.add([], ['line 1']) self.assertEqual(k._l, [('{', 0), 'line 1', ('}', 0), ]) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # currently there are 3 lines in the weave, and we insert after them self.assertEquals(changes, [(3, 3, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(1, 1, ['top line'])]) self.check_read_write(k) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Weave() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): k = Weave() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) text3 = ['line 1', 'middle line', 'line 2'] k.add([0, 1], text3) self.log("changes to text3: " + pformat(list(k._delta(set([0, 1]), text3)))) self.log("k._l=" + pformat(k._l)) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) # now multiple insertions at different places k.add([0, 1, 3], ['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc']) self.assertEqual(k.annotate(4), [(0, 'line 1'), (4, 'aaa'), (3, 'middle line'), (4, 'bbb'), (1, 'line 2'), (4, 'ccc')]) class DeleteLines(TestBase): """Deletion of lines from existing text. Try various texts all based on a common ancestor.""" def runTest(self): k = Weave() base_text = ['one', 'two', 'three', 'four'] k.add([], base_text) texts = [['one', 'two', 'three'], ['two', 'three', 'four'], ['one', 'four'], ['one', 'two', 'three', 'four'], ] for t in texts: ver = k.add([0], t) self.log('final weave:') self.log('k._l=' + pformat(k._l)) for i in range(len(texts)): self.assertEqual(k.get(i+1), texts[i]) class SuicideDelete(TestBase): """Invalid weave which tries to add and delete simultaneously.""" def runTest(self): k = Weave() k._v = [(), ] k._l = [('{', 0), 'first line', ('[', 0), 'deleted in 0', (']', 0), ('}', 0), ] self.assertRaises(WeaveFormatError, k.get, 0) class CannedDelete(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [(), frozenset([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'last line', ]) class CannedReplacement(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), ('{', 1), 'replacement line', ('}', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'replacement line', 'last line', ]) class BadWeave(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [frozenset(), ] k._l = ['bad line', ('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) class BadInsert(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0]), frozenset([0]), frozenset([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 1), ' more in 1', ('}', 1), ('}', 1), ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) self.assertRaises(WeaveFormatError, k.get, 1) class InsertNested(TestBase): """Insertion with nested instructions.""" def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0]), frozenset([0]), frozenset([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertEqual(k.get(0), ['foo {', '}']) self.assertEqual(k.get(1), ['foo {', ' added in version 1', ' also from v1', '}']) self.assertEqual(k.get(2), ['foo {', ' added in v2', '}']) self.assertEqual(k.get(3), ['foo {', ' added in version 1', ' added in v2', ' also from v1', '}']) class DeleteLines2(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Weave() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) self.assertEqual(k.annotate(1), [(0, "line the first"), (0, "fine")]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a weave with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0])] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1)] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Weave with two diverged texts based on version 0. """ def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0]), frozenset([0]), ] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1), ('{', 2), "alternative second line", ('}', 2), ] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) self.assertEqual(k.inclusions([2]), set([0, 2])) class ReplaceLine(TestBase): def runTest(self): k = Weave() text0 = ['cheddar', 'stilton', 'gruyere'] text1 = ['cheddar', 'blue vein', 'neufchatel', 'chevre'] k.add([], text0) k.add([0], text1) self.log('k._l=' + pformat(k._l)) self.assertEqual(k.get(0), text0) self.assertEqual(k.get(1), text1) class Merge(TestBase): """Storage of versions that merge diverged parents""" def runTest(self): k = Weave() texts = [['header'], ['header', '', 'line from 1'], ['header', '', 'line from 2', 'more from 2'], ['header', '', 'line from 1', 'fixup line', 'line from 2'], ] k.add([], texts[0]) k.add([0], texts[1]) k.add([0], texts[2]) k.add([0, 1, 2], texts[3]) for i, t in enumerate(texts): self.assertEqual(k.get(i), t) self.assertEqual(k.annotate(3), [(0, 'header'), (1, ''), (1, 'line from 1'), (3, 'fixup line'), (2, 'line from 2'), ]) self.assertEqual(k.inclusions([3]), set([0, 1, 2, 3])) self.log('k._l=' + pformat(k._l)) self.check_read_write(k) class Conflicts(TestBase): """Test detection of conflicting regions during a merge. A base version is inserted, then two descendents try to insert different lines in the same place. These should be reported as a possible conflict and forwarded to the user.""" def runTest(self): return # NOT RUN k = Weave() k.add([], ['aaa', 'bbb']) k.add([0], ['aaa', '111', 'bbb']) k.add([1], ['aaa', '222', 'bbb']) merged = k.merge([1, 2]) self.assertEquals([[['aaa']], [['111'], ['222']], [['bbb']]]) class NonConflict(TestBase): """Two descendants insert compatible changes. No conflict should be reported.""" def runTest(self): return # NOT RUN k = Weave() k.add([], ['aaa', 'bbb']) k.add([0], ['111', 'aaa', 'ccc', 'bbb']) k.add([1], ['aaa', 'ccc', 'bbb', '222']) class AutoMerge(TestBase): def runTest(self): k = Weave() texts = [['header', 'aaa', 'bbb'], ['header', 'aaa', 'line from 1', 'bbb'], ['header', 'aaa', 'bbb', 'line from 2', 'more from 2'], ] k.add([], texts[0]) k.add([0], texts[1]) k.add([0], texts[2]) self.log('k._l=' + pformat(k._l)) m = list(k.mash_iter([0, 1, 2])) self.assertEqual(m, ['header', 'aaa', 'line from 1', 'bbb', 'line from 2', 'more from 2']) class Khayyam(TestBase): """Test changes to multi-line texts, and read/write""" def runTest(self): rawtexts = [ """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, -- and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise enow!""", """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, -- and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now!""", """A Book of poems underneath the tree, A Jug of Wine, a Loaf of Bread, and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now! -- O. Khayyam""", """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now!""", ] texts = [[l.strip() for l in t.split('\n')] for t in rawtexts] k = Weave() parents = set() for t in texts: ver = k.add(list(parents), t) parents.add(ver) self.log("k._l=" + pformat(k._l)) for i, t in enumerate(texts): self.assertEqual(k.get(i), t) self.check_read_write(k) def testweave(): import testsweet from unittest import TestSuite, TestLoader import testweave tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testweave)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testweave()) M 644 inline weave.py data 19737 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? # TODO: Separate out some code to read and write weaves. # TODO: End marker for each version so we can stop reading? # TODO: Check that no insertion occurs inside a deletion that was # active in the version of the insertion. # TODO: Perhaps a special slower check() method that verifies more # nesting constraints and the MD5 of each version? try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. To clients the "lines" of the file are represented as a list of strings. These strings will typically have terminal newline characters, but this is not required. In particular files commonly do not have a newline at the end of the file. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the set (included_versions), which lists the previous versions also considered active; the versions included in those versions are included transitively. So new versions created from nothing list []; most versions have a single entry; some have more. _sha1s List of hex SHA-1 of each version, or None if not recorded. """ def __init__(self): self._l = [] self._v = [] self._sha1s = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. This must normally include the parents and the parent's parents, or wierd things might happen. text Sequence of lines to be added in the new version.""" ## self._check_versions(parents) ## self._check_lines(text) idx = len(self._v) import sha s = sha.new() for l in text: s.update(l) sha1 = s.hexdigest() del s if parents: delta = self._delta(self.inclusions(parents), text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._addversion(parents) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._addversion(None) self._sha1s.append(sha1) return idx def inclusions(self, versions): """Expand out everything included by versions.""" i = set(versions) for v in versions: i.update(self._v[v]) return i def _addversion(self, parents): if parents: self._v.append(frozenset(parents)) else: self._v.append(frozenset()) def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, version): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" included = self.inclusions([version]) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = None lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. # TODO: In fact, I think we only need to store the *count* of # active insertions and deletions, and we can maintain that by # just by just counting as we go along. WFE = WeaveFormatError for l in self._l: if isinstance(l, tuple): isactive = None # recalculate c, v = l if c == '{': if istack and (istack[-1] >= v): raise WFE("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WFE("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WFE("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WFE("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WFE("version %d deletes own text on line %d" % (v, lineno)) # XXX dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WFE("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WFE("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WFE("literal at top level on line %d" % lineno) if isactive == None: isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WFE("unclosed insertion blocks at end of weave", istack) if dset: raise WFE("unclosed deletion blocks at end of weave", dset) def get_iter(self, version): """Yield lines for the specified version.""" for origin, lineno, line in self._extract(self.inclusions([version])): yield line def get(self, index): return list(self.get_iter(index)) def mash_iter(self, included): """Return composed version of multiple included versions.""" included = frozenset(included) for origin, lineno, text in self._extract(included): yield text def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def numversions(self): l = len(self._v) assert l == len(self._sha1s) return l def check(self): # check no circular inclusions for version in range(self.numversions()): inclusions = list(self._v[version]) if inclusions: inclusions.sort() if inclusions[-1] >= version: raise WeaveFormatError("invalid included version %d for index %d" % (inclusions[-1], version)) # try extracting all versions; this is a bit slow and parallel # extraction could be used import sha for version in range(self.numversions()): s = sha.new() for l in self.get_iter(version): s.update(l) hd = s.hexdigest() expected = self._sha1s[version] if hd != expected: raise WeaveError("mismatched sha1 for version %d; " "got %s, expected %s" % (version, hd, expected)) def merge(self, merge_versions): """Automerge and mark conflicts between versions. This returns a sequence, each entry describing alternatives for a chunk of the file. Each of the alternatives is given as a list of lines. If there is a chunk of the file where there's no diagreement, only one alternative is given. """ # approach: find the included versions common to all the # merged versions raise NotImplementedError() def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ # basis a list of (origin, lineno, line) basis_lineno = [] basis_lines = [] for origin, lineno, line in self._extract(included): basis_lineno.append(lineno) basis_lines.append(line) # add a sentinal, because we can also match against the final line basis_lineno.append(len(self._l)) # XXX: which line of the weave should we really consider # matches the end of the file? the current code says it's the # last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis_lineno[i1] real_i2 = basis_lineno[i2] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def weave_info(filename, out): """Show some text information about the weave.""" from weavefile import read_weave wf = file(filename, 'rb') w = read_weave(wf) # FIXME: doesn't work on pipes weave_size = wf.tell() print >>out, "weave file size %d bytes" % weave_size print >>out, "weave contains %d versions" % len(w._v) total = 0 print ' %8s %8s %8s %s' % ('version', 'lines', 'bytes', 'sha1') print ' -------- -------- -------- ----------------------------------------' for i in range(len(w._v)): text = w.get(i) lines = len(text) bytes = sum((len(a) for a in text)) sha1 = w._sha1s[i] print ' %8d %8d %8d %s' % (i, lines, bytes, sha1) total += bytes print >>out, "versions total %d bytes" % total print >>out, "compression ratio %.3f" % (float(total)/float(weave_size)) def main(argv): import sys import os from weavefile import write_weave_v1, read_weave cmd = argv[1] if cmd == 'add': w = read_weave(file(argv[2], 'rb')) # at the moment, based on everything in the file parents = set(range(len(w._v))) lines = sys.stdin.readlines() ver = w.add(parents, lines) write_weave_v1(w, file(argv[2], 'wb')) print 'added %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() write_weave_v1(w, file(fn, 'wb')) elif cmd == 'get': w = read_weave(file(argv[2], 'rb')) sys.stdout.writelines(w.get_iter(int(argv[3]))) elif cmd == 'annotate': w = read_weave(file(argv[2], 'rb')) # newline is added to all lines regardless; too hard to get # reasonable formatting otherwise lasto = None for origin, text in w.annotate(int(argv[3])): text = text.rstrip('\r\n') if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin elif cmd == 'info': weave_info(argv[2], sys.stdout) elif cmd == 'check': w = read_weave(file(argv[2], 'rb')) w.check() else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) M 644 inline weavefile.py data 4353 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Store and retrieve weaves in files. There is one format marker followed by a blank line, followed by a series of version headers, followed by the weave itself. Each version marker has 'i' and the included previous versions, then '1' and the SHA-1 of the text, if known. The inclusions do not need to list versions included by a parent. The weave is bracketed by 'w' and 'W' lines, and includes the '{}[]' processing instructions. Lines of text are prefixed by '.' if the line contains a newline, or ',' if not. """ # TODO: When extracting a single version it'd be enough to just pass # an iterator returning the weave lines... FORMAT_1 = '# bzr weave file v3\n' def write_weave(weave, f, format=None): if format == None or format == 1: return write_weave_v1(weave, f) else: raise ValueError("unknown weave format %r" % format) def write_weave_v1(weave, f): """Write weave to file f.""" print >>f, FORMAT_1, for version, included in enumerate(weave._v): if included: # find a minimal expression of it; bias towards using # later revisions li = list(included) li.sort() li.reverse() mininc = [] gotit = set() for pv in li: if pv not in gotit: mininc.append(pv) gotit.update(weave._v[pv]) assert mininc[0] >= 0 assert mininc[-1] < version print >>f, 'i', for i in mininc: print >>f, i, print >>f else: print >>f, 'i' print >>f, '1', weave._sha1s[version] print >>f print >>f, 'w' for l in weave._l: if isinstance(l, tuple): assert l[0] in '{}[]' print >>f, '%s %d' % l else: # text line if not l: print >>f, ', ' elif l[-1] == '\n': assert l.find('\n', 0, -1) == -1 print >>f, '.', l, else: assert l.find('\n') == -1 print >>f, ',', l print >>f, 'W' def read_weave(f): return read_weave_v1(f) def read_weave_v1(f): from weave import Weave, WeaveFormatError w = Weave() wfe = WeaveFormatError l = f.readline() if l != FORMAT_1: raise WeaveFormatError('invalid weave file header: %r' % l) ver = 0 while True: l = f.readline() if l[0] == 'i': ver += 1 if len(l) > 2: included = map(int, l[2:].split(' ')) full = set() for pv in included: full.add(pv) full.update(w._v[pv]) w._addversion(full) else: w._addversion(None) l = f.readline()[:-1] assert l.startswith('1 ') w._sha1s.append(l[2:]) l = f.readline() assert l == '\n' elif l == 'w\n': break else: raise WeaveFormatError('unexpected line %r' % l) while True: l = f.readline() if l == 'W\n': break elif l.startswith('. '): w._l.append(intern(l[2:])) # include newline elif l.startswith(', '): w._l.append(l[2:-1]) # exclude newline else: assert l[0] in '{}[]', l assert l[1] == ' ', l w._l.append((intern(l[0]), int(l[2:]))) return w commit refs/heads/master mark :852 committer Martin Pool 1120731223 +1000 data 4 todo from :851 merge :947 M 644 inline TODO data 13371 .. -*- mode: indented-text; compile-command: "make -C doc" -*- ******************* Things to do in bzr ******************* See also various low-level TODOs in the source code. Try looking in the list archive or on gmane.org for previous discussion of these issues. These are classified by approximate size: an hour or less, a day or less, and several days or more. Small things ------------ * Merging add of a new file clashing with an existing file doesn't work; add gets an error that it's already versioned and the merge aborts. * Merge should ignore the destination's working directory, otherwise we get an error about the statcache when pulling from a remote branch. * Add of a file that was present in the base revision should put back the previous file-id. * Handle diff of files which do not have a trailing newline; probably requires patching difflib to get it exactly right, or otherwise calling out to GNU diff. * -r option should take a revision-id as well as a revno. * ``bzr info`` should count only people with distinct email addresses as different committers. (Or perhaps only distinct userids?) * On Windows, command-line arguments should be `glob-expanded`__, because the shell doesn't do this. However, there are probably some commands where this shouldn't be done, such as 'bzr ignore', because we want to accept globs. * ``bzr ignore`` command that just adds a line to the ``.bzrignore`` file and makes it versioned. Fix this to break symlinks. * Any useful sanity checks in 'bzr ignore'? Perhaps give a warning if they try to add a single file which is already versioned, or if they add a pattern which already exists, or if it looks like they gave an unquoted glob. __ http://mail.python.org/pipermail/python-list/2001-April/037847.html * Separate read and write version checks? * ``bzr status DIR`` should give status on all files under that directory. * ``bzr log DIR`` should give changes to any files within DIR. * ``bzr inventory -r REV`` and perhaps unify this with ``bzr ls``, giving options to display ids, types, etc. * Split BzrError into various more specific subclasses for different errors people might want to catch. * If the export destination ends in '.tar', '.tar.gz', etc then create a tarball instead of a directory. (Need to actually make a temporary directory and then tar that up.) http://www.gelato.unsw.edu.au/archives/git/0504/2194.html * RemoteBranch could maintain a cache either in memory or on disk. We know more than an external cache might about which files are immutable and which can vary. On the other hand, it's much simpler to just use an external proxy cache. Perhaps ~/.bzr/http-cache. Baz has a fairly simple cache under ~/.arch-cache, containing revision information encoded almost as a bunch of archives. Perhaps we could simply store full paths. * Maybe also store directories in the statcache so that we can quickly identify that they still exist. * Diff should show timestamps; for files from the working directory we can use the file itself; for files from a revision we should use the commit time of the revision. * Perhaps split command infrastructure from the actual command definitions. * Cleaner support for negative boolean options like --no-recurse. * Statcache should possibly map all file paths to / separators * quotefn doubles all backslashes on Windows; this is probably not the best thing to do. What would be a better way to safely represent filenames? Perhaps we could doublequote things containing spaces, on the principle that filenames containing quotes are unlikely? Nice for humans; less good for machine parsing. * Patches should probably use only forward slashes, even on Windows, otherwise Unix patch can't apply them. (?) * Branch.update_revisions() inefficiently fetches revisions from the remote server twice; once to find out what text and inventory they need and then again to actually get the thing. This is a bit inefficient. One complicating factor here is that we don't really want to have revisions present in the revision-store until all their constituent parts are also stored. The basic problem is that RemoteBranch.get_revision() and similar methods return object, but what we really want is the raw XML, which can be popped into our own store. That needs to be refactored. * ``bzr status FOO`` where foo is ignored should say so. * ``bzr mkdir A...`` should just create and add A. * Guard against repeatedly merging any particular patch. Medium things ------------- * Merge revert patch. * ``bzr mv`` that does either rename or move as in Unix. * More efficient diff of only selected files. We should be able to just get the id for the selected files, look up their location and diff just those files. No need to traverse the entire inventories. * ``bzr status DIR`` or ``bzr diff DIR`` should report on all changes under that directory. * Fix up Inventory objects to represent root object as an entry. * Don't convert entire entry from ElementTree to an object when it is read in, but rather wait until the program actually wants to know about that node. * Extract changes from one revision to the next to a text form suitable for transmission over email. * More test cases. - Selected-file commit - Impossible selected-file commit: adding things in non-versioned directories, crossing renames, etc. * Write a reproducible benchmark, perhaps importing various kernel versions. * Directly import diffs! It seems a bit redundant to need to rescan the directory to work out what files diff added/deleted/changed when all the information is there in the diff in the first place. Getting the exact behaviour for added/deleted subdirectories etc might be hard. At the very least we could run diffstat over the diff, or perhaps read the status output from patch. Just knowing which files might be modified would be enough to guide the add and commit. Given this we might be able to import patches at 1/second or better. * Get branch over http. * Pull pure updates over http. * revfile compression. * Split inventory into per-directory files. * Fix ignore file parsing: - fnmatch is not the same as unix patterns - perhaps add extended globs from rsh/rsync - perhaps a pattern that matches only directories or non-directories * Consider using Python logging library as well as/instead of bzrlib.trace. * Commands should give some progress indication by default. - But quieten this with ``--silent``. * Change to using gettext message localization. * Make a clearer separation between internal and external bzrlib interfaces. Make internal interfaces use protected names. Write at least some documentation for those APIs, probably as docstrings. Consider using ZopeInterface definitions for the external interface; I think these are already used in PyBaz. They allow automatic checking of the interface but may be unfamiliar to general Python developers, so I'm not really keen. * Commands to dump out all command help into a manpage or HTML file or whatever. * Handle symlinks in the working directory; at the very least it should be possible for them to be present and ignored/unknown without causing assertion failures. Eventually symlinks should be versioned. * Allow init in a subdirectory to create a nested repository, but only if the subdirectory is not already versioned. Perhaps also require a ``--nested`` to protect against confusion. * Branch names? * More test framework: - Class that describes the state of a working tree so we can just assert it's equal. * There are too many methods on Branch() that really manipulate the WorkingTree. They should be moved across. Also there are some methods which are duplicated on Tree and Inventory objects, and it should be made more clear which ones are proxies and which ones behave differently, and how. * Try using XSLT to add some formatting to REST-generated HTML. Or maybe write a small Python program that specifies a header and foot for the pages and calls into the docutils libraries. * --format=xml for log, status and other commands. * Attempting to explicitly add a file that's already added should give a warning; however there should be no warning for directories (since we scan for new children) or files encountered in a directory that's being scanned. * Better handling of possible collisions on case-losing filesystems; make sure a single file does not get added twice under different names. * Clean up XML inventory: - Use nesting rather than parent_id pointers. - Hold the ElementTree in memory in the Inventory object and work directly on that, rather than converting into Python objects every time it is read in. Probably still exposoe it through some kind of object interface though, but perhaps that should just be a proxy for the elements. - Less special cases for the root directory. * Perhaps inventories should remember the revision in which each file was last changed, as well as its current state? This is a bit redundant but might often be interested to know. * stat cache should perhaps only stat files as necessary, rather than doing them all up-front. On the other hand, that disallows the opimization of stating them in inode order. * It'd be nice to pipeline multiple HTTP requests. Often we can predict what will be wanted in future: all revisions, or all texts in a particular revision, etc. urlgrabber's docs say they are working on batched downloads; we could perhaps ride on that or just create a background thread (ew). * Paranoid mode where we never trust SHA-1 matches. * Don't commit if there are no changes unless forced. * --dry-run mode for commit? (Or maybe just run with check-command=false?) * Generally, be a bit more verbose unless --silent is specified. * Function that finds all changes to files under a given directory; perhaps log should use this if a directory is given. * XML attributes might have trouble with filenames containing \n and \r. Do we really want to support this? I think perhaps not. * Remember execute bits, so that exports will work OK. * Unify smart_add and plain Branch.add(); perhaps smart_add should just build a list of files to add and pass that to the regular add function. * Function to list a directory, saying in which revision each file was last modified. Useful for web and gui interfaces, and slow to compute one file at a time. * unittest is standard, but the results are kind of ugly; would be nice to make it cleaner. * Check locking is correct during merge-related operations. * Perhaps attempts to get locks should timeout after some period of time, or at least display a progress message. * Split out upgrade functionality from check command into a separate ``bzr upgrade``. * Don't pass around command classes but rather pass objects. This'd make it cleaner to construct objects wrapping external commands. * Track all merged-in revisions in a versioned add-only metafile. Large things ------------ * Generate annotations from current file relative to previous annotations. - Is it necessary to store any kind of annotation where data was deleted? * Update revfile_ format and make it active: - Texts should be identified by something keyed on the revision, not an individual text-id. This is much more useful for annotate I think; we want to map back to the revision that last changed it. - Access revfile revisions through the Tree/Store classes. - Check them from check commands. - Store annotations. .. _revfile: revfile.html * Hooks for pre-commit, post-commit, etc. Consider the security implications; probably should not enable hooks for remotely-fetched branches by default. * Pre-commit check. If this hook is defined, it needs to be handled specially: create a temporary directory containing the tree as it will be after the commit. This means excluding any ignored/unknown files, and respecting selective commits. Run the pre-commit check (e.g. compile and run test suite) in there. Possibly this should be done by splitting the commit function into several parts (under a single interface). It is already rather large. Decomposition: - find tree modifications and prepare in-memory inventory - export that inventory to a temporary directory - run the test in that temporary directory - if that succeeded, continue to actually finish the commit What should be done with the text of modified files while this is underway? I don't think we want to count on holding them in memory and we can't trust the working files to stay in one place so I suppose we need to move them into the text store, or otherwise into a temporary directory. If the commit does not actually complete, we would rather the content was not left behind in the stores. * Web interface * GUI (maybe in Python GTK+?) * C library interface * Expansion of $Id$ keywords within working files. Perhaps do this in exports first as a simpler case because then we don't need to deal with removing the tags on the way back in. * ``bzr find`` commit refs/heads/master mark :949 committer Martin Pool 1120731277 +1000 data 17 prepare for merge from :947 R testsweet.py lib/testsweet.py D .bzrignore commit refs/heads/master mark :948 committer Martin Pool 1120731628 +1000 data 42 - merge in separately-developed weave code from :852 merge :949 M 644 inline convertfile.py data 2143 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Experiment in converting existing bzr branches to weaves.""" import sys import bzrlib.branch from weave import Weave from weavefile import write_weave import hotshot import tempfile def convert(): WEAVE_NAME = "test.weave" wf = Weave() toconvert = sys.argv[1] b = bzrlib.branch.find_branch(toconvert) rp = b.relpath(toconvert) print 'converting...' fid = b.read_working_inventory().path2id(rp) last_lines = None parents = set() revno = 0 for rev_id in b.revision_history(): revno += 1 print revno tree = b.revision_tree(rev_id) inv = tree.inventory if fid not in tree: print ' (not present)' continue text = tree.get_file(fid).readlines() if text == last_lines: continue last_lines = text weave_id = wf.add(parents, text) parents.add(weave_id) print ' %4d lines' % len(text) write_weave(wf, file(WEAVE_NAME, 'wb')) prof_f = tempfile.NamedTemporaryFile() prof = hotshot.Profile(prof_f.name) prof.runcall(convert) prof.close() import hotshot.stats stats = hotshot.stats.load(prof_f.name) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) M 644 inline convertinv.py data 1725 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Experiment in converting existing bzr branches to weaves.""" import bzrlib.branch from weave import Weave from weavefile import write_weave import tempfile import hotshot def convert(): WEAVE_NAME = "inventory.weave" wf = Weave() b = bzrlib.branch.find_branch('.') print 'converting...' parents = set() revno = 1 for rev_id in b.revision_history(): print revno inv_xml = b.inventory_store[rev_id].readlines() weave_id = wf.add(parents, inv_xml) parents.add(weave_id) revno += 1 write_weave(wf, file(WEAVE_NAME, 'wb')) prof_f = tempfile.NamedTemporaryFile() prof = hotshot.Profile(prof_f.name) prof.runcall(convert) prof.close() import hotshot.stats stats = hotshot.stats.load(prof_f.name) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) M 644 inline lib/testsweet.py data 9473 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from unittest import TestResult, TestCase def _need_subprocess(): sys.stderr.write("sorry, this test suite requires the subprocess module\n" "this is shipped with python2.4 and available separately for 2.3\n") class CommandFailed(Exception): pass class TestSkipped(Exception): """Indicates that a test was intentionally skipped, rather than failing.""" # XXX: Not used yet class TestBase(TestCase): """Base class for bzr test cases. Just defines some useful helper functions; doesn't actually test anything. """ # TODO: Special methods to invoke bzr, so that we can run it # through a specified Python intepreter OVERRIDE_PYTHON = None # to run with alternative python 'python' BZRPATH = 'bzr' _log_buf = "" def setUp(self): super(TestBase, self).setUp() self.log("%s setup" % self.id()) def tearDown(self): super(TestBase, self).tearDown() self.log("%s teardown" % self.id()) self.log('') def formcmd(self, cmd): if isinstance(cmd, basestring): cmd = cmd.split() if cmd[0] == 'bzr': cmd[0] = self.BZRPATH if self.OVERRIDE_PYTHON: cmd.insert(0, self.OVERRIDE_PYTHON) self.log('$ %r' % cmd) return cmd def runcmd(self, cmd, retcode=0): """Run one command and check the return code. Returns a tuple of (stdout,stderr) strings. If a single string is based, it is split into words. For commands that are not simple space-separated words, please pass a list instead.""" try: from subprocess import call, Popen, PIPE except ImportError, e: _need_subprocess raise cmd = self.formcmd(cmd) self.log('$ ' + ' '.join(cmd)) actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) def backtick(self, cmd, retcode=0): """Run a command and return its output""" try: from subprocess import call, Popen, PIPE except ImportError, e: _need_subprocess() raise cmd = self.formcmd(cmd) child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG) outd, errd = child.communicate() self.log(outd) actual_retcode = child.wait() outd = outd.replace('\r', '') if retcode != actual_retcode: raise CommandFailed("test failed: %r returned %d, expected %d" % (cmd, actual_retcode, retcode)) return outd def build_tree(self, shape): """Build a test tree according to a pattern. shape is a sequence of file specifications. If the final character is '/', a directory is created. This doesn't add anything to a branch. """ # XXX: It's OK to just create them using forward slashes on windows? import os for name in shape: assert isinstance(name, basestring) if name[-1] == '/': os.mkdir(name[:-1]) else: f = file(name, 'wt') print >>f, "contents of", name f.close() def log(self, msg): """Log a message to a progress file""" self._log_buf = self._log_buf + str(msg) + '\n' print >>self.TEST_LOG, msg def check_inventory_shape(self, inv, shape): """ Compare an inventory to a list of expected names. Fail if they are not precisely equal. """ extras = [] shape = list(shape) # copy for path, ie in inv.entries(): name = path.replace('\\', '/') if ie.kind == 'dir': name = name + '/' if name in shape: shape.remove(name) else: extras.append(name) if shape: self.fail("expected paths not found in inventory: %r" % shape) if extras: self.fail("unexpected paths found in inventory: %r" % extras) def check_file_contents(self, filename, expect): self.log("check contents of file %s" % filename) contents = file(filename, 'r').read() if contents != expect: self.log("expected: %r" % expected) self.log("actually: %r" % contents) self.fail("contents of %s not as expected") class InTempDir(TestBase): """Base class for tests run in a temporary branch.""" def setUp(self): import os self.test_dir = os.path.join(self.TEST_ROOT, self.__class__.__name__) os.mkdir(self.test_dir) os.chdir(self.test_dir) def tearDown(self): import os os.chdir(self.TEST_ROOT) class _MyResult(TestResult): """ Custom TestResult. No special behaviour for now. """ def __init__(self, out): self.out = out TestResult.__init__(self) def startTest(self, test): # TODO: Maybe show test.shortDescription somewhere? print >>self.out, '%-60.60s' % test.id(), self.out.flush() TestResult.startTest(self, test) def stopTest(self, test): # print TestResult.stopTest(self, test) def addError(self, test, err): print >>self.out, 'ERROR' TestResult.addError(self, test, err) _show_test_failure('error', test, err, self.out) def addFailure(self, test, err): print >>self.out, 'FAILURE' TestResult.addFailure(self, test, err) _show_test_failure('failure', test, err, self.out) def addSuccess(self, test): print >>self.out, 'OK' TestResult.addSuccess(self, test) def selftest(): from unittest import TestLoader, TestSuite import bzrlib import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning from doctest import DocTestSuite import os import shutil import time import sys suite = TestSuite() tl = TestLoader() for m in bzrlib.selftest.whitebox, \ bzrlib.selftest.versioning: suite.addTest(tl.loadTestsFromModule(m)) for m in bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, \ bzrlib.commands: suite.addTest(DocTestSuite(m)) suite.addTest(bzrlib.selftest.blackbox.suite()) return run_suite(suite) def run_suite(suite): import os import shutil import time import sys _setup_test_log() _setup_test_dir() print # save stdout & stderr so there's no leakage from code-under-test real_stdout = sys.stdout real_stderr = sys.stderr sys.stdout = sys.stderr = TestBase.TEST_LOG try: result = _MyResult(real_stdout) suite.run(result) finally: sys.stdout = real_stdout sys.stderr = real_stderr _show_results(result) return result.wasSuccessful() def _setup_test_log(): import time import os log_filename = os.path.abspath('test.log') TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered print >>TestBase.TEST_LOG, "tests run at " + time.ctime() print '%-30s %s' % ('test log', log_filename) def _setup_test_dir(): import os import shutil TestBase.ORIG_DIR = os.getcwdu() TestBase.TEST_ROOT = os.path.abspath("test.tmp") print '%-30s %s' % ('running tests in', TestBase.TEST_ROOT) if os.path.exists(TestBase.TEST_ROOT): shutil.rmtree(TestBase.TEST_ROOT) os.mkdir(TestBase.TEST_ROOT) os.chdir(TestBase.TEST_ROOT) # make a fake bzr directory there to prevent any tests propagating # up onto the source directory's real branch os.mkdir(os.path.join(TestBase.TEST_ROOT, '.bzr')) def _show_results(result): print print '%4d tests run' % result.testsRun print '%4d errors' % len(result.errors) print '%4d failures' % len(result.failures) def _show_test_failure(kind, case, exc_info, out): from traceback import print_exception print >>out, '-' * 60 print >>out, case desc = case.shortDescription() if desc: print >>out, ' (%s)' % desc print_exception(exc_info[0], exc_info[1], exc_info[2], None, out) if isinstance(case, TestBase): print >>out print >>out, 'log from this test:' print >>out, case._log_buf print >>out, '-' * 60 M 644 inline testweave.py data 18090 #! /usr/bin/python2.4 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test suite for weave algorithm""" import testsweet from weave import Weave, WeaveFormatError from pprint import pformat try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class TestBase(testsweet.TestBase): def check_read_write(self, k): """Check the weave k can be written & re-read.""" from tempfile import TemporaryFile from weavefile import write_weave, read_weave tf = TemporaryFile() write_weave(k, tf) tf.seek(0) k2 = read_weave(tf) if k != k2: tf.seek(0) self.log('serialized weave:') self.log(tf.read()) self.fail('read/write check failed') class Easy(TestBase): def runTest(self): k = Weave() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Weave() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class DeltaAdd(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): k = Weave() k.add([], ['line 1']) self.assertEqual(k._l, [('{', 0), 'line 1', ('}', 0), ]) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # currently there are 3 lines in the weave, and we insert after them self.assertEquals(changes, [(3, 3, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(1, 1, ['top line'])]) self.check_read_write(k) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Weave() self.assertRaises(IndexError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): k = Weave() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) text3 = ['line 1', 'middle line', 'line 2'] k.add([0, 1], text3) self.log("changes to text3: " + pformat(list(k._delta(set([0, 1]), text3)))) self.log("k._l=" + pformat(k._l)) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) # now multiple insertions at different places k.add([0, 1, 3], ['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc']) self.assertEqual(k.annotate(4), [(0, 'line 1'), (4, 'aaa'), (3, 'middle line'), (4, 'bbb'), (1, 'line 2'), (4, 'ccc')]) class DeleteLines(TestBase): """Deletion of lines from existing text. Try various texts all based on a common ancestor.""" def runTest(self): k = Weave() base_text = ['one', 'two', 'three', 'four'] k.add([], base_text) texts = [['one', 'two', 'three'], ['two', 'three', 'four'], ['one', 'four'], ['one', 'two', 'three', 'four'], ] for t in texts: ver = k.add([0], t) self.log('final weave:') self.log('k._l=' + pformat(k._l)) for i in range(len(texts)): self.assertEqual(k.get(i+1), texts[i]) class SuicideDelete(TestBase): """Invalid weave which tries to add and delete simultaneously.""" def runTest(self): k = Weave() k._v = [(), ] k._l = [('{', 0), 'first line', ('[', 0), 'deleted in 0', (']', 0), ('}', 0), ] self.assertRaises(WeaveFormatError, k.get, 0) class CannedDelete(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [(), frozenset([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'last line', ]) class CannedReplacement(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), ('{', 1), 'replacement line', ('}', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'replacement line', 'last line', ]) class BadWeave(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [frozenset(), ] k._l = ['bad line', ('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) class BadInsert(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0]), frozenset([0]), frozenset([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 1), ' more in 1', ('}', 1), ('}', 1), ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) self.assertRaises(WeaveFormatError, k.get, 1) class InsertNested(TestBase): """Insertion with nested instructions.""" def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0]), frozenset([0]), frozenset([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertEqual(k.get(0), ['foo {', '}']) self.assertEqual(k.get(1), ['foo {', ' added in version 1', ' also from v1', '}']) self.assertEqual(k.get(2), ['foo {', ' added in v2', '}']) self.assertEqual(k.get(3), ['foo {', ' added in version 1', ' added in v2', ' also from v1', '}']) class DeleteLines2(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Weave() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) self.assertEqual(k.annotate(1), [(0, "line the first"), (0, "fine")]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a weave with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0])] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1)] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Weave with two diverged texts based on version 0. """ def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0]), frozenset([0]), ] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1), ('{', 2), "alternative second line", ('}', 2), ] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) self.assertEqual(k.inclusions([2]), set([0, 2])) class ReplaceLine(TestBase): def runTest(self): k = Weave() text0 = ['cheddar', 'stilton', 'gruyere'] text1 = ['cheddar', 'blue vein', 'neufchatel', 'chevre'] k.add([], text0) k.add([0], text1) self.log('k._l=' + pformat(k._l)) self.assertEqual(k.get(0), text0) self.assertEqual(k.get(1), text1) class Merge(TestBase): """Storage of versions that merge diverged parents""" def runTest(self): k = Weave() texts = [['header'], ['header', '', 'line from 1'], ['header', '', 'line from 2', 'more from 2'], ['header', '', 'line from 1', 'fixup line', 'line from 2'], ] k.add([], texts[0]) k.add([0], texts[1]) k.add([0], texts[2]) k.add([0, 1, 2], texts[3]) for i, t in enumerate(texts): self.assertEqual(k.get(i), t) self.assertEqual(k.annotate(3), [(0, 'header'), (1, ''), (1, 'line from 1'), (3, 'fixup line'), (2, 'line from 2'), ]) self.assertEqual(k.inclusions([3]), set([0, 1, 2, 3])) self.log('k._l=' + pformat(k._l)) self.check_read_write(k) class Conflicts(TestBase): """Test detection of conflicting regions during a merge. A base version is inserted, then two descendents try to insert different lines in the same place. These should be reported as a possible conflict and forwarded to the user.""" def runTest(self): return # NOT RUN k = Weave() k.add([], ['aaa', 'bbb']) k.add([0], ['aaa', '111', 'bbb']) k.add([1], ['aaa', '222', 'bbb']) merged = k.merge([1, 2]) self.assertEquals([[['aaa']], [['111'], ['222']], [['bbb']]]) class NonConflict(TestBase): """Two descendants insert compatible changes. No conflict should be reported.""" def runTest(self): return # NOT RUN k = Weave() k.add([], ['aaa', 'bbb']) k.add([0], ['111', 'aaa', 'ccc', 'bbb']) k.add([1], ['aaa', 'ccc', 'bbb', '222']) class AutoMerge(TestBase): def runTest(self): k = Weave() texts = [['header', 'aaa', 'bbb'], ['header', 'aaa', 'line from 1', 'bbb'], ['header', 'aaa', 'bbb', 'line from 2', 'more from 2'], ] k.add([], texts[0]) k.add([0], texts[1]) k.add([0], texts[2]) self.log('k._l=' + pformat(k._l)) m = list(k.mash_iter([0, 1, 2])) self.assertEqual(m, ['header', 'aaa', 'line from 1', 'bbb', 'line from 2', 'more from 2']) class Khayyam(TestBase): """Test changes to multi-line texts, and read/write""" def runTest(self): rawtexts = [ """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, -- and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise enow!""", """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, -- and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now!""", """A Book of poems underneath the tree, A Jug of Wine, a Loaf of Bread, and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now! -- O. Khayyam""", """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now!""", ] texts = [[l.strip() for l in t.split('\n')] for t in rawtexts] k = Weave() parents = set() for t in texts: ver = k.add(list(parents), t) parents.add(ver) self.log("k._l=" + pformat(k._l)) for i, t in enumerate(texts): self.assertEqual(k.get(i), t) self.check_read_write(k) def testweave(): import testsweet from unittest import TestSuite, TestLoader import testweave tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testweave)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testweave()) M 644 inline weave.py data 19737 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? # TODO: Separate out some code to read and write weaves. # TODO: End marker for each version so we can stop reading? # TODO: Check that no insertion occurs inside a deletion that was # active in the version of the insertion. # TODO: Perhaps a special slower check() method that verifies more # nesting constraints and the MD5 of each version? try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. To clients the "lines" of the file are represented as a list of strings. These strings will typically have terminal newline characters, but this is not required. In particular files commonly do not have a newline at the end of the file. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the set (included_versions), which lists the previous versions also considered active; the versions included in those versions are included transitively. So new versions created from nothing list []; most versions have a single entry; some have more. _sha1s List of hex SHA-1 of each version, or None if not recorded. """ def __init__(self): self._l = [] self._v = [] self._sha1s = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. This must normally include the parents and the parent's parents, or wierd things might happen. text Sequence of lines to be added in the new version.""" ## self._check_versions(parents) ## self._check_lines(text) idx = len(self._v) import sha s = sha.new() for l in text: s.update(l) sha1 = s.hexdigest() del s if parents: delta = self._delta(self.inclusions(parents), text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._addversion(parents) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._addversion(None) self._sha1s.append(sha1) return idx def inclusions(self, versions): """Expand out everything included by versions.""" i = set(versions) for v in versions: i.update(self._v[v]) return i def _addversion(self, parents): if parents: self._v.append(frozenset(parents)) else: self._v.append(frozenset()) def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, version): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" included = self.inclusions([version]) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = None lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. # TODO: In fact, I think we only need to store the *count* of # active insertions and deletions, and we can maintain that by # just by just counting as we go along. WFE = WeaveFormatError for l in self._l: if isinstance(l, tuple): isactive = None # recalculate c, v = l if c == '{': if istack and (istack[-1] >= v): raise WFE("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WFE("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WFE("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WFE("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WFE("version %d deletes own text on line %d" % (v, lineno)) # XXX dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WFE("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WFE("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WFE("literal at top level on line %d" % lineno) if isactive == None: isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WFE("unclosed insertion blocks at end of weave", istack) if dset: raise WFE("unclosed deletion blocks at end of weave", dset) def get_iter(self, version): """Yield lines for the specified version.""" for origin, lineno, line in self._extract(self.inclusions([version])): yield line def get(self, index): return list(self.get_iter(index)) def mash_iter(self, included): """Return composed version of multiple included versions.""" included = frozenset(included) for origin, lineno, text in self._extract(included): yield text def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def numversions(self): l = len(self._v) assert l == len(self._sha1s) return l def check(self): # check no circular inclusions for version in range(self.numversions()): inclusions = list(self._v[version]) if inclusions: inclusions.sort() if inclusions[-1] >= version: raise WeaveFormatError("invalid included version %d for index %d" % (inclusions[-1], version)) # try extracting all versions; this is a bit slow and parallel # extraction could be used import sha for version in range(self.numversions()): s = sha.new() for l in self.get_iter(version): s.update(l) hd = s.hexdigest() expected = self._sha1s[version] if hd != expected: raise WeaveError("mismatched sha1 for version %d; " "got %s, expected %s" % (version, hd, expected)) def merge(self, merge_versions): """Automerge and mark conflicts between versions. This returns a sequence, each entry describing alternatives for a chunk of the file. Each of the alternatives is given as a list of lines. If there is a chunk of the file where there's no diagreement, only one alternative is given. """ # approach: find the included versions common to all the # merged versions raise NotImplementedError() def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ # basis a list of (origin, lineno, line) basis_lineno = [] basis_lines = [] for origin, lineno, line in self._extract(included): basis_lineno.append(lineno) basis_lines.append(line) # add a sentinal, because we can also match against the final line basis_lineno.append(len(self._l)) # XXX: which line of the weave should we really consider # matches the end of the file? the current code says it's the # last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis_lineno[i1] real_i2 = basis_lineno[i2] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def weave_info(filename, out): """Show some text information about the weave.""" from weavefile import read_weave wf = file(filename, 'rb') w = read_weave(wf) # FIXME: doesn't work on pipes weave_size = wf.tell() print >>out, "weave file size %d bytes" % weave_size print >>out, "weave contains %d versions" % len(w._v) total = 0 print ' %8s %8s %8s %s' % ('version', 'lines', 'bytes', 'sha1') print ' -------- -------- -------- ----------------------------------------' for i in range(len(w._v)): text = w.get(i) lines = len(text) bytes = sum((len(a) for a in text)) sha1 = w._sha1s[i] print ' %8d %8d %8d %s' % (i, lines, bytes, sha1) total += bytes print >>out, "versions total %d bytes" % total print >>out, "compression ratio %.3f" % (float(total)/float(weave_size)) def main(argv): import sys import os from weavefile import write_weave_v1, read_weave cmd = argv[1] if cmd == 'add': w = read_weave(file(argv[2], 'rb')) # at the moment, based on everything in the file parents = set(range(len(w._v))) lines = sys.stdin.readlines() ver = w.add(parents, lines) write_weave_v1(w, file(argv[2], 'wb')) print 'added %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() write_weave_v1(w, file(fn, 'wb')) elif cmd == 'get': w = read_weave(file(argv[2], 'rb')) sys.stdout.writelines(w.get_iter(int(argv[3]))) elif cmd == 'annotate': w = read_weave(file(argv[2], 'rb')) # newline is added to all lines regardless; too hard to get # reasonable formatting otherwise lasto = None for origin, text in w.annotate(int(argv[3])): text = text.rstrip('\r\n') if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin elif cmd == 'info': weave_info(argv[2], sys.stdout) elif cmd == 'check': w = read_weave(file(argv[2], 'rb')) w.check() else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) M 644 inline weavefile.py data 4353 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Store and retrieve weaves in files. There is one format marker followed by a blank line, followed by a series of version headers, followed by the weave itself. Each version marker has 'i' and the included previous versions, then '1' and the SHA-1 of the text, if known. The inclusions do not need to list versions included by a parent. The weave is bracketed by 'w' and 'W' lines, and includes the '{}[]' processing instructions. Lines of text are prefixed by '.' if the line contains a newline, or ',' if not. """ # TODO: When extracting a single version it'd be enough to just pass # an iterator returning the weave lines... FORMAT_1 = '# bzr weave file v3\n' def write_weave(weave, f, format=None): if format == None or format == 1: return write_weave_v1(weave, f) else: raise ValueError("unknown weave format %r" % format) def write_weave_v1(weave, f): """Write weave to file f.""" print >>f, FORMAT_1, for version, included in enumerate(weave._v): if included: # find a minimal expression of it; bias towards using # later revisions li = list(included) li.sort() li.reverse() mininc = [] gotit = set() for pv in li: if pv not in gotit: mininc.append(pv) gotit.update(weave._v[pv]) assert mininc[0] >= 0 assert mininc[-1] < version print >>f, 'i', for i in mininc: print >>f, i, print >>f else: print >>f, 'i' print >>f, '1', weave._sha1s[version] print >>f print >>f, 'w' for l in weave._l: if isinstance(l, tuple): assert l[0] in '{}[]' print >>f, '%s %d' % l else: # text line if not l: print >>f, ', ' elif l[-1] == '\n': assert l.find('\n', 0, -1) == -1 print >>f, '.', l, else: assert l.find('\n') == -1 print >>f, ',', l print >>f, 'W' def read_weave(f): return read_weave_v1(f) def read_weave_v1(f): from weave import Weave, WeaveFormatError w = Weave() wfe = WeaveFormatError l = f.readline() if l != FORMAT_1: raise WeaveFormatError('invalid weave file header: %r' % l) ver = 0 while True: l = f.readline() if l[0] == 'i': ver += 1 if len(l) > 2: included = map(int, l[2:].split(' ')) full = set() for pv in included: full.add(pv) full.update(w._v[pv]) w._addversion(full) else: w._addversion(None) l = f.readline()[:-1] assert l.startswith('1 ') w._sha1s.append(l[2:]) l = f.readline() assert l == '\n' elif l == 'w\n': break else: raise WeaveFormatError('unexpected line %r' % l) while True: l = f.readline() if l == 'W\n': break elif l.startswith('. '): w._l.append(intern(l[2:])) # include newline elif l.startswith(', '): w._l.append(l[2:-1]) # exclude newline else: assert l[0] in '{}[]', l assert l[1] == ' ', l w._l.append((intern(l[0]), int(l[2:]))) return w commit refs/heads/master mark :950 committer Martin Pool 1120731722 +1000 data 37 - rearrange and clear up merged weave from :948 R convertfile.py tools/convertfile.py R convertinv.py tools/convertinv.py R testweave.py tools/testweave.py R weave.py bzrlib/weave.py R weavefile.py bzrlib/weavefile.py D lib D lib/testsweet.py commit refs/heads/master mark :951 committer Martin Pool 1120732087 +1000 data 58 - Patch from John to allow plugins to add their own tests. from :950 M 644 inline bzrlib/selftest/__init__.py data 2791 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from testsweet import TestBase, run_suite, InTempDir MODULES_TO_TEST = [] MODULES_TO_DOCTEST = [] def selftest(): from unittest import TestLoader, TestSuite import bzrlib, bzrlib.store, bzrlib.inventory, bzrlib.branch import bzrlib.osutils, bzrlib.commands, bzrlib.merge3 global MODULES_TO_TEST, MODULES_TO_DOCTEST import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning import bzrlib.selftest.testmerge3 import bzrlib.selftest.testhashcache import bzrlib.merge_core from doctest import DocTestSuite import os import shutil import time import sys import unittest for m in (bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, bzrlib.commands, bzrlib.merge3): if m not in MODULES_TO_DOCTEST: MODULES_TO_DOCTEST.append(m) for m in (bzrlib.selftest.whitebox, bzrlib.selftest.versioning, bzrlib.selftest.testmerge3): if m not in MODULES_TO_TEST: MODULES_TO_TEST.append(m) TestBase.BZRPATH = os.path.join(os.path.realpath(os.path.dirname(bzrlib.__path__[0])), 'bzr') print '%-30s %s' % ('bzr binary', TestBase.BZRPATH) print suite = TestSuite() # should also test bzrlib.merge_core, but they seem to be out of date with # the code. # python2.3's TestLoader() doesn't seem to work well; don't know why for m in MODULES_TO_TEST: suite.addTest(TestLoader().loadTestsFromModule(m)) for m in (MODULES_TO_DOCTEST): suite.addTest(DocTestSuite(m)) # for cl in (bzrlib.selftest.whitebox.TEST_CLASSES # + bzrlib.selftest.versioning.TEST_CLASSES # + bzrlib.selftest.testmerge3.TEST_CLASSES # + bzrlib.selftest.testhashcache.TEST_CLASSES # + bzrlib.selftest.blackbox.TEST_CLASSES): # suite.addTest(cl()) suite.addTest(unittest.makeSuite(bzrlib.merge_core.MergeTest, 'test_')) return run_suite(suite, 'testbzr') commit refs/heads/master mark :952 committer Martin Pool 1120732296 +1000 data 14 - fix pwk help from :951 M 644 inline contrib/pwk data 951 #! /bin/sh -pe # take patches from patchwork into bzr # authentication must be in ~/.netrc # TODO: Scan all pending patches and say which ones apply cleanly. # these should be moved into some kind of per-project configuration PWK_ROOT='http://patchwork.ozlabs.org/bazaar-ng' PWK_AUTH_ROOT='https://patchwork.ozlabs.org/bazaar-ng' # bzr uses -p0 style; others use -p1 PATCH_OPTS='-p0' usage() { cat < 1120733787 +1000 data 82 - If export filename ends in .tar, etc, then make a tarball instead of a directory from :952 M 644 inline bzrlib/commands.py data 54200 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import BzrError, BzrCheckError, BzrCommandError from bzrlib.branch import find_branch from bzrlib import BZRDIR plugin_cmds = {} def register_command(cmd): "Utility function to help register a command" global plugin_cmds k = cmd.__name__ if k.startswith("cmd_"): k_unsquished = _unsquish_command_name(k) else: k_unsquished = k if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = cmd else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r' % sys.modules[cmd.__module__]) def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _get_cmd_dict(plugins_override=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v # If we didn't load plugins, the plugin_cmds dict will be empty if plugins_override: d.update(plugin_cmds) else: d2 = plugin_cmds.copy() d2.update(d) d = d2 return d def get_all_cmds(plugins_override=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(plugins_override=plugins_override).iteritems(): yield k,v def get_cmd_class(cmd, plugins_override=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(plugins_override=plugins_override) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() for opt in self.takes_options: if not opt in OPTIONS: raise BzrError("Unknown option '%s' returned by external command %s" % (opt, path)) # TODO: Is there any way to check takes_args is valid here? self.takes_args = pipe.readline().split() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-usage'" % path) pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-help'" % path) def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: optname = name.replace('_','-') value = kargs[name] if OPTIONS.has_key(optname): # it's an option opts.append('--%s' % optname) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = find_branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): from bzrlib.xml import pack_xml pack_xml(find_branch('.').get_revision(revision_id), sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print find_branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): from bzrlib.add import smart_add smart_add(file_list, verbose, not no_recurse) class cmd_mkdir(Command): """Create a new versioned directory. This is equivalent to creating the directory and then adding it. """ takes_args = ['dir+'] def run(self, dir_list): b = None for d in dir_list: os.mkdir(d) if not b: b = find_branch(d) b.add([d], verbose=True) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print find_branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = find_branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = find_branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = find_branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import tempfile from shutil import rmtree import errno br_to = find_branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if e.errno != errno.ENOENT: raise if location is None: if stored_loc is None: raise BzrCommandError("No pull location known or specified.") else: print "Using last location: %s" % stored_loc location = stored_loc cache_root = tempfile.mkdtemp() from bzrlib.branch import DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: from branch import find_cached_branch, DivergedBranches br_from = find_cached_branch(location, cache_root) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged." " Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") finally: rmtree(cache_root) class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. To retrieve the branch as of a particular revision, supply the --revision parameter, as in "branch foo/bar -r 5". """ takes_args = ['from_location', 'to_location?'] takes_options = ['revision'] def run(self, from_location, to_location=None, revision=None): import errno from bzrlib.merge import merge from bzrlib.branch import DivergedBranches, NoSuchRevision, \ find_cached_branch, Branch from shutil import rmtree from meta_store import CachedStore import tempfile cache_root = tempfile.mkdtemp() try: try: br_from = find_cached_branch(from_location, cache_root) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not' ' exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location.rstrip("/\\")) try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already' ' exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) try: br_to.update_revisions(br_from, stop_revision=revision) except NoSuchRevision: rmtree(to_location) msg = "The branch %s has no revision %d." % (from_location, revision) raise BzrCommandError(msg) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False, ignore_zero=True) from_location = pull_loc(br_from) br_to.controlfile("x-pull", "wb").write(from_location + "\n") finally: rmtree(cache_root) def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = find_branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = find_branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = find_branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: raise BzrError("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = find_branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: raise BzrError("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in find_branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in find_branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): from bzrlib.branch import Branch Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = find_branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = find_branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): from bzrlib.statcache import update_cache, SC_SHA1 b = find_branch('.') inv = b.read_working_inventory() sc = update_cache(b, inv) basis = b.basis_tree() basis_inv = basis.inventory # We used to do this through iter_entries(), but that's slow # when most of the files are unmodified, as is usually the # case. So instead we iterate by inventory entry, and only # calculate paths as necessary. for file_id in basis_inv: cacheentry = sc.get(file_id) if not cacheentry: # deleted continue ie = basis_inv[file_id] if cacheentry[SC_SHA1] != ie.text_sha1: path = inv.id2path(file_id) print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = find_branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision','long'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None, long=False): from bzrlib.branch import find_branch from bzrlib.log import log_formatter, show_log import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') if long: log_format = 'long' else: log_format = 'short' lf = log_formatter(log_format, show_ids=show_ids, to_file=outf, show_timezone=timezone) show_log(b, lf, file_id, verbose=verbose, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = find_branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = find_branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): from bzrlib.osutils import quotefn for f in find_branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = find_branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = find_branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print find_branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, try to find the format with the extension. If no extension is found exports to a directory (equivalent to --format=dir). Root may be the top directory for tar, tgz and tbz2 formats. If none is given, the top directory will be the root name of the file.""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format', 'root'] def run(self, dest, revision=None, format=None, root=None): import os.path b = find_branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) root, ext = os.path.splitext(dest) if not format: if ext in (".tar",): format = "tar" elif ext in (".gz", ".tgz"): format = "tgz" elif ext in (".bz2", ".tbz2"): format = "tbz2" else: format = "dir" t.export(dest, format, root) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = find_branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit from bzrlib.osutils import get_text_message ## Warning: shadows builtin file() if not message and not file: import cStringIO stdout = sys.stdout catcher = cStringIO.StringIO() sys.stdout = catcher cmd_status({"file_list":selected_list}, {}) info = catcher.getvalue() sys.stdout = stdout message = get_text_message(info) if message is None: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = find_branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.check import check check(find_branch(dir)) class cmd_upgrade(Command): """Upgrade branch storage to current format. This should normally be used only after the check command tells you to run it. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.upgrade import upgrade upgrade(find_branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest return int(not selftest()) class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Restore selected files from a previous revision. """ takes_args = ['file+'] def run(self, file_list): from bzrlib.branch import find_branch if not file_list: file_list = ['.'] b = find_branch(file_list[0]) b.revert([b.relpath(f) for f in file_list]) class cmd_merge_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): from bzrlib.statcache import update_cache b = find_branch('.') update_cache(b.base, b.read_working_inventory()) class cmd_plugins(Command): """List plugins""" hidden = True def run(self): import bzrlib.plugin from pprint import pprint pprint(bzrlib.plugin.all_plugins) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'update': None, 'long': None, 'root': str, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', 'l': 'long', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) >>> parse_args('log -r 500'.split()) (['log'], {'revision': 500}) >>> parse_args('log -r500:600'.split()) (['log'], {'revision': [500, 600]}) >>> parse_args('log -vr500:600'.split()) (['log'], {'verbose': True, 'revision': [500, 600]}) >>> parse_args('log -rv500:600'.split()) #the r takes an argument Traceback (most recent call last): ... ValueError: invalid literal for int(): v500 """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: raise BzrError('unknown long option %r' % a) else: shortopt = a[1:] if shortopt in SHORT_OPTIONS: # Multi-character options must have a space to delimit # their value optname = SHORT_OPTIONS[shortopt] else: # Single character short options, can be chained, # and have their value appended to their name shortopt = a[1:2] if shortopt not in SHORT_OPTIONS: # We didn't find the multi-character name, and we # didn't find the single char name raise BzrError('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if a[2:]: # There are extra things on this option # see if it is the value, or if it is another # short option optargfn = OPTIONS[optname] if optargfn is None: # This option does not take an argument, so the # next entry is another short option, pack it back # into the list argv.insert(0, '-' + a[2:]) else: # This option takes an argument, so pack it # into the array optarg = a[2:] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? raise BzrError('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: raise BzrError('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: raise BzrError('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def _parse_master_args(argv): """Parse the arguments that always go with the original command. These are things like bzr --no-plugins, etc. There are now 2 types of option flags. Ones that come *before* the command, and ones that come *after* the command. Ones coming *before* the command are applied against all possible commands. And are generally applied before plugins are loaded. The current list are: --builtin Allow plugins to load, but don't let them override builtin commands, they will still be allowed if they do not override a builtin. --no-plugins Don't load any plugins. This lets you get back to official source behavior. --profile Enable the hotspot profile before running the command. For backwards compatibility, this is also a non-master option. --version Spit out the version of bzr that is running and exit. This is also a non-master option. --help Run help and exit, also a non-master option (I think that should stay, though) >>> argv, opts = _parse_master_args(['bzr', '--test']) Traceback (most recent call last): ... BzrCommandError: Invalid master option: 'test' >>> argv, opts = _parse_master_args(['bzr', '--version', 'command']) >>> print argv ['command'] >>> print opts['version'] True >>> argv, opts = _parse_master_args(['bzr', '--profile', 'command', '--more-options']) >>> print argv ['command', '--more-options'] >>> print opts['profile'] True >>> argv, opts = _parse_master_args(['bzr', '--no-plugins', 'command']) >>> print argv ['command'] >>> print opts['no-plugins'] True >>> print opts['profile'] False >>> argv, opts = _parse_master_args(['bzr', 'command', '--profile']) >>> print argv ['command', '--profile'] >>> print opts['profile'] False """ master_opts = {'builtin':False, 'no-plugins':False, 'version':False, 'profile':False, 'help':False } # This is the point where we could hook into argv[0] to determine # what front-end is supposed to be run # For now, we are just ignoring it. cmd_name = argv.pop(0) for arg in argv[:]: if arg[:2] != '--': # at the first non-option, we return the rest break arg = arg[2:] # Remove '--' if arg not in master_opts: # We could say that this is not an error, that we should # just let it be handled by the main section instead raise BzrCommandError('Invalid master option: %r' % arg) argv.pop(0) # We are consuming this entry master_opts[arg] = True return argv, master_opts def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: # some options like --builtin and --no-plugins have special effects argv, master_opts = _parse_master_args(argv) if not master_opts['no-plugins']: from bzrlib.plugin import load_plugins load_plugins() args, opts = parse_args(argv) if master_opts['help']: from bzrlib.help import help if argv: help(argv[0]) else: help() return 0 if 'help' in opts: from bzrlib.help import help if args: help(args[0]) else: help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 plugins_override = not (master_opts['builtin']) canonical_cmd, cmd_class = get_cmd_class(cmd, plugins_override=plugins_override) profile = master_opts['profile'] # For backwards compatibility, I would rather stick with --profile being a # master/global option if 'profile' in opts: profile = True del opts['profile'] # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): bzrlib.trace.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: import errno quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :954 committer Martin Pool 1120784641 +1000 data 27 - re-enable hashcache tests from :953 M 644 inline bzrlib/selftest/__init__.py data 2869 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from testsweet import TestBase, run_suite, InTempDir MODULES_TO_TEST = [] MODULES_TO_DOCTEST = [] def selftest(): from unittest import TestLoader, TestSuite import bzrlib, bzrlib.store, bzrlib.inventory, bzrlib.branch import bzrlib.osutils, bzrlib.commands, bzrlib.merge3 global MODULES_TO_TEST, MODULES_TO_DOCTEST import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning import bzrlib.selftest.testmerge3 import bzrlib.selftest.testhashcache import bzrlib.merge_core from doctest import DocTestSuite import os import shutil import time import sys import unittest for m in (bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, bzrlib.commands, bzrlib.merge3): if m not in MODULES_TO_DOCTEST: MODULES_TO_DOCTEST.append(m) for m in (bzrlib.selftest.whitebox, bzrlib.selftest.versioning, bzrlib.selftest.testmerge3, bzrlib.selftest.testhashcache): if m not in MODULES_TO_TEST: MODULES_TO_TEST.append(m) TestBase.BZRPATH = os.path.join(os.path.realpath(os.path.dirname(bzrlib.__path__[0])), 'bzr') print '%-30s %s' % ('bzr binary', TestBase.BZRPATH) print suite = TestSuite() # should also test bzrlib.merge_core, but they seem to be out of date with # the code. # XXX: python2.3's TestLoader() doesn't seem to find all the # tests; don't know why for m in MODULES_TO_TEST: suite.addTest(TestLoader().loadTestsFromModule(m)) for m in (MODULES_TO_DOCTEST): suite.addTest(DocTestSuite(m)) # for cl in (bzrlib.selftest.whitebox.TEST_CLASSES # + bzrlib.selftest.versioning.TEST_CLASSES # + bzrlib.selftest.testmerge3.TEST_CLASSES # + bzrlib.selftest.testhashcache.TEST_CLASSES # + bzrlib.selftest.blackbox.TEST_CLASSES): # suite.addTest(cl()) suite.addTest(unittest.makeSuite(bzrlib.merge_core.MergeTest, 'test_')) return run_suite(suite, 'testbzr') M 644 inline bzrlib/selftest/testhashcache.py data 2866 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from bzrlib.selftest import InTempDir def sha1(t): import sha return sha.new(t).hexdigest() def pause(): import time # allow it to stabilize start = int(time.time()) while int(time.time()) == start: time.sleep(0.2) class TestStatCache(InTempDir): """Functional tests for statcache""" def runTest(self): from bzrlib.hashcache import HashCache import os import time hc = HashCache('.') file('foo', 'wb').write('hello') os.mkdir('subdir') pause() self.assertEquals(hc.get_sha1('foo'), 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d') self.assertEquals(hc.miss_count, 1) self.assertEquals(hc.hit_count, 0) # check we hit without re-reading self.assertEquals(hc.get_sha1('foo'), 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d') self.assertEquals(hc.miss_count, 1) self.assertEquals(hc.hit_count, 1) # check again without re-reading self.assertEquals(hc.get_sha1('foo'), 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d') self.assertEquals(hc.miss_count, 1) self.assertEquals(hc.hit_count, 2) # write new file and make sure it is seen file('foo', 'wb').write('goodbye') pause() self.assertEquals(hc.get_sha1('foo'), '3c8ec4874488f6090a157b014ce3397ca8e06d4f') self.assertEquals(hc.miss_count, 2) # quickly write new file of same size and make sure it is seen # this may rely on detection of timestamps that are too close # together to be safe file('foo', 'wb').write('g00dbye') self.assertEquals(hc.get_sha1('foo'), sha1('g00dbye')) # this is not quite guaranteed to be true; we might have # crossed a 1s boundary before self.assertEquals(hc.danger_count, 1) self.assertEquals(hc.get_sha1('subdir'), None) #hc.write('stat-cache') #del hc TEST_CLASSES = [ TestStatCache, ] commit refs/heads/master mark :955 committer Martin Pool 1120788790 +1000 data 47 - add HashCache.write and a simple test for it from :954 M 644 inline bzrlib/hashcache.py data 4600 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA CACHE_HEADER = "### bzr statcache v5" def _fingerprint(abspath): import os, stat try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) class HashCache(object): """Cache for looking up file SHA-1. Files are considered to match the cached value if the fingerprint of the file has not changed. This includes its mtime, ctime, device number, inode number, and size. This should catch modifications or replacement of the file by a new one. This may not catch modifications that do not change the file's size and that occur within the resolution window of the timestamps. To handle this we specifically do not cache files which have changed since the start of the present second, since they could undetectably change again. This scheme may fail if the machine's clock steps backwards. Don't do that. This does not canonicalize the paths passed in; that should be done by the caller. cache_sha1 Indexed by path, gives the SHA-1 of the file. validator Indexed by path, gives the fingerprint of the file last time it was read. stat_count number of times files have been statted hit_count number of times files have been retrieved from the cache, avoiding a re-read miss_count number of misses (times files have been completely re-read) """ def __init__(self, basedir): self.basedir = basedir self.hit_count = 0 self.miss_count = 0 self.stat_count = 0 self.danger_count = 0 self.cache_sha1 = {} self.validator = {} def clear(self): """Discard all cached information.""" self.validator = {} self.cache_sha1 = {} def get_sha1(self, path): """Return the hex SHA-1 of the contents of the file at path. XXX: If the file does not exist or is not a plain file??? """ import os, time from bzrlib.osutils import sha_file abspath = os.path.join(self.basedir, path) fp = _fingerprint(abspath) cache_fp = self.validator.get(path) self.stat_count += 1 if not fp: # not a regular file return None elif cache_fp and (cache_fp == fp): self.hit_count += 1 return self.cache_sha1[path] else: self.miss_count += 1 digest = sha_file(file(abspath, 'rb')) now = int(time.time()) if fp[1] >= now or fp[2] >= now: # changed too recently; can't be cached. we can # return the result and it could possibly be cached # next time. self.danger_count += 1 if cache_fp: del self.validator[path] del self.cache_sha1[path] else: self.validator[path] = fp self.cache_sha1[path] = digest return digest def write(self, cachefn): """Write contents of cache to file.""" from atomicfile import AtomicFile outf = AtomicFile(cachefn, 'wb') try: outf.write(CACHE_HEADER + '\n') for path in self.cache_sha1: assert '//' not in path, path outf.write(path.encode('utf-8')) outf.write('// ') print >>outf, self.cache_sha1[path], for fld in self.validator[path]: print >>outf, fld, print >>outf outf.commit() finally: if not outf.closed: outf.abort() M 644 inline bzrlib/selftest/testhashcache.py data 2805 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from bzrlib.selftest import InTempDir def sha1(t): import sha return sha.new(t).hexdigest() def pause(): import time # allow it to stabilize start = int(time.time()) while int(time.time()) == start: time.sleep(0.2) class TestHashCache(InTempDir): """Functional tests for statcache""" def runTest(self): from bzrlib.hashcache import HashCache import os import time hc = HashCache('.') file('foo', 'wb').write('hello') os.mkdir('subdir') pause() self.assertEquals(hc.get_sha1('foo'), 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d') self.assertEquals(hc.miss_count, 1) self.assertEquals(hc.hit_count, 0) # check we hit without re-reading self.assertEquals(hc.get_sha1('foo'), 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d') self.assertEquals(hc.miss_count, 1) self.assertEquals(hc.hit_count, 1) # check again without re-reading self.assertEquals(hc.get_sha1('foo'), 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d') self.assertEquals(hc.miss_count, 1) self.assertEquals(hc.hit_count, 2) # write new file and make sure it is seen file('foo', 'wb').write('goodbye') pause() self.assertEquals(hc.get_sha1('foo'), '3c8ec4874488f6090a157b014ce3397ca8e06d4f') self.assertEquals(hc.miss_count, 2) hc.write('stat-cache') # quickly write new file of same size and make sure it is seen # this may rely on detection of timestamps that are too close # together to be safe file('foo', 'wb').write('g00dbye') self.assertEquals(hc.get_sha1('foo'), sha1('g00dbye')) # this is not quite guaranteed to be true; we might have # crossed a 1s boundary before self.assertEquals(hc.danger_count, 1) self.assertEquals(hc.get_sha1('subdir'), None) commit refs/heads/master mark :956 committer Martin Pool 1120789273 +1000 data 48 - refactor hashcache to use just one dictionary from :955 M 644 inline bzrlib/hashcache.py data 4527 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA CACHE_HEADER = "### bzr statcache v5" def _fingerprint(abspath): import os, stat try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) class HashCache(object): """Cache for looking up file SHA-1. Files are considered to match the cached value if the fingerprint of the file has not changed. This includes its mtime, ctime, device number, inode number, and size. This should catch modifications or replacement of the file by a new one. This may not catch modifications that do not change the file's size and that occur within the resolution window of the timestamps. To handle this we specifically do not cache files which have changed since the start of the present second, since they could undetectably change again. This scheme may fail if the machine's clock steps backwards. Don't do that. This does not canonicalize the paths passed in; that should be done by the caller. _cache Indexed by path, points to a two-tuple of the SHA-1 of the file. and its fingerprint. stat_count number of times files have been statted hit_count number of times files have been retrieved from the cache, avoiding a re-read miss_count number of misses (times files have been completely re-read) """ def __init__(self, basedir): self.basedir = basedir self.hit_count = 0 self.miss_count = 0 self.stat_count = 0 self.danger_count = 0 self._cache = {} def clear(self): """Discard all cached information. This does not reset the counters.""" self._cache_sha1 = {} def get_sha1(self, path): """Return the hex SHA-1 of the contents of the file at path. XXX: If the file does not exist or is not a plain file??? """ import os, time from bzrlib.osutils import sha_file abspath = os.path.join(self.basedir, path) fp = _fingerprint(abspath) c = self._cache.get(path) if c: cache_sha1, cache_fp = c else: cache_sha1, cache_fp = None, None self.stat_count += 1 if not fp: # not a regular file return None elif cache_fp and (cache_fp == fp): self.hit_count += 1 return cache_sha1 else: self.miss_count += 1 digest = sha_file(file(abspath, 'rb')) now = int(time.time()) if fp[1] >= now or fp[2] >= now: # changed too recently; can't be cached. we can # return the result and it could possibly be cached # next time. self.danger_count += 1 if cache_fp: del self._cache[path] else: self._cache[path] = (digest, fp) return digest def write(self, cachefn): """Write contents of cache to file.""" from atomicfile import AtomicFile outf = AtomicFile(cachefn, 'wb') try: outf.write(CACHE_HEADER + '\n') for path, c in self._cache.iteritems(): assert '//' not in path, path outf.write(path.encode('utf-8')) outf.write('// ') print >>outf, c[0], # hex sha1 for fld in c[1]: print >>outf, fld, print >>outf outf.commit() finally: if not outf.closed: outf.abort() commit refs/heads/master mark :957 committer Martin Pool 1120789738 +1000 data 24 - more hash-cache tests from :956 M 644 inline bzrlib/selftest/testhashcache.py data 3223 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from bzrlib.selftest import InTempDir def sha1(t): import sha return sha.new(t).hexdigest() def pause(): import time # allow it to stabilize start = int(time.time()) while int(time.time()) == start: time.sleep(0.2) class TestHashCache(InTempDir): """Functional tests for statcache""" def runTest(self): from bzrlib.hashcache import HashCache import os import time hc = HashCache('.') file('foo', 'wb').write('hello') os.mkdir('subdir') pause() self.assertEquals(hc.get_sha1('foo'), 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d') self.assertEquals(hc.miss_count, 1) self.assertEquals(hc.hit_count, 0) # check we hit without re-reading self.assertEquals(hc.get_sha1('foo'), 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d') self.assertEquals(hc.miss_count, 1) self.assertEquals(hc.hit_count, 1) # check again without re-reading self.assertEquals(hc.get_sha1('foo'), 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d') self.assertEquals(hc.miss_count, 1) self.assertEquals(hc.hit_count, 2) # write new file and make sure it is seen file('foo', 'wb').write('goodbye') pause() self.assertEquals(hc.get_sha1('foo'), '3c8ec4874488f6090a157b014ce3397ca8e06d4f') self.assertEquals(hc.miss_count, 2) # quickly write new file of same size and make sure it is seen # this may rely on detection of timestamps that are too close # together to be safe file('foo', 'wb').write('g00dbye') self.assertEquals(hc.get_sha1('foo'), sha1('g00dbye')) # this is not quite guaranteed to be true; we might have # crossed a 1s boundary before self.assertEquals(hc.danger_count, 1) self.assertEquals(len(hc._cache), 0) self.assertEquals(hc.get_sha1('subdir'), None) pause() # should now be safe to cache it self.assertEquals(hc.get_sha1('foo'), sha1('g00dbye')) self.assertEquals(len(hc._cache), 1) # write out, read back in and check that we don't need to # re-read any files hc.write('stat-cache') del hc hc = HashCache('.') # hc.read('stat-cache') commit refs/heads/master mark :958 committer Martin Pool 1120790422 +1000 data 37 - code to re-read hashcache from file from :957 M 644 inline bzrlib/hashcache.py data 5680 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA CACHE_HEADER = "### bzr statcache v5\n" def _fingerprint(abspath): import os, stat try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) class HashCache(object): """Cache for looking up file SHA-1. Files are considered to match the cached value if the fingerprint of the file has not changed. This includes its mtime, ctime, device number, inode number, and size. This should catch modifications or replacement of the file by a new one. This may not catch modifications that do not change the file's size and that occur within the resolution window of the timestamps. To handle this we specifically do not cache files which have changed since the start of the present second, since they could undetectably change again. This scheme may fail if the machine's clock steps backwards. Don't do that. This does not canonicalize the paths passed in; that should be done by the caller. _cache Indexed by path, points to a two-tuple of the SHA-1 of the file. and its fingerprint. stat_count number of times files have been statted hit_count number of times files have been retrieved from the cache, avoiding a re-read miss_count number of misses (times files have been completely re-read) """ def __init__(self, basedir): self.basedir = basedir self.hit_count = 0 self.miss_count = 0 self.stat_count = 0 self.danger_count = 0 self._cache = {} def clear(self): """Discard all cached information. This does not reset the counters.""" self._cache_sha1 = {} def get_sha1(self, path): """Return the hex SHA-1 of the contents of the file at path. XXX: If the file does not exist or is not a plain file??? """ import os, time from bzrlib.osutils import sha_file abspath = os.path.join(self.basedir, path) fp = _fingerprint(abspath) c = self._cache.get(path) if c: cache_sha1, cache_fp = c else: cache_sha1, cache_fp = None, None self.stat_count += 1 if not fp: # not a regular file return None elif cache_fp and (cache_fp == fp): self.hit_count += 1 return cache_sha1 else: self.miss_count += 1 digest = sha_file(file(abspath, 'rb')) now = int(time.time()) if fp[1] >= now or fp[2] >= now: # changed too recently; can't be cached. we can # return the result and it could possibly be cached # next time. self.danger_count += 1 if cache_fp: del self._cache[path] else: self._cache[path] = (digest, fp) return digest def write(self, cachefn): """Write contents of cache to file.""" from atomicfile import AtomicFile outf = AtomicFile(cachefn, 'wb') try: print >>outf, CACHE_HEADER, for path, c in self._cache.iteritems(): assert '//' not in path, path outf.write(path.encode('utf-8')) outf.write('// ') print >>outf, c[0], # hex sha1 for fld in c[1]: print >>outf, "%d" % fld, print >>outf outf.commit() finally: if not outf.closed: outf.abort() def read(self, cachefn): """Reinstate cache from file. Overwrites existing cache. If the cache file has the wrong version marker, this just clears the cache.""" from bzrlib.trace import mutter, warning inf = file(cachefn, 'rb') self._cache = {} hdr = inf.readline() if hdr != CACHE_HEADER: mutter('cache header marker not found at top of %s; discarding cache' % cachefn) return for l in inf: pos = l.index('// ') path = l[:pos].decode('utf-8') if path in self._cache: warning('duplicated path %r in cache' % path) continue pos += 3 fields = l[pos:].split(' ') if len(fields) != 6: warning("bad line in hashcache: %r" % l) continue sha1 = fields[0] if len(sha1) != 40: warning("bad sha1 in hashcache: %r" % sha1) continue fp = tuple(map(long, fields[1:])) self._cache[path] = (sha1, fp) M 644 inline bzrlib/selftest/testhashcache.py data 3417 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from bzrlib.selftest import InTempDir def sha1(t): import sha return sha.new(t).hexdigest() def pause(): import time # allow it to stabilize start = int(time.time()) while int(time.time()) == start: time.sleep(0.2) class TestHashCache(InTempDir): """Functional tests for statcache""" def runTest(self): from bzrlib.hashcache import HashCache import os import time hc = HashCache('.') file('foo', 'wb').write('hello') os.mkdir('subdir') pause() self.assertEquals(hc.get_sha1('foo'), 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d') self.assertEquals(hc.miss_count, 1) self.assertEquals(hc.hit_count, 0) # check we hit without re-reading self.assertEquals(hc.get_sha1('foo'), 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d') self.assertEquals(hc.miss_count, 1) self.assertEquals(hc.hit_count, 1) # check again without re-reading self.assertEquals(hc.get_sha1('foo'), 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d') self.assertEquals(hc.miss_count, 1) self.assertEquals(hc.hit_count, 2) # write new file and make sure it is seen file('foo', 'wb').write('goodbye') pause() self.assertEquals(hc.get_sha1('foo'), '3c8ec4874488f6090a157b014ce3397ca8e06d4f') self.assertEquals(hc.miss_count, 2) # quickly write new file of same size and make sure it is seen # this may rely on detection of timestamps that are too close # together to be safe file('foo', 'wb').write('g00dbye') self.assertEquals(hc.get_sha1('foo'), sha1('g00dbye')) # this is not quite guaranteed to be true; we might have # crossed a 1s boundary before self.assertEquals(hc.danger_count, 1) self.assertEquals(len(hc._cache), 0) self.assertEquals(hc.get_sha1('subdir'), None) pause() # should now be safe to cache it self.assertEquals(hc.get_sha1('foo'), sha1('g00dbye')) self.assertEquals(len(hc._cache), 1) # write out, read back in and check that we don't need to # re-read any files hc.write('stat-cache') del hc hc = HashCache('.') hc.read('stat-cache') self.assertEquals(len(hc._cache), 1) self.assertEquals(hc.get_sha1('foo'), sha1('g00dbye')) self.assertEquals(hc.hit_count, 1) self.assertEquals(hc.miss_count, 0) commit refs/heads/master mark :959 committer Martin Pool 1120790955 +1000 data 23 - more hashcache tests from :958 M 644 inline bzrlib/selftest/testhashcache.py data 3817 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from bzrlib.selftest import InTempDir def sha1(t): import sha return sha.new(t).hexdigest() def pause(): import time # allow it to stabilize start = int(time.time()) while int(time.time()) == start: time.sleep(0.2) class TestHashCache(InTempDir): """Functional tests for statcache""" def runTest(self): from bzrlib.hashcache import HashCache import os import time hc = HashCache('.') file('foo', 'wb').write('hello') os.mkdir('subdir') pause() self.assertEquals(hc.get_sha1('foo'), 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d') self.assertEquals(hc.miss_count, 1) self.assertEquals(hc.hit_count, 0) # check we hit without re-reading self.assertEquals(hc.get_sha1('foo'), 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d') self.assertEquals(hc.miss_count, 1) self.assertEquals(hc.hit_count, 1) # check again without re-reading self.assertEquals(hc.get_sha1('foo'), 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d') self.assertEquals(hc.miss_count, 1) self.assertEquals(hc.hit_count, 2) # write new file and make sure it is seen file('foo', 'wb').write('goodbye') pause() self.assertEquals(hc.get_sha1('foo'), '3c8ec4874488f6090a157b014ce3397ca8e06d4f') self.assertEquals(hc.miss_count, 2) # quickly write new file of same size and make sure it is seen # this may rely on detection of timestamps that are too close # together to be safe file('foo', 'wb').write('g00dbye') self.assertEquals(hc.get_sha1('foo'), sha1('g00dbye')) file('foo2', 'wb').write('other file') self.assertEquals(hc.get_sha1('foo2'), sha1('other file')) os.remove('foo2') self.assertEquals(hc.get_sha1('foo2'), None) file('foo2', 'wb').write('new content') self.assertEquals(hc.get_sha1('foo2'), sha1('new content')) self.assertEquals(hc.get_sha1('subdir'), None) # it's likely neither are cached at the moment because they # changed recently, but we can't be sure pause() # should now be safe to cache it if we reread them self.assertEquals(hc.get_sha1('foo'), sha1('g00dbye')) self.assertEquals(len(hc._cache), 1) self.assertEquals(hc.get_sha1('foo2'), sha1('new content')) self.assertEquals(len(hc._cache), 2) # write out, read back in and check that we don't need to # re-read any files hc.write('stat-cache') del hc hc = HashCache('.') hc.read('stat-cache') self.assertEquals(len(hc._cache), 2) self.assertEquals(hc.get_sha1('foo'), sha1('g00dbye')) self.assertEquals(hc.hit_count, 1) self.assertEquals(hc.miss_count, 0) self.assertEquals(hc.get_sha1('foo2'), sha1('new content')) commit refs/heads/master mark :960 committer Martin Pool 1120791046 +1000 data 3 doc from :959 M 644 inline bzrlib/hashcache.py data 6022 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # TODO: Perhaps have a way to stat all the files in inode order, and # then remember that they're all fresh for the lifetime of the object? # TODO: Keep track of whether there are in-memory updates that need to # be flushed. # TODO: Perhaps return more details on the file to avoid statting it # again: nonexistent, file type, size, etc CACHE_HEADER = "### bzr statcache v5\n" def _fingerprint(abspath): import os, stat try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None return (fs.st_size, fs.st_mtime, fs.st_ctime, fs.st_ino, fs.st_dev) class HashCache(object): """Cache for looking up file SHA-1. Files are considered to match the cached value if the fingerprint of the file has not changed. This includes its mtime, ctime, device number, inode number, and size. This should catch modifications or replacement of the file by a new one. This may not catch modifications that do not change the file's size and that occur within the resolution window of the timestamps. To handle this we specifically do not cache files which have changed since the start of the present second, since they could undetectably change again. This scheme may fail if the machine's clock steps backwards. Don't do that. This does not canonicalize the paths passed in; that should be done by the caller. _cache Indexed by path, points to a two-tuple of the SHA-1 of the file. and its fingerprint. stat_count number of times files have been statted hit_count number of times files have been retrieved from the cache, avoiding a re-read miss_count number of misses (times files have been completely re-read) """ def __init__(self, basedir): self.basedir = basedir self.hit_count = 0 self.miss_count = 0 self.stat_count = 0 self.danger_count = 0 self._cache = {} def clear(self): """Discard all cached information. This does not reset the counters.""" self._cache_sha1 = {} def get_sha1(self, path): """Return the hex SHA-1 of the contents of the file at path. XXX: If the file does not exist or is not a plain file??? """ import os, time from bzrlib.osutils import sha_file abspath = os.path.join(self.basedir, path) fp = _fingerprint(abspath) c = self._cache.get(path) if c: cache_sha1, cache_fp = c else: cache_sha1, cache_fp = None, None self.stat_count += 1 if not fp: # not a regular file return None elif cache_fp and (cache_fp == fp): self.hit_count += 1 return cache_sha1 else: self.miss_count += 1 digest = sha_file(file(abspath, 'rb')) now = int(time.time()) if fp[1] >= now or fp[2] >= now: # changed too recently; can't be cached. we can # return the result and it could possibly be cached # next time. self.danger_count += 1 if cache_fp: del self._cache[path] else: self._cache[path] = (digest, fp) return digest def write(self, cachefn): """Write contents of cache to file.""" from atomicfile import AtomicFile outf = AtomicFile(cachefn, 'wb') try: print >>outf, CACHE_HEADER, for path, c in self._cache.iteritems(): assert '//' not in path, path outf.write(path.encode('utf-8')) outf.write('// ') print >>outf, c[0], # hex sha1 for fld in c[1]: print >>outf, "%d" % fld, print >>outf outf.commit() finally: if not outf.closed: outf.abort() def read(self, cachefn): """Reinstate cache from file. Overwrites existing cache. If the cache file has the wrong version marker, this just clears the cache.""" from bzrlib.trace import mutter, warning inf = file(cachefn, 'rb') self._cache = {} hdr = inf.readline() if hdr != CACHE_HEADER: mutter('cache header marker not found at top of %s; discarding cache' % cachefn) return for l in inf: pos = l.index('// ') path = l[:pos].decode('utf-8') if path in self._cache: warning('duplicated path %r in cache' % path) continue pos += 3 fields = l[pos:].split(' ') if len(fields) != 6: warning("bad line in hashcache: %r" % l) continue sha1 = fields[0] if len(sha1) != 40: warning("bad sha1 in hashcache: %r" % sha1) continue fp = tuple(map(long, fields[1:])) self._cache[path] = (sha1, fp) commit refs/heads/master mark :961 committer Martin Pool 1120791193 +1000 data 28 - cleaner 'modified 'command from :960 M 644 inline bzrlib/commands.py data 53663 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import BzrError, BzrCheckError, BzrCommandError from bzrlib.branch import find_branch from bzrlib import BZRDIR plugin_cmds = {} def register_command(cmd): "Utility function to help register a command" global plugin_cmds k = cmd.__name__ if k.startswith("cmd_"): k_unsquished = _unsquish_command_name(k) else: k_unsquished = k if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = cmd else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r' % sys.modules[cmd.__module__]) def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _get_cmd_dict(plugins_override=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v # If we didn't load plugins, the plugin_cmds dict will be empty if plugins_override: d.update(plugin_cmds) else: d2 = plugin_cmds.copy() d2.update(d) d = d2 return d def get_all_cmds(plugins_override=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(plugins_override=plugins_override).iteritems(): yield k,v def get_cmd_class(cmd, plugins_override=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(plugins_override=plugins_override) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() for opt in self.takes_options: if not opt in OPTIONS: raise BzrError("Unknown option '%s' returned by external command %s" % (opt, path)) # TODO: Is there any way to check takes_args is valid here? self.takes_args = pipe.readline().split() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-usage'" % path) pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-help'" % path) def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: optname = name.replace('_','-') value = kargs[name] if OPTIONS.has_key(optname): # it's an option opts.append('--%s' % optname) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = find_branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): from bzrlib.xml import pack_xml pack_xml(find_branch('.').get_revision(revision_id), sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print find_branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): from bzrlib.add import smart_add smart_add(file_list, verbose, not no_recurse) class cmd_mkdir(Command): """Create a new versioned directory. This is equivalent to creating the directory and then adding it. """ takes_args = ['dir+'] def run(self, dir_list): b = None for d in dir_list: os.mkdir(d) if not b: b = find_branch(d) b.add([d], verbose=True) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print find_branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = find_branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = find_branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = find_branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import tempfile from shutil import rmtree import errno br_to = find_branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if e.errno != errno.ENOENT: raise if location is None: if stored_loc is None: raise BzrCommandError("No pull location known or specified.") else: print "Using last location: %s" % stored_loc location = stored_loc cache_root = tempfile.mkdtemp() from bzrlib.branch import DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: from branch import find_cached_branch, DivergedBranches br_from = find_cached_branch(location, cache_root) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged." " Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") finally: rmtree(cache_root) class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. To retrieve the branch as of a particular revision, supply the --revision parameter, as in "branch foo/bar -r 5". """ takes_args = ['from_location', 'to_location?'] takes_options = ['revision'] def run(self, from_location, to_location=None, revision=None): import errno from bzrlib.merge import merge from bzrlib.branch import DivergedBranches, NoSuchRevision, \ find_cached_branch, Branch from shutil import rmtree from meta_store import CachedStore import tempfile cache_root = tempfile.mkdtemp() try: try: br_from = find_cached_branch(from_location, cache_root) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not' ' exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location.rstrip("/\\")) try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already' ' exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) try: br_to.update_revisions(br_from, stop_revision=revision) except NoSuchRevision: rmtree(to_location) msg = "The branch %s has no revision %d." % (from_location, revision) raise BzrCommandError(msg) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False, ignore_zero=True) from_location = pull_loc(br_from) br_to.controlfile("x-pull", "wb").write(from_location + "\n") finally: rmtree(cache_root) def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = find_branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = find_branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = find_branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: raise BzrError("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = find_branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: raise BzrError("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in find_branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in find_branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): from bzrlib.branch import Branch Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = find_branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = find_branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): from bzrlib.statcache import update_cache, SC_SHA1 from bzrlib.diff import compare_trees b = find_branch('.') td = compare_trees(b.basis_tree(), b.working_tree()) for path, id, kind in td.modified: print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = find_branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision','long'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None, long=False): from bzrlib.branch import find_branch from bzrlib.log import log_formatter, show_log import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') if long: log_format = 'long' else: log_format = 'short' lf = log_formatter(log_format, show_ids=show_ids, to_file=outf, show_timezone=timezone) show_log(b, lf, file_id, verbose=verbose, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = find_branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = find_branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): from bzrlib.osutils import quotefn for f in find_branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = find_branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = find_branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print find_branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, try to find the format with the extension. If no extension is found exports to a directory (equivalent to --format=dir). Root may be the top directory for tar, tgz and tbz2 formats. If none is given, the top directory will be the root name of the file.""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format', 'root'] def run(self, dest, revision=None, format=None, root=None): import os.path b = find_branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) root, ext = os.path.splitext(dest) if not format: if ext in (".tar",): format = "tar" elif ext in (".gz", ".tgz"): format = "tgz" elif ext in (".bz2", ".tbz2"): format = "tbz2" else: format = "dir" t.export(dest, format, root) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = find_branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit from bzrlib.osutils import get_text_message ## Warning: shadows builtin file() if not message and not file: import cStringIO stdout = sys.stdout catcher = cStringIO.StringIO() sys.stdout = catcher cmd_status({"file_list":selected_list}, {}) info = catcher.getvalue() sys.stdout = stdout message = get_text_message(info) if message is None: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = find_branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.check import check check(find_branch(dir)) class cmd_upgrade(Command): """Upgrade branch storage to current format. This should normally be used only after the check command tells you to run it. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.upgrade import upgrade upgrade(find_branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest return int(not selftest()) class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Restore selected files from a previous revision. """ takes_args = ['file+'] def run(self, file_list): from bzrlib.branch import find_branch if not file_list: file_list = ['.'] b = find_branch(file_list[0]) b.revert([b.relpath(f) for f in file_list]) class cmd_merge_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_update_stat_cache(Command): """Update stat-cache mapping inodes to SHA-1 hashes. For testing only.""" hidden = True def run(self): from bzrlib.statcache import update_cache b = find_branch('.') update_cache(b.base, b.read_working_inventory()) class cmd_plugins(Command): """List plugins""" hidden = True def run(self): import bzrlib.plugin from pprint import pprint pprint(bzrlib.plugin.all_plugins) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'update': None, 'long': None, 'root': str, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', 'l': 'long', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) >>> parse_args('log -r 500'.split()) (['log'], {'revision': 500}) >>> parse_args('log -r500:600'.split()) (['log'], {'revision': [500, 600]}) >>> parse_args('log -vr500:600'.split()) (['log'], {'verbose': True, 'revision': [500, 600]}) >>> parse_args('log -rv500:600'.split()) #the r takes an argument Traceback (most recent call last): ... ValueError: invalid literal for int(): v500 """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: raise BzrError('unknown long option %r' % a) else: shortopt = a[1:] if shortopt in SHORT_OPTIONS: # Multi-character options must have a space to delimit # their value optname = SHORT_OPTIONS[shortopt] else: # Single character short options, can be chained, # and have their value appended to their name shortopt = a[1:2] if shortopt not in SHORT_OPTIONS: # We didn't find the multi-character name, and we # didn't find the single char name raise BzrError('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if a[2:]: # There are extra things on this option # see if it is the value, or if it is another # short option optargfn = OPTIONS[optname] if optargfn is None: # This option does not take an argument, so the # next entry is another short option, pack it back # into the list argv.insert(0, '-' + a[2:]) else: # This option takes an argument, so pack it # into the array optarg = a[2:] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? raise BzrError('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: raise BzrError('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: raise BzrError('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def _parse_master_args(argv): """Parse the arguments that always go with the original command. These are things like bzr --no-plugins, etc. There are now 2 types of option flags. Ones that come *before* the command, and ones that come *after* the command. Ones coming *before* the command are applied against all possible commands. And are generally applied before plugins are loaded. The current list are: --builtin Allow plugins to load, but don't let them override builtin commands, they will still be allowed if they do not override a builtin. --no-plugins Don't load any plugins. This lets you get back to official source behavior. --profile Enable the hotspot profile before running the command. For backwards compatibility, this is also a non-master option. --version Spit out the version of bzr that is running and exit. This is also a non-master option. --help Run help and exit, also a non-master option (I think that should stay, though) >>> argv, opts = _parse_master_args(['bzr', '--test']) Traceback (most recent call last): ... BzrCommandError: Invalid master option: 'test' >>> argv, opts = _parse_master_args(['bzr', '--version', 'command']) >>> print argv ['command'] >>> print opts['version'] True >>> argv, opts = _parse_master_args(['bzr', '--profile', 'command', '--more-options']) >>> print argv ['command', '--more-options'] >>> print opts['profile'] True >>> argv, opts = _parse_master_args(['bzr', '--no-plugins', 'command']) >>> print argv ['command'] >>> print opts['no-plugins'] True >>> print opts['profile'] False >>> argv, opts = _parse_master_args(['bzr', 'command', '--profile']) >>> print argv ['command', '--profile'] >>> print opts['profile'] False """ master_opts = {'builtin':False, 'no-plugins':False, 'version':False, 'profile':False, 'help':False } # This is the point where we could hook into argv[0] to determine # what front-end is supposed to be run # For now, we are just ignoring it. cmd_name = argv.pop(0) for arg in argv[:]: if arg[:2] != '--': # at the first non-option, we return the rest break arg = arg[2:] # Remove '--' if arg not in master_opts: # We could say that this is not an error, that we should # just let it be handled by the main section instead raise BzrCommandError('Invalid master option: %r' % arg) argv.pop(0) # We are consuming this entry master_opts[arg] = True return argv, master_opts def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: # some options like --builtin and --no-plugins have special effects argv, master_opts = _parse_master_args(argv) if not master_opts['no-plugins']: from bzrlib.plugin import load_plugins load_plugins() args, opts = parse_args(argv) if master_opts['help']: from bzrlib.help import help if argv: help(argv[0]) else: help() return 0 if 'help' in opts: from bzrlib.help import help if args: help(args[0]) else: help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 plugins_override = not (master_opts['builtin']) canonical_cmd, cmd_class = get_cmd_class(cmd, plugins_override=plugins_override) profile = master_opts['profile'] # For backwards compatibility, I would rather stick with --profile being a # master/global option if 'profile' in opts: profile = True del opts['profile'] # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): bzrlib.trace.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: import errno quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/selftest/testhashcache.py data 3817 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from bzrlib.selftest import InTempDir def sha1(t): import sha return sha.new(t).hexdigest() def pause(): import time # allow it to stabilize start = int(time.time()) while int(time.time()) == start: time.sleep(0.2) class TestHashCache(InTempDir): """Functional tests for hashcache""" def runTest(self): from bzrlib.hashcache import HashCache import os import time hc = HashCache('.') file('foo', 'wb').write('hello') os.mkdir('subdir') pause() self.assertEquals(hc.get_sha1('foo'), 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d') self.assertEquals(hc.miss_count, 1) self.assertEquals(hc.hit_count, 0) # check we hit without re-reading self.assertEquals(hc.get_sha1('foo'), 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d') self.assertEquals(hc.miss_count, 1) self.assertEquals(hc.hit_count, 1) # check again without re-reading self.assertEquals(hc.get_sha1('foo'), 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d') self.assertEquals(hc.miss_count, 1) self.assertEquals(hc.hit_count, 2) # write new file and make sure it is seen file('foo', 'wb').write('goodbye') pause() self.assertEquals(hc.get_sha1('foo'), '3c8ec4874488f6090a157b014ce3397ca8e06d4f') self.assertEquals(hc.miss_count, 2) # quickly write new file of same size and make sure it is seen # this may rely on detection of timestamps that are too close # together to be safe file('foo', 'wb').write('g00dbye') self.assertEquals(hc.get_sha1('foo'), sha1('g00dbye')) file('foo2', 'wb').write('other file') self.assertEquals(hc.get_sha1('foo2'), sha1('other file')) os.remove('foo2') self.assertEquals(hc.get_sha1('foo2'), None) file('foo2', 'wb').write('new content') self.assertEquals(hc.get_sha1('foo2'), sha1('new content')) self.assertEquals(hc.get_sha1('subdir'), None) # it's likely neither are cached at the moment because they # changed recently, but we can't be sure pause() # should now be safe to cache it if we reread them self.assertEquals(hc.get_sha1('foo'), sha1('g00dbye')) self.assertEquals(len(hc._cache), 1) self.assertEquals(hc.get_sha1('foo2'), sha1('new content')) self.assertEquals(len(hc._cache), 2) # write out, read back in and check that we don't need to # re-read any files hc.write('stat-cache') del hc hc = HashCache('.') hc.read('stat-cache') self.assertEquals(len(hc._cache), 2) self.assertEquals(hc.get_sha1('foo'), sha1('g00dbye')) self.assertEquals(hc.hit_count, 1) self.assertEquals(hc.miss_count, 0) self.assertEquals(hc.get_sha1('foo2'), sha1('new content')) commit refs/heads/master mark :962 committer Martin Pool 1120805698 +1000 data 166 - use new path-based hashcache for WorkingTree- squash mtime/ctime to whole seconds- update and if necessary write out hashcache when WorkingTree object is created. from :961 M 644 inline bzrlib/commands.py data 53309 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import BzrError, BzrCheckError, BzrCommandError from bzrlib.branch import find_branch from bzrlib import BZRDIR plugin_cmds = {} def register_command(cmd): "Utility function to help register a command" global plugin_cmds k = cmd.__name__ if k.startswith("cmd_"): k_unsquished = _unsquish_command_name(k) else: k_unsquished = k if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = cmd else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r' % sys.modules[cmd.__module__]) def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _get_cmd_dict(plugins_override=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v # If we didn't load plugins, the plugin_cmds dict will be empty if plugins_override: d.update(plugin_cmds) else: d2 = plugin_cmds.copy() d2.update(d) d = d2 return d def get_all_cmds(plugins_override=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(plugins_override=plugins_override).iteritems(): yield k,v def get_cmd_class(cmd, plugins_override=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(plugins_override=plugins_override) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() for opt in self.takes_options: if not opt in OPTIONS: raise BzrError("Unknown option '%s' returned by external command %s" % (opt, path)) # TODO: Is there any way to check takes_args is valid here? self.takes_args = pipe.readline().split() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-usage'" % path) pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-help'" % path) def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: optname = name.replace('_','-') value = kargs[name] if OPTIONS.has_key(optname): # it's an option opts.append('--%s' % optname) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = find_branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): from bzrlib.xml import pack_xml pack_xml(find_branch('.').get_revision(revision_id), sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print find_branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): from bzrlib.add import smart_add smart_add(file_list, verbose, not no_recurse) class cmd_mkdir(Command): """Create a new versioned directory. This is equivalent to creating the directory and then adding it. """ takes_args = ['dir+'] def run(self, dir_list): b = None for d in dir_list: os.mkdir(d) if not b: b = find_branch(d) b.add([d], verbose=True) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print find_branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = find_branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = find_branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = find_branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import tempfile from shutil import rmtree import errno br_to = find_branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if e.errno != errno.ENOENT: raise if location is None: if stored_loc is None: raise BzrCommandError("No pull location known or specified.") else: print "Using last location: %s" % stored_loc location = stored_loc cache_root = tempfile.mkdtemp() from bzrlib.branch import DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: from branch import find_cached_branch, DivergedBranches br_from = find_cached_branch(location, cache_root) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged." " Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") finally: rmtree(cache_root) class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. To retrieve the branch as of a particular revision, supply the --revision parameter, as in "branch foo/bar -r 5". """ takes_args = ['from_location', 'to_location?'] takes_options = ['revision'] def run(self, from_location, to_location=None, revision=None): import errno from bzrlib.merge import merge from bzrlib.branch import DivergedBranches, NoSuchRevision, \ find_cached_branch, Branch from shutil import rmtree from meta_store import CachedStore import tempfile cache_root = tempfile.mkdtemp() try: try: br_from = find_cached_branch(from_location, cache_root) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not' ' exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location.rstrip("/\\")) try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already' ' exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) try: br_to.update_revisions(br_from, stop_revision=revision) except NoSuchRevision: rmtree(to_location) msg = "The branch %s has no revision %d." % (from_location, revision) raise BzrCommandError(msg) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False, ignore_zero=True) from_location = pull_loc(br_from) br_to.controlfile("x-pull", "wb").write(from_location + "\n") finally: rmtree(cache_root) def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = find_branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = find_branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = find_branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: raise BzrError("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = find_branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: raise BzrError("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in find_branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in find_branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): from bzrlib.branch import Branch Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = find_branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = find_branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): from bzrlib.diff import compare_trees b = find_branch('.') td = compare_trees(b.basis_tree(), b.working_tree()) for path, id, kind in td.modified: print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = find_branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision','long'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None, long=False): from bzrlib.branch import find_branch from bzrlib.log import log_formatter, show_log import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') if long: log_format = 'long' else: log_format = 'short' lf = log_formatter(log_format, show_ids=show_ids, to_file=outf, show_timezone=timezone) show_log(b, lf, file_id, verbose=verbose, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = find_branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = find_branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): from bzrlib.osutils import quotefn for f in find_branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = find_branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = find_branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print find_branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, try to find the format with the extension. If no extension is found exports to a directory (equivalent to --format=dir). Root may be the top directory for tar, tgz and tbz2 formats. If none is given, the top directory will be the root name of the file.""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format', 'root'] def run(self, dest, revision=None, format=None, root=None): import os.path b = find_branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) root, ext = os.path.splitext(dest) if not format: if ext in (".tar",): format = "tar" elif ext in (".gz", ".tgz"): format = "tgz" elif ext in (".bz2", ".tbz2"): format = "tbz2" else: format = "dir" t.export(dest, format, root) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = find_branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None): from bzrlib.commit import commit from bzrlib.osutils import get_text_message ## Warning: shadows builtin file() if not message and not file: import cStringIO stdout = sys.stdout catcher = cStringIO.StringIO() sys.stdout = catcher cmd_status({"file_list":selected_list}, {}) info = catcher.getvalue() sys.stdout = stdout message = get_text_message(info) if message is None: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = find_branch('.') commit(b, message, verbose=verbose, specific_files=selected_list) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.check import check check(find_branch(dir)) class cmd_upgrade(Command): """Upgrade branch storage to current format. This should normally be used only after the check command tells you to run it. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.upgrade import upgrade upgrade(find_branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest return int(not selftest()) class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Restore selected files from a previous revision. """ takes_args = ['file+'] def run(self, file_list): from bzrlib.branch import find_branch if not file_list: file_list = ['.'] b = find_branch(file_list[0]) b.revert([b.relpath(f) for f in file_list]) class cmd_merge_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_plugins(Command): """List plugins""" hidden = True def run(self): import bzrlib.plugin from pprint import pprint pprint(bzrlib.plugin.all_plugins) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'update': None, 'long': None, 'root': str, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', 'l': 'long', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) >>> parse_args('log -r 500'.split()) (['log'], {'revision': 500}) >>> parse_args('log -r500:600'.split()) (['log'], {'revision': [500, 600]}) >>> parse_args('log -vr500:600'.split()) (['log'], {'verbose': True, 'revision': [500, 600]}) >>> parse_args('log -rv500:600'.split()) #the r takes an argument Traceback (most recent call last): ... ValueError: invalid literal for int(): v500 """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: raise BzrError('unknown long option %r' % a) else: shortopt = a[1:] if shortopt in SHORT_OPTIONS: # Multi-character options must have a space to delimit # their value optname = SHORT_OPTIONS[shortopt] else: # Single character short options, can be chained, # and have their value appended to their name shortopt = a[1:2] if shortopt not in SHORT_OPTIONS: # We didn't find the multi-character name, and we # didn't find the single char name raise BzrError('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if a[2:]: # There are extra things on this option # see if it is the value, or if it is another # short option optargfn = OPTIONS[optname] if optargfn is None: # This option does not take an argument, so the # next entry is another short option, pack it back # into the list argv.insert(0, '-' + a[2:]) else: # This option takes an argument, so pack it # into the array optarg = a[2:] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? raise BzrError('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: raise BzrError('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: raise BzrError('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def _parse_master_args(argv): """Parse the arguments that always go with the original command. These are things like bzr --no-plugins, etc. There are now 2 types of option flags. Ones that come *before* the command, and ones that come *after* the command. Ones coming *before* the command are applied against all possible commands. And are generally applied before plugins are loaded. The current list are: --builtin Allow plugins to load, but don't let them override builtin commands, they will still be allowed if they do not override a builtin. --no-plugins Don't load any plugins. This lets you get back to official source behavior. --profile Enable the hotspot profile before running the command. For backwards compatibility, this is also a non-master option. --version Spit out the version of bzr that is running and exit. This is also a non-master option. --help Run help and exit, also a non-master option (I think that should stay, though) >>> argv, opts = _parse_master_args(['bzr', '--test']) Traceback (most recent call last): ... BzrCommandError: Invalid master option: 'test' >>> argv, opts = _parse_master_args(['bzr', '--version', 'command']) >>> print argv ['command'] >>> print opts['version'] True >>> argv, opts = _parse_master_args(['bzr', '--profile', 'command', '--more-options']) >>> print argv ['command', '--more-options'] >>> print opts['profile'] True >>> argv, opts = _parse_master_args(['bzr', '--no-plugins', 'command']) >>> print argv ['command'] >>> print opts['no-plugins'] True >>> print opts['profile'] False >>> argv, opts = _parse_master_args(['bzr', 'command', '--profile']) >>> print argv ['command', '--profile'] >>> print opts['profile'] False """ master_opts = {'builtin':False, 'no-plugins':False, 'version':False, 'profile':False, 'help':False } # This is the point where we could hook into argv[0] to determine # what front-end is supposed to be run # For now, we are just ignoring it. cmd_name = argv.pop(0) for arg in argv[:]: if arg[:2] != '--': # at the first non-option, we return the rest break arg = arg[2:] # Remove '--' if arg not in master_opts: # We could say that this is not an error, that we should # just let it be handled by the main section instead raise BzrCommandError('Invalid master option: %r' % arg) argv.pop(0) # We are consuming this entry master_opts[arg] = True return argv, master_opts def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: # some options like --builtin and --no-plugins have special effects argv, master_opts = _parse_master_args(argv) if not master_opts['no-plugins']: from bzrlib.plugin import load_plugins load_plugins() args, opts = parse_args(argv) if master_opts['help']: from bzrlib.help import help if argv: help(argv[0]) else: help() return 0 if 'help' in opts: from bzrlib.help import help if args: help(args[0]) else: help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 plugins_override = not (master_opts['builtin']) canonical_cmd, cmd_class = get_cmd_class(cmd, plugins_override=plugins_override) profile = master_opts['profile'] # For backwards compatibility, I would rather stick with --profile being a # master/global option if 'profile' in opts: profile = True del opts['profile'] # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): bzrlib.trace.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: import errno quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/hashcache.py data 6953 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # TODO: Perhaps have a way to stat all the files in inode order, and # then remember that they're all fresh for the lifetime of the object? # TODO: Keep track of whether there are in-memory updates that need to # be flushed. # TODO: Perhaps return more details on the file to avoid statting it # again: nonexistent, file type, size, etc CACHE_HEADER = "### bzr hashcache v5\n" def _fingerprint(abspath): import os, stat try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None # we discard any high precision because it's not reliable; perhaps we # could do better on some systems? return (fs.st_size, long(fs.st_mtime), long(fs.st_ctime), fs.st_ino, fs.st_dev) class HashCache(object): """Cache for looking up file SHA-1. Files are considered to match the cached value if the fingerprint of the file has not changed. This includes its mtime, ctime, device number, inode number, and size. This should catch modifications or replacement of the file by a new one. This may not catch modifications that do not change the file's size and that occur within the resolution window of the timestamps. To handle this we specifically do not cache files which have changed since the start of the present second, since they could undetectably change again. This scheme may fail if the machine's clock steps backwards. Don't do that. This does not canonicalize the paths passed in; that should be done by the caller. _cache Indexed by path, points to a two-tuple of the SHA-1 of the file. and its fingerprint. stat_count number of times files have been statted hit_count number of times files have been retrieved from the cache, avoiding a re-read miss_count number of misses (times files have been completely re-read) """ needs_write = False def __init__(self, basedir): self.basedir = basedir self.hit_count = 0 self.miss_count = 0 self.stat_count = 0 self.danger_count = 0 self._cache = {} def cache_file_name(self): import os.path return os.path.join(self.basedir, '.bzr', 'stat-cache') def clear(self): """Discard all cached information. This does not reset the counters.""" if self._cache: self.needs_write = True self._cache = {} def get_sha1(self, path): """Return the hex SHA-1 of the contents of the file at path. XXX: If the file does not exist or is not a plain file??? """ import os, time from bzrlib.osutils import sha_file from bzrlib.trace import mutter abspath = os.path.join(self.basedir, path) fp = _fingerprint(abspath) c = self._cache.get(path) if c: cache_sha1, cache_fp = c else: cache_sha1, cache_fp = None, None self.stat_count += 1 if not fp: # not a regular file return None elif cache_fp and (cache_fp == fp): self.hit_count += 1 return cache_sha1 else: self.miss_count += 1 digest = sha_file(file(abspath, 'rb')) now = int(time.time()) if fp[1] >= now or fp[2] >= now: # changed too recently; can't be cached. we can # return the result and it could possibly be cached # next time. self.danger_count += 1 if cache_fp: mutter("remove outdated entry for %s" % path) self.needs_write = True del self._cache[path] elif (fp != cache_fp) or (digest != cache_sha1): mutter("update entry for %s" % path) mutter(" %r" % (fp,)) mutter(" %r" % (cache_fp,)) self.needs_write = True self._cache[path] = (digest, fp) return digest def write(self): """Write contents of cache to file.""" from atomicfile import AtomicFile outf = AtomicFile(self.cache_file_name(), 'wb') try: print >>outf, CACHE_HEADER, for path, c in self._cache.iteritems(): assert '//' not in path, path outf.write(path.encode('utf-8')) outf.write('// ') print >>outf, c[0], # hex sha1 for fld in c[1]: print >>outf, "%d" % fld, print >>outf outf.commit() self.needs_write = False finally: if not outf.closed: outf.abort() def read(self): """Reinstate cache from file. Overwrites existing cache. If the cache file has the wrong version marker, this just clears the cache.""" from bzrlib.trace import mutter, warning self._cache = {} fn = self.cache_file_name() try: inf = file(fn, 'rb') except IOError, e: mutter("failed to open %s: %s" % (fn, e)) return hdr = inf.readline() if hdr != CACHE_HEADER: mutter('cache header marker not found at top of %s; discarding cache' % cachefn) return for l in inf: pos = l.index('// ') path = l[:pos].decode('utf-8') if path in self._cache: warning('duplicated path %r in cache' % path) continue pos += 3 fields = l[pos:].split(' ') if len(fields) != 6: warning("bad line in hashcache: %r" % l) continue sha1 = fields[0] if len(sha1) != 40: warning("bad sha1 in hashcache: %r" % sha1) continue fp = tuple(map(long, fields[1:])) self._cache[path] = (sha1, fp) self.needs_write = False M 644 inline bzrlib/selftest/testhashcache.py data 3878 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from bzrlib.selftest import InTempDir def sha1(t): import sha return sha.new(t).hexdigest() def pause(): import time # allow it to stabilize start = int(time.time()) while int(time.time()) == start: time.sleep(0.2) class TestHashCache(InTempDir): """Functional tests for hashcache""" def runTest(self): from bzrlib.hashcache import HashCache import os import time # make a dummy bzr directory just to hold the cache os.mkdir('.bzr') hc = HashCache('.') file('foo', 'wb').write('hello') os.mkdir('subdir') pause() self.assertEquals(hc.get_sha1('foo'), 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d') self.assertEquals(hc.miss_count, 1) self.assertEquals(hc.hit_count, 0) # check we hit without re-reading self.assertEquals(hc.get_sha1('foo'), 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d') self.assertEquals(hc.miss_count, 1) self.assertEquals(hc.hit_count, 1) # check again without re-reading self.assertEquals(hc.get_sha1('foo'), 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d') self.assertEquals(hc.miss_count, 1) self.assertEquals(hc.hit_count, 2) # write new file and make sure it is seen file('foo', 'wb').write('goodbye') pause() self.assertEquals(hc.get_sha1('foo'), '3c8ec4874488f6090a157b014ce3397ca8e06d4f') self.assertEquals(hc.miss_count, 2) # quickly write new file of same size and make sure it is seen # this may rely on detection of timestamps that are too close # together to be safe file('foo', 'wb').write('g00dbye') self.assertEquals(hc.get_sha1('foo'), sha1('g00dbye')) file('foo2', 'wb').write('other file') self.assertEquals(hc.get_sha1('foo2'), sha1('other file')) os.remove('foo2') self.assertEquals(hc.get_sha1('foo2'), None) file('foo2', 'wb').write('new content') self.assertEquals(hc.get_sha1('foo2'), sha1('new content')) self.assertEquals(hc.get_sha1('subdir'), None) # it's likely neither are cached at the moment because they # changed recently, but we can't be sure pause() # should now be safe to cache it if we reread them self.assertEquals(hc.get_sha1('foo'), sha1('g00dbye')) self.assertEquals(len(hc._cache), 1) self.assertEquals(hc.get_sha1('foo2'), sha1('new content')) self.assertEquals(len(hc._cache), 2) # write out, read back in and check that we don't need to # re-read any files hc.write() del hc hc = HashCache('.') hc.read() self.assertEquals(len(hc._cache), 2) self.assertEquals(hc.get_sha1('foo'), sha1('g00dbye')) self.assertEquals(hc.hit_count, 1) self.assertEquals(hc.miss_count, 0) self.assertEquals(hc.get_sha1('foo2'), sha1('new content')) M 644 inline bzrlib/workingtree.py data 8976 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # TODO: Don't allow WorkingTrees to be constructed for remote branches. import os import bzrlib.tree from errors import BzrCheckError from trace import mutter class WorkingTree(bzrlib.tree.Tree): """Working copy tree. The inventory is held in the `Branch` working-inventory, and the files are in a directory on disk. It is possible for a `WorkingTree` to have a filename which is not listed in the Inventory and vice versa. """ def __init__(self, basedir, inv): from bzrlib.hashcache import HashCache from bzrlib.trace import note, mutter self._inventory = inv self.basedir = basedir self.path2id = inv.path2id # update the whole cache up front and write to disk if anything changed; # in the future we might want to do this more selectively hc = self._hashcache = HashCache(basedir) hc.read() for path, ie in inv.iter_entries(): hc.get_sha1(path) if hc.needs_write: mutter("write hc") hc.write() def __iter__(self): """Iterate through file_ids for this tree. file_ids are in a WorkingTree if they are in the working inventory and the working file exists. """ inv = self._inventory for path, ie in inv.iter_entries(): if os.path.exists(self.abspath(path)): yield ie.file_id def __repr__(self): return "<%s of %s>" % (self.__class__.__name__, self.basedir) def abspath(self, filename): return os.path.join(self.basedir, filename) def has_filename(self, filename): return os.path.exists(self.abspath(filename)) def get_file(self, file_id): return self.get_file_byname(self.id2path(file_id)) def get_file_byname(self, filename): return file(self.abspath(filename), 'rb') def _get_store_filename(self, file_id): ## XXX: badly named; this isn't in the store at all return self.abspath(self.id2path(file_id)) def has_id(self, file_id): # files that have been deleted are excluded inv = self._inventory if not inv.has_id(file_id): return False path = inv.id2path(file_id) return os.path.exists(self.abspath(path)) __contains__ = has_id def get_file_size(self, file_id): # is this still called? raise NotImplementedError() def get_file_sha1(self, file_id): path = self._inventory.id2path(file_id) return self._hashcache.get_sha1(path) def file_class(self, filename): if self.path2id(filename): return 'V' elif self.is_ignored(filename): return 'I' else: return '?' def list_files(self): """Recursively list all files as (path, class, kind, id). Lists, but does not descend into unversioned directories. This does not include files that have been deleted in this tree. Skips the control directory. """ from osutils import appendpath, file_kind import os inv = self._inventory def descend(from_dir_relpath, from_dir_id, dp): ls = os.listdir(dp) ls.sort() for f in ls: ## TODO: If we find a subdirectory with its own .bzr ## directory, then that is a separate tree and we ## should exclude it. if bzrlib.BZRDIR == f: continue # path within tree fp = appendpath(from_dir_relpath, f) # absolute path fap = appendpath(dp, f) f_ie = inv.get_child(from_dir_id, f) if f_ie: c = 'V' elif self.is_ignored(fp): c = 'I' else: c = '?' fk = file_kind(fap) if f_ie: if f_ie.kind != fk: raise BzrCheckError("file %r entered as kind %r id %r, " "now of kind %r" % (fap, f_ie.kind, f_ie.file_id, fk)) yield fp, c, fk, (f_ie and f_ie.file_id) if fk != 'directory': continue if c != 'V': # don't descend unversioned directories continue for ff in descend(fp, f_ie.file_id, fap): yield ff for f in descend('', inv.root.file_id, self.basedir): yield f def unknowns(self): for subp in self.extras(): if not self.is_ignored(subp): yield subp def extras(self): """Yield all unknown files in this WorkingTree. If there are any unknown directories then only the directory is returned, not all its children. But if there are unknown files under a versioned subdirectory, they are returned. Currently returned depth-first, sorted by name within directories. """ ## TODO: Work from given directory downwards from osutils import isdir, appendpath for path, dir_entry in self.inventory.directories(): mutter("search for unknowns in %r" % path) dirabs = self.abspath(path) if not isdir(dirabs): # e.g. directory deleted continue fl = [] for subf in os.listdir(dirabs): if (subf != '.bzr' and (subf not in dir_entry.children)): fl.append(subf) fl.sort() for subf in fl: subp = appendpath(path, subf) yield subp def ignored_files(self): """Yield list of PATH, IGNORE_PATTERN""" for subp in self.extras(): pat = self.is_ignored(subp) if pat != None: yield subp, pat def get_ignore_list(self): """Return list of ignore patterns. Cached in the Tree object after the first call. """ if hasattr(self, '_ignorelist'): return self._ignorelist l = bzrlib.DEFAULT_IGNORE[:] if self.has_filename(bzrlib.IGNORE_FILENAME): f = self.get_file_byname(bzrlib.IGNORE_FILENAME) l.extend([line.rstrip("\n\r") for line in f.readlines()]) self._ignorelist = l return l def is_ignored(self, filename): r"""Check whether the filename matches an ignore pattern. Patterns containing '/' or '\' need to match the whole path; others match against only the last component. If the file is ignored, returns the pattern which caused it to be ignored, otherwise None. So this can simply be used as a boolean if desired.""" # TODO: Use '**' to match directories, and other extended # globbing stuff from cvs/rsync. # XXX: fnmatch is actually not quite what we want: it's only # approximately the same as real Unix fnmatch, and doesn't # treat dotfiles correctly and allows * to match /. # Eventually it should be replaced with something more # accurate. import fnmatch from osutils import splitpath for pat in self.get_ignore_list(): if '/' in pat or '\\' in pat: # as a special case, you can put ./ at the start of a # pattern; this is good to match in the top-level # only; if (pat[:2] == './') or (pat[:2] == '.\\'): newpat = pat[2:] else: newpat = pat if fnmatch.fnmatchcase(filename, newpat): return pat else: if fnmatch.fnmatchcase(splitpath(filename)[-1], pat): return pat else: return None commit refs/heads/master mark :963 committer Martin Pool 1120806269 +1000 data 52 - remove bzrlib.statcache module, no longer needed. from :962 D bzrlib/statcache.py commit refs/heads/master mark :964 committer Martin Pool 1120806572 +1000 data 3 doc from :963 M 644 inline bzrlib/trace.py data 4061 # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # TODO: Could probably replace this with something based on Python logging # module. __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " """Messages and logging for bazaar-ng Nothing is actually logged unless you call bzrlib.open_tracefile(). """ import sys, os ###################################################################### # messages and logging global _tracefile, _starttime _tracefile = None # used to have % (os.environ['USER'], time.time(), os.getpid()), 'w') _starttime = None # If false, notes also go to stdout; should replace this with --silent # at some point. silent = False # fix this if we ever fork within python _mypid = os.getpid() _logprefix = '[%d] ' % _mypid def _write_trace(msg): if _tracefile: _tracefile.write(_logprefix + msg + '\n') def warning(msg): sys.stderr.write('bzr: warning: ' + msg + '\n') _write_trace('warning: ' + msg) mutter = _write_trace def note(msg): b = '* ' + str(msg) + '\n' if not silent: sys.stderr.write(b) _write_trace('note: ' + msg) def log_error(msg): sys.stderr.write(msg + '\n') _write_trace(msg) def _rollover_trace_maybe(trace_fname): import stat try: size = os.stat(trace_fname)[stat.ST_SIZE] if size <= 4 << 20: return old_fname = trace_fname + '.old' try: # must remove before rename on windows os.remove(old_fname) except OSError: pass try: # might fail if in use on windows os.rename(trace_fname, old_fname) except OSError: pass except OSError: return def open_tracefile(argv=[], tracefilename='~/.bzr.log'): # Messages are always written to here, so that we have some # information if something goes wrong. In a future version this # file will be removed on successful completion. global _starttime, _tracefile import stat, codecs _starttime = os.times()[4] trace_fname = os.path.join(os.path.expanduser(tracefilename)) _rollover_trace_maybe(trace_fname) # buffering=1 means line buffered try: _tracefile = codecs.open(trace_fname, 'at', 'utf8', buffering=1) t = _tracefile if os.fstat(t.fileno())[stat.ST_SIZE] == 0: t.write("\nthis is a debug log for diagnosing/reporting problems in bzr\n") t.write("you can delete or truncate this file, or include sections in\n") t.write("bug reports to bazaar-ng@lists.canonical.com\n\n") import bzrlib _write_trace('bzr %s invoked on python %s (%s)' % (bzrlib.__version__, '.'.join(map(str, sys.version_info)), sys.platform)) _write_trace(' arguments: %r' % argv) _write_trace(' working dir: ' + os.getcwdu()) except IOError, e: warning("failed to open trace file: %s" % (e)) def close_trace(): times = os.times() mutter("finished, %.3fu/%.3fs cpu, %.3fu/%.3fs cum, %.3f elapsed" % (times[:4] + ((times[4] - _starttime),))) def log_exception(): """Log the last exception into the trace file.""" import cgitb s = cgitb.text(sys.exc_info()) for l in s.split('\n'): _write_trace(l) commit refs/heads/master mark :965 committer Martin Pool 1121047378 +1000 data 121 - more weave.py command line options - better error when invalid version is given - weave help and weave mash commands from :964 M 644 inline bzrlib/weave.py data 20738 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? # TODO: Separate out some code to read and write weaves. # TODO: End marker for each version so we can stop reading? # TODO: Check that no insertion occurs inside a deletion that was # active in the version of the insertion. # TODO: Perhaps a special slower check() method that verifies more # nesting constraints and the MD5 of each version? try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. To clients the "lines" of the file are represented as a list of strings. These strings will typically have terminal newline characters, but this is not required. In particular files commonly do not have a newline at the end of the file. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the set (included_versions), which lists the previous versions also considered active; the versions included in those versions are included transitively. So new versions created from nothing list []; most versions have a single entry; some have more. _sha1s List of hex SHA-1 of each version, or None if not recorded. """ def __init__(self): self._l = [] self._v = [] self._sha1s = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. This must normally include the parents and the parent's parents, or wierd things might happen. text Sequence of lines to be added in the new version.""" ## self._check_versions(parents) ## self._check_lines(text) idx = len(self._v) import sha s = sha.new() for l in text: s.update(l) sha1 = s.hexdigest() del s if parents: delta = self._delta(self.inclusions(parents), text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._addversion(parents) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._addversion(None) self._sha1s.append(sha1) return idx def inclusions(self, versions): """Expand out everything included by versions.""" i = set(versions) for v in versions: try: i.update(self._v[v]) except IndexError: raise ValueError("version %d not present in weave" % v) return i def _addversion(self, parents): if parents: self._v.append(frozenset(parents)) else: self._v.append(frozenset()) def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, version): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" included = self.inclusions([version]) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = None lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. # TODO: In fact, I think we only need to store the *count* of # active insertions and deletions, and we can maintain that by # just by just counting as we go along. WFE = WeaveFormatError for l in self._l: if isinstance(l, tuple): isactive = None # recalculate c, v = l if c == '{': if istack and (istack[-1] >= v): raise WFE("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WFE("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WFE("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WFE("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WFE("version %d deletes own text on line %d" % (v, lineno)) # XXX dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WFE("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WFE("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WFE("literal at top level on line %d" % lineno) if isactive == None: isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WFE("unclosed insertion blocks at end of weave", istack) if dset: raise WFE("unclosed deletion blocks at end of weave", dset) def get_iter(self, version): """Yield lines for the specified version.""" for origin, lineno, line in self._extract(self.inclusions([version])): yield line def get(self, index): return list(self.get_iter(index)) def mash_iter(self, included): """Return composed version of multiple included versions.""" included = frozenset(included) for origin, lineno, text in self._extract(included): yield text def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def numversions(self): l = len(self._v) assert l == len(self._sha1s) return l def check(self): # check no circular inclusions for version in range(self.numversions()): inclusions = list(self._v[version]) if inclusions: inclusions.sort() if inclusions[-1] >= version: raise WeaveFormatError("invalid included version %d for index %d" % (inclusions[-1], version)) # try extracting all versions; this is a bit slow and parallel # extraction could be used import sha for version in range(self.numversions()): s = sha.new() for l in self.get_iter(version): s.update(l) hd = s.hexdigest() expected = self._sha1s[version] if hd != expected: raise WeaveError("mismatched sha1 for version %d; " "got %s, expected %s" % (version, hd, expected)) def merge(self, merge_versions): """Automerge and mark conflicts between versions. This returns a sequence, each entry describing alternatives for a chunk of the file. Each of the alternatives is given as a list of lines. If there is a chunk of the file where there's no diagreement, only one alternative is given. """ # approach: find the included versions common to all the # merged versions raise NotImplementedError() def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ # basis a list of (origin, lineno, line) basis_lineno = [] basis_lines = [] for origin, lineno, line in self._extract(included): basis_lineno.append(lineno) basis_lines.append(line) # add a sentinal, because we can also match against the final line basis_lineno.append(len(self._l)) # XXX: which line of the weave should we really consider # matches the end of the file? the current code says it's the # last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis_lineno[i1] real_i2 = basis_lineno[i2] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def weave_info(filename, out): """Show some text information about the weave.""" from weavefile import read_weave wf = file(filename, 'rb') w = read_weave(wf) # FIXME: doesn't work on pipes weave_size = wf.tell() print >>out, "weave file size %d bytes" % weave_size print >>out, "weave contains %d versions" % len(w._v) total = 0 print ' %8s %8s %8s %s' % ('version', 'lines', 'bytes', 'sha1') print ' -------- -------- -------- ----------------------------------------' for i in range(len(w._v)): text = w.get(i) lines = len(text) bytes = sum((len(a) for a in text)) sha1 = w._sha1s[i] print ' %8d %8d %8d %s' % (i, lines, bytes, sha1) total += bytes print >>out, "versions total %d bytes" % total print >>out, "compression ratio %.3f" % (float(total)/float(weave_size)) def usage(): print """bzr weave tool: usage: weave init WEAVEFILE Create an empty weave file weave get WEAVEFILE VERSION Write out specified version. weave check WEAVEFILE Check consistency of all versions. weave info WEAVEFILE Display table of contents. weave add WEAVEFILE [BASE...] < NEWTEXT Add NEWTEXT, with specified parent versions. weave annotate WEAVEFILE VERSION Display origin of each line. weave mash WEAVEFILE VERSION... Display composite of all selected versions. weave merge WEAVEFILE VERSION1 VERSION2 > OUT Auto-merge two versions and display conflicts. """ def main(argv): import sys import os from weavefile import write_weave, read_weave cmd = argv[1] def readit(): return read_weave(file(argv[2], 'rb')) if cmd == 'help': usage() elif cmd == 'add': w = readit() # at the moment, based on everything in the file parents = map(int, argv[3:]) lines = sys.stdin.readlines() ver = w.add(parents, lines) write_weave(w, file(argv[2], 'wb')) print 'added version %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() write_weave(w, file(fn, 'wb')) elif cmd == 'get': # get one version w = readit() sys.stdout.writelines(w.get_iter(int(argv[3]))) elif cmd == 'mash': # get composite w = readit() sys.stdout.writelines(w.mash_iter(map(int, argv[3:]))) elif cmd == 'annotate': w = readit() # newline is added to all lines regardless; too hard to get # reasonable formatting otherwise lasto = None for origin, text in w.annotate(int(argv[3])): text = text.rstrip('\r\n') if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin elif cmd == 'info': weave_info(argv[2], sys.stdout) elif cmd == 'check': w = readit() w.check() else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) commit refs/heads/master mark :966 committer Martin Pool 1121047602 +1000 data 27 - better weave info display from :965 M 644 inline bzrlib/weave.py data 20780 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? # TODO: Separate out some code to read and write weaves. # TODO: End marker for each version so we can stop reading? # TODO: Check that no insertion occurs inside a deletion that was # active in the version of the insertion. # TODO: Perhaps a special slower check() method that verifies more # nesting constraints and the MD5 of each version? try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. To clients the "lines" of the file are represented as a list of strings. These strings will typically have terminal newline characters, but this is not required. In particular files commonly do not have a newline at the end of the file. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the set (included_versions), which lists the previous versions also considered active; the versions included in those versions are included transitively. So new versions created from nothing list []; most versions have a single entry; some have more. _sha1s List of hex SHA-1 of each version, or None if not recorded. """ def __init__(self): self._l = [] self._v = [] self._sha1s = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. This must normally include the parents and the parent's parents, or wierd things might happen. text Sequence of lines to be added in the new version.""" ## self._check_versions(parents) ## self._check_lines(text) idx = len(self._v) import sha s = sha.new() for l in text: s.update(l) sha1 = s.hexdigest() del s if parents: delta = self._delta(self.inclusions(parents), text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._addversion(parents) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._addversion(None) self._sha1s.append(sha1) return idx def inclusions(self, versions): """Expand out everything included by versions.""" i = set(versions) for v in versions: try: i.update(self._v[v]) except IndexError: raise ValueError("version %d not present in weave" % v) return i def _addversion(self, parents): if parents: self._v.append(frozenset(parents)) else: self._v.append(frozenset()) def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, version): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" included = self.inclusions([version]) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = None lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. # TODO: In fact, I think we only need to store the *count* of # active insertions and deletions, and we can maintain that by # just by just counting as we go along. WFE = WeaveFormatError for l in self._l: if isinstance(l, tuple): isactive = None # recalculate c, v = l if c == '{': if istack and (istack[-1] >= v): raise WFE("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WFE("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WFE("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WFE("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WFE("version %d deletes own text on line %d" % (v, lineno)) # XXX dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WFE("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WFE("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WFE("literal at top level on line %d" % lineno) if isactive == None: isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WFE("unclosed insertion blocks at end of weave", istack) if dset: raise WFE("unclosed deletion blocks at end of weave", dset) def get_iter(self, version): """Yield lines for the specified version.""" for origin, lineno, line in self._extract(self.inclusions([version])): yield line def get(self, index): return list(self.get_iter(index)) def mash_iter(self, included): """Return composed version of multiple included versions.""" included = frozenset(included) for origin, lineno, text in self._extract(included): yield text def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def numversions(self): l = len(self._v) assert l == len(self._sha1s) return l def check(self): # check no circular inclusions for version in range(self.numversions()): inclusions = list(self._v[version]) if inclusions: inclusions.sort() if inclusions[-1] >= version: raise WeaveFormatError("invalid included version %d for index %d" % (inclusions[-1], version)) # try extracting all versions; this is a bit slow and parallel # extraction could be used import sha for version in range(self.numversions()): s = sha.new() for l in self.get_iter(version): s.update(l) hd = s.hexdigest() expected = self._sha1s[version] if hd != expected: raise WeaveError("mismatched sha1 for version %d; " "got %s, expected %s" % (version, hd, expected)) def merge(self, merge_versions): """Automerge and mark conflicts between versions. This returns a sequence, each entry describing alternatives for a chunk of the file. Each of the alternatives is given as a list of lines. If there is a chunk of the file where there's no diagreement, only one alternative is given. """ # approach: find the included versions common to all the # merged versions raise NotImplementedError() def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ # basis a list of (origin, lineno, line) basis_lineno = [] basis_lines = [] for origin, lineno, line in self._extract(included): basis_lineno.append(lineno) basis_lines.append(line) # add a sentinal, because we can also match against the final line basis_lineno.append(len(self._l)) # XXX: which line of the weave should we really consider # matches the end of the file? the current code says it's the # last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis_lineno[i1] real_i2 = basis_lineno[i2] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def weave_info(filename, out): """Show some text information about the weave.""" from weavefile import read_weave wf = file(filename, 'rb') w = read_weave(wf) # FIXME: doesn't work on pipes weave_size = wf.tell() print >>out, "weave file size %d bytes" % weave_size print >>out, "weave contains %d versions" % len(w._v) total = 0 print '%6s %6s %8s %40s %20s' % ('ver', 'lines', 'bytes', 'sha1', 'parents') for i in (6, 6, 8, 40, 20): print '-' * i, print for i in range(len(w._v)): text = w.get(i) lines = len(text) bytes = sum((len(a) for a in text)) sha1 = w._sha1s[i] print '%6d %6d %8d %40s' % (i, lines, bytes, sha1), print ', '.join(map(str, w._v[i])) total += bytes print >>out, "versions total %d bytes" % total print >>out, "compression ratio %.3f" % (float(total)/float(weave_size)) def usage(): print """bzr weave tool: usage: weave init WEAVEFILE Create an empty weave file weave get WEAVEFILE VERSION Write out specified version. weave check WEAVEFILE Check consistency of all versions. weave info WEAVEFILE Display table of contents. weave add WEAVEFILE [BASE...] < NEWTEXT Add NEWTEXT, with specified parent versions. weave annotate WEAVEFILE VERSION Display origin of each line. weave mash WEAVEFILE VERSION... Display composite of all selected versions. weave merge WEAVEFILE VERSION1 VERSION2 > OUT Auto-merge two versions and display conflicts. """ def main(argv): import sys import os from weavefile import write_weave, read_weave cmd = argv[1] def readit(): return read_weave(file(argv[2], 'rb')) if cmd == 'help': usage() elif cmd == 'add': w = readit() # at the moment, based on everything in the file parents = map(int, argv[3:]) lines = sys.stdin.readlines() ver = w.add(parents, lines) write_weave(w, file(argv[2], 'wb')) print 'added version %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() write_weave(w, file(fn, 'wb')) elif cmd == 'get': # get one version w = readit() sys.stdout.writelines(w.get_iter(int(argv[3]))) elif cmd == 'mash': # get composite w = readit() sys.stdout.writelines(w.mash_iter(map(int, argv[3:]))) elif cmd == 'annotate': w = readit() # newline is added to all lines regardless; too hard to get # reasonable formatting otherwise lasto = None for origin, text in w.annotate(int(argv[3])): text = text.rstrip('\r\n') if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin elif cmd == 'info': weave_info(argv[2], sys.stdout) elif cmd == 'check': w = readit() w.check() else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) commit refs/heads/master mark :967 committer Martin Pool 1121049070 +1000 data 35 - add command for merge-based weave from :966 M 644 inline bzrlib/weave.py data 22030 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? # TODO: Separate out some code to read and write weaves. # TODO: End marker for each version so we can stop reading? # TODO: Check that no insertion occurs inside a deletion that was # active in the version of the insertion. # TODO: Perhaps a special slower check() method that verifies more # nesting constraints and the MD5 of each version? try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. To clients the "lines" of the file are represented as a list of strings. These strings will typically have terminal newline characters, but this is not required. In particular files commonly do not have a newline at the end of the file. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the set (included_versions), which lists the previous versions also considered active; the versions included in those versions are included transitively. So new versions created from nothing list []; most versions have a single entry; some have more. _sha1s List of hex SHA-1 of each version, or None if not recorded. """ def __init__(self): self._l = [] self._v = [] self._sha1s = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. This must normally include the parents and the parent's parents, or wierd things might happen. text Sequence of lines to be added in the new version.""" ## self._check_versions(parents) ## self._check_lines(text) idx = len(self._v) import sha s = sha.new() for l in text: s.update(l) sha1 = s.hexdigest() del s if parents: delta = self._delta(self.inclusions(parents), text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._addversion(parents) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._addversion(None) self._sha1s.append(sha1) return idx def inclusions(self, versions): """Expand out everything included by versions.""" i = set(versions) for v in versions: try: i.update(self._v[v]) except IndexError: raise ValueError("version %d not present in weave" % v) return i def _addversion(self, parents): if parents: self._v.append(frozenset(parents)) else: self._v.append(frozenset()) def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, version): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" included = self.inclusions([version]) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] # versions for which an insertion block is current dset = set() # versions for which a deletion block is current isactive = None lineno = 0 # line of weave, 0-based # TODO: Probably only need to put included revisions in the istack # TODO: Could split this into two functions, one that updates # the stack and the other that processes the results -- but # I'm not sure it's really needed. # TODO: In fact, I think we only need to store the *count* of # active insertions and deletions, and we can maintain that by # just by just counting as we go along. WFE = WeaveFormatError for l in self._l: if isinstance(l, tuple): isactive = None # recalculate c, v = l if c == '{': if istack and (istack[-1] >= v): raise WFE("improperly nested insertions %d>=%d on line %d" % (istack[-1], v, lineno)) istack.append(v) elif c == '}': try: oldv = istack.pop() except IndexError: raise WFE("unmatched close of insertion %d on line %d" % (v, lineno)) if oldv != v: raise WFE("mismatched close of insertion %d!=%d on line %d" % (oldv, v, lineno)) elif c == '[': # block deleted in v if v in dset: raise WFE("repeated deletion marker for version %d on line %d" % (v, lineno)) if istack: if istack[-1] == v: raise WFE("version %d deletes own text on line %d" % (v, lineno)) # XXX dset.add(v) elif c == ']': if v in dset: dset.remove(v) else: raise WFE("unmatched close of deletion %d on line %d" % (v, lineno)) else: raise WFE("invalid processing instruction %r on line %d" % (l, lineno)) else: assert isinstance(l, basestring) if not istack: raise WFE("literal at top level on line %d" % lineno) if isactive == None: isactive = (istack[-1] in included) \ and not included.intersection(dset) if isactive: origin = istack[-1] yield origin, lineno, l lineno += 1 if istack: raise WFE("unclosed insertion blocks at end of weave", istack) if dset: raise WFE("unclosed deletion blocks at end of weave", dset) def get_iter(self, version): """Yield lines for the specified version.""" for origin, lineno, line in self._extract(self.inclusions([version])): yield line def get(self, index): return list(self.get_iter(index)) def mash_iter(self, included): """Return composed version of multiple included versions.""" included = frozenset(included) for origin, lineno, text in self._extract(included): yield text def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def numversions(self): l = len(self._v) assert l == len(self._sha1s) return l def check(self): # check no circular inclusions for version in range(self.numversions()): inclusions = list(self._v[version]) if inclusions: inclusions.sort() if inclusions[-1] >= version: raise WeaveFormatError("invalid included version %d for index %d" % (inclusions[-1], version)) # try extracting all versions; this is a bit slow and parallel # extraction could be used import sha for version in range(self.numversions()): s = sha.new() for l in self.get_iter(version): s.update(l) hd = s.hexdigest() expected = self._sha1s[version] if hd != expected: raise WeaveError("mismatched sha1 for version %d; " "got %s, expected %s" % (version, hd, expected)) def merge(self, merge_versions): """Automerge and mark conflicts between versions. This returns a sequence, each entry describing alternatives for a chunk of the file. Each of the alternatives is given as a list of lines. If there is a chunk of the file where there's no diagreement, only one alternative is given. """ # approach: find the included versions common to all the # merged versions raise NotImplementedError() def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ # basis a list of (origin, lineno, line) basis_lineno = [] basis_lines = [] for origin, lineno, line in self._extract(included): basis_lineno.append(lineno) basis_lines.append(line) # add a sentinal, because we can also match against the final line basis_lineno.append(len(self._l)) # XXX: which line of the weave should we really consider # matches the end of the file? the current code says it's the # last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis_lineno[i1] real_i2 = basis_lineno[i2] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def weave_info(filename, out): """Show some text information about the weave.""" from weavefile import read_weave wf = file(filename, 'rb') w = read_weave(wf) # FIXME: doesn't work on pipes weave_size = wf.tell() print >>out, "weave file size %d bytes" % weave_size print >>out, "weave contains %d versions" % len(w._v) total = 0 print '%6s %6s %8s %40s %20s' % ('ver', 'lines', 'bytes', 'sha1', 'parents') for i in (6, 6, 8, 40, 20): print '-' * i, print for i in range(len(w._v)): text = w.get(i) lines = len(text) bytes = sum((len(a) for a in text)) sha1 = w._sha1s[i] print '%6d %6d %8d %40s' % (i, lines, bytes, sha1), print ', '.join(map(str, w._v[i])) total += bytes print >>out, "versions total %d bytes" % total print >>out, "compression ratio %.3f" % (float(total)/float(weave_size)) def usage(): print """bzr weave tool Experimental tool for weave algorithm. usage: weave init WEAVEFILE Create an empty weave file weave get WEAVEFILE VERSION Write out specified version. weave check WEAVEFILE Check consistency of all versions. weave info WEAVEFILE Display table of contents. weave add WEAVEFILE [BASE...] < NEWTEXT Add NEWTEXT, with specified parent versions. weave annotate WEAVEFILE VERSION Display origin of each line. weave mash WEAVEFILE VERSION... Display composite of all selected versions. weave merge WEAVEFILE VERSION1 VERSION2 > OUT Auto-merge two versions and display conflicts. example: % weave init foo.weave % vi foo.txt % weave add foo.weave < foo.txt added version 0 (create updated version) % vi foo.txt % weave get foo.weave 0 | diff -u - foo.txt % weave add foo.weave 0 < foo.txt added version 1 % weave get foo.weave 0 > foo.txt (create forked version) % vi foo.txt % weave add foo.weave 0 < foo.txt added version 2 % weave merge foo.weave 1 2 > foo.txt (merge them) % vi foo.txt (resolve conflicts) % weave add foo.weave 1 2 < foo.txt (commit merged version) """ def main(argv): import sys import os from weavefile import write_weave, read_weave cmd = argv[1] def readit(): return read_weave(file(argv[2], 'rb')) if cmd == 'help': usage() elif cmd == 'add': w = readit() # at the moment, based on everything in the file parents = map(int, argv[3:]) lines = sys.stdin.readlines() ver = w.add(parents, lines) write_weave(w, file(argv[2], 'wb')) print 'added version %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() write_weave(w, file(fn, 'wb')) elif cmd == 'get': # get one version w = readit() sys.stdout.writelines(w.get_iter(int(argv[3]))) elif cmd == 'mash': # get composite w = readit() sys.stdout.writelines(w.mash_iter(map(int, argv[3:]))) elif cmd == 'annotate': w = readit() # newline is added to all lines regardless; too hard to get # reasonable formatting otherwise lasto = None for origin, text in w.annotate(int(argv[3])): text = text.rstrip('\r\n') if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin elif cmd == 'info': weave_info(argv[2], sys.stdout) elif cmd == 'check': w = readit() w.check() elif cmd == 'merge': if len(argv) != 5: usage() return 1 w = readit() v1, v2 = map(int, argv[3:5]) basis = w.inclusions([v1]).intersection(w.inclusions([v2])) base_lines = list(w.mash_iter(basis)) a_lines = list(w.get(v1)) b_lines = list(w.get(v2)) from bzrlib.merge3 import Merge3 m3 = Merge3(base_lines, a_lines, b_lines) name_a = 'version %d' % v1 name_b = 'version %d' % v2 sys.stdout.writelines(m3.merge_lines(name_a=name_a, name_b=name_b)) else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) commit refs/heads/master mark :968 committer Martin Pool 1121050535 +1000 data 19 - update testweave from :967 M 644 inline tools/testweave.py data 18096 #! /usr/bin/python2.4 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test suite for weave algorithm""" import testsweet from bzrlib.weave import Weave, WeaveFormatError from bzrlib.weavefile import write_weave, read_weave from pprint import pformat try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class TestBase(testsweet.TestBase): def check_read_write(self, k): """Check the weave k can be written & re-read.""" from tempfile import TemporaryFile tf = TemporaryFile() write_weave(k, tf) tf.seek(0) k2 = read_weave(tf) if k != k2: tf.seek(0) self.log('serialized weave:') self.log(tf.read()) self.fail('read/write check failed') class Easy(TestBase): def runTest(self): k = Weave() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Weave() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class DeltaAdd(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): k = Weave() k.add([], ['line 1']) self.assertEqual(k._l, [('{', 0), 'line 1', ('}', 0), ]) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # currently there are 3 lines in the weave, and we insert after them self.assertEquals(changes, [(3, 3, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(1, 1, ['top line'])]) self.check_read_write(k) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Weave() self.assertRaises(ValueError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): k = Weave() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) text3 = ['line 1', 'middle line', 'line 2'] k.add([0, 1], text3) self.log("changes to text3: " + pformat(list(k._delta(set([0, 1]), text3)))) self.log("k._l=" + pformat(k._l)) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) # now multiple insertions at different places k.add([0, 1, 3], ['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc']) self.assertEqual(k.annotate(4), [(0, 'line 1'), (4, 'aaa'), (3, 'middle line'), (4, 'bbb'), (1, 'line 2'), (4, 'ccc')]) class DeleteLines(TestBase): """Deletion of lines from existing text. Try various texts all based on a common ancestor.""" def runTest(self): k = Weave() base_text = ['one', 'two', 'three', 'four'] k.add([], base_text) texts = [['one', 'two', 'three'], ['two', 'three', 'four'], ['one', 'four'], ['one', 'two', 'three', 'four'], ] for t in texts: ver = k.add([0], t) self.log('final weave:') self.log('k._l=' + pformat(k._l)) for i in range(len(texts)): self.assertEqual(k.get(i+1), texts[i]) class SuicideDelete(TestBase): """Invalid weave which tries to add and delete simultaneously.""" def runTest(self): k = Weave() k._v = [(), ] k._l = [('{', 0), 'first line', ('[', 0), 'deleted in 0', (']', 0), ('}', 0), ] self.assertRaises(WeaveFormatError, k.get, 0) class CannedDelete(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [(), frozenset([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'last line', ]) class CannedReplacement(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), ('{', 1), 'replacement line', ('}', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'replacement line', 'last line', ]) class BadWeave(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [frozenset(), ] k._l = ['bad line', ('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) class BadInsert(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0]), frozenset([0]), frozenset([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 1), ' more in 1', ('}', 1), ('}', 1), ('}', 0)] self.assertRaises(WeaveFormatError, k.get, 0) self.assertRaises(WeaveFormatError, k.get, 1) class InsertNested(TestBase): """Insertion with nested instructions.""" def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0]), frozenset([0]), frozenset([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertEqual(k.get(0), ['foo {', '}']) self.assertEqual(k.get(1), ['foo {', ' added in version 1', ' also from v1', '}']) self.assertEqual(k.get(2), ['foo {', ' added in v2', '}']) self.assertEqual(k.get(3), ['foo {', ' added in version 1', ' added in v2', ' also from v1', '}']) class DeleteLines2(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Weave() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) self.assertEqual(k.annotate(1), [(0, "line the first"), (0, "fine")]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a weave with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0])] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1)] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Weave with two diverged texts based on version 0. """ def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0]), frozenset([0]), ] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1), ('{', 2), "alternative second line", ('}', 2), ] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) self.assertEqual(k.inclusions([2]), set([0, 2])) class ReplaceLine(TestBase): def runTest(self): k = Weave() text0 = ['cheddar', 'stilton', 'gruyere'] text1 = ['cheddar', 'blue vein', 'neufchatel', 'chevre'] k.add([], text0) k.add([0], text1) self.log('k._l=' + pformat(k._l)) self.assertEqual(k.get(0), text0) self.assertEqual(k.get(1), text1) class Merge(TestBase): """Storage of versions that merge diverged parents""" def runTest(self): k = Weave() texts = [['header'], ['header', '', 'line from 1'], ['header', '', 'line from 2', 'more from 2'], ['header', '', 'line from 1', 'fixup line', 'line from 2'], ] k.add([], texts[0]) k.add([0], texts[1]) k.add([0], texts[2]) k.add([0, 1, 2], texts[3]) for i, t in enumerate(texts): self.assertEqual(k.get(i), t) self.assertEqual(k.annotate(3), [(0, 'header'), (1, ''), (1, 'line from 1'), (3, 'fixup line'), (2, 'line from 2'), ]) self.assertEqual(k.inclusions([3]), set([0, 1, 2, 3])) self.log('k._l=' + pformat(k._l)) self.check_read_write(k) class Conflicts(TestBase): """Test detection of conflicting regions during a merge. A base version is inserted, then two descendents try to insert different lines in the same place. These should be reported as a possible conflict and forwarded to the user.""" def runTest(self): return # NOT RUN k = Weave() k.add([], ['aaa', 'bbb']) k.add([0], ['aaa', '111', 'bbb']) k.add([1], ['aaa', '222', 'bbb']) merged = k.merge([1, 2]) self.assertEquals([[['aaa']], [['111'], ['222']], [['bbb']]]) class NonConflict(TestBase): """Two descendants insert compatible changes. No conflict should be reported.""" def runTest(self): return # NOT RUN k = Weave() k.add([], ['aaa', 'bbb']) k.add([0], ['111', 'aaa', 'ccc', 'bbb']) k.add([1], ['aaa', 'ccc', 'bbb', '222']) class AutoMerge(TestBase): def runTest(self): k = Weave() texts = [['header', 'aaa', 'bbb'], ['header', 'aaa', 'line from 1', 'bbb'], ['header', 'aaa', 'bbb', 'line from 2', 'more from 2'], ] k.add([], texts[0]) k.add([0], texts[1]) k.add([0], texts[2]) self.log('k._l=' + pformat(k._l)) m = list(k.mash_iter([0, 1, 2])) self.assertEqual(m, ['header', 'aaa', 'line from 1', 'bbb', 'line from 2', 'more from 2']) class Khayyam(TestBase): """Test changes to multi-line texts, and read/write""" def runTest(self): rawtexts = [ """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, -- and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise enow!""", """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, -- and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now!""", """A Book of poems underneath the tree, A Jug of Wine, a Loaf of Bread, and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now! -- O. Khayyam""", """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now!""", ] texts = [[l.strip() for l in t.split('\n')] for t in rawtexts] k = Weave() parents = set() for t in texts: ver = k.add(list(parents), t) parents.add(ver) self.log("k._l=" + pformat(k._l)) for i, t in enumerate(texts): self.assertEqual(k.get(i), t) self.check_read_write(k) def testweave(): import testsweet from unittest import TestSuite, TestLoader import testweave tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testweave)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testweave()) commit refs/heads/master mark :969 committer Martin Pool 1121050549 +1000 data 18 - ignore test.log from :968 M 644 inline .bzrignore data 139 ## *.diff ./doc/*.html *.py[oc] *~ .arch-ids .bzr.profile .arch-inventory {arch} CHANGELOG bzr-test.log bzr.1 ,,* testbzr.log api test.log commit refs/heads/master mark :970 committer Martin Pool 1121051184 +1000 data 7 - typo from :969 commit refs/heads/master mark :971 committer Martin Pool 1121051270 +1000 data 10 - fix typo from :970 commit refs/heads/master mark :972 committer Martin Pool 1121051503 +1000 data 10 - fix typo from :971 commit refs/heads/master mark :973 committer Martin Pool 1121051513 +1000 data 10 - fix typo from :972 commit refs/heads/master mark :974 committer Martin Pool 1121051531 +1000 data 10 - fix typo from :973 M 644 inline bzrlib/hashcache.py data 6948 # (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # TODO: Perhaps have a way to stat all the files in inode order, and # then remember that they're all fresh for the lifetime of the object? # TODO: Keep track of whether there are in-memory updates that need to # be flushed. # TODO: Perhaps return more details on the file to avoid statting it # again: nonexistent, file type, size, etc CACHE_HEADER = "### bzr hashcache v5\n" def _fingerprint(abspath): import os, stat try: fs = os.lstat(abspath) except OSError: # might be missing, etc return None if stat.S_ISDIR(fs.st_mode): return None # we discard any high precision because it's not reliable; perhaps we # could do better on some systems? return (fs.st_size, long(fs.st_mtime), long(fs.st_ctime), fs.st_ino, fs.st_dev) class HashCache(object): """Cache for looking up file SHA-1. Files are considered to match the cached value if the fingerprint of the file has not changed. This includes its mtime, ctime, device number, inode number, and size. This should catch modifications or replacement of the file by a new one. This may not catch modifications that do not change the file's size and that occur within the resolution window of the timestamps. To handle this we specifically do not cache files which have changed since the start of the present second, since they could undetectably change again. This scheme may fail if the machine's clock steps backwards. Don't do that. This does not canonicalize the paths passed in; that should be done by the caller. _cache Indexed by path, points to a two-tuple of the SHA-1 of the file. and its fingerprint. stat_count number of times files have been statted hit_count number of times files have been retrieved from the cache, avoiding a re-read miss_count number of misses (times files have been completely re-read) """ needs_write = False def __init__(self, basedir): self.basedir = basedir self.hit_count = 0 self.miss_count = 0 self.stat_count = 0 self.danger_count = 0 self._cache = {} def cache_file_name(self): import os.path return os.path.join(self.basedir, '.bzr', 'stat-cache') def clear(self): """Discard all cached information. This does not reset the counters.""" if self._cache: self.needs_write = True self._cache = {} def get_sha1(self, path): """Return the hex SHA-1 of the contents of the file at path. XXX: If the file does not exist or is not a plain file??? """ import os, time from bzrlib.osutils import sha_file from bzrlib.trace import mutter abspath = os.path.join(self.basedir, path) fp = _fingerprint(abspath) c = self._cache.get(path) if c: cache_sha1, cache_fp = c else: cache_sha1, cache_fp = None, None self.stat_count += 1 if not fp: # not a regular file return None elif cache_fp and (cache_fp == fp): self.hit_count += 1 return cache_sha1 else: self.miss_count += 1 digest = sha_file(file(abspath, 'rb')) now = int(time.time()) if fp[1] >= now or fp[2] >= now: # changed too recently; can't be cached. we can # return the result and it could possibly be cached # next time. self.danger_count += 1 if cache_fp: mutter("remove outdated entry for %s" % path) self.needs_write = True del self._cache[path] elif (fp != cache_fp) or (digest != cache_sha1): mutter("update entry for %s" % path) mutter(" %r" % (fp,)) mutter(" %r" % (cache_fp,)) self.needs_write = True self._cache[path] = (digest, fp) return digest def write(self): """Write contents of cache to file.""" from atomicfile import AtomicFile outf = AtomicFile(self.cache_file_name(), 'wb') try: print >>outf, CACHE_HEADER, for path, c in self._cache.iteritems(): assert '//' not in path, path outf.write(path.encode('utf-8')) outf.write('// ') print >>outf, c[0], # hex sha1 for fld in c[1]: print >>outf, "%d" % fld, print >>outf outf.commit() self.needs_write = False finally: if not outf.closed: outf.abort() def read(self): """Reinstate cache from file. Overwrites existing cache. If the cache file has the wrong version marker, this just clears the cache.""" from bzrlib.trace import mutter, warning self._cache = {} fn = self.cache_file_name() try: inf = file(fn, 'rb') except IOError, e: mutter("failed to open %s: %s" % (fn, e)) return hdr = inf.readline() if hdr != CACHE_HEADER: mutter('cache header marker not found at top of %s; discarding cache' % fn) return for l in inf: pos = l.index('// ') path = l[:pos].decode('utf-8') if path in self._cache: warning('duplicated path %r in cache' % path) continue pos += 3 fields = l[pos:].split(' ') if len(fields) != 6: warning("bad line in hashcache: %r" % l) continue sha1 = fields[0] if len(sha1) != 40: warning("bad sha1 in hashcache: %r" % sha1) continue fp = tuple(map(long, fields[1:])) self._cache[path] = (sha1, fp) self.needs_write = False commit refs/heads/master mark :975 committer Martin Pool 1121051554 +1000 data 10 - fix typo from :974 commit refs/heads/master mark :976 committer Martin Pool 1121051575 +1000 data 5 trace from :975 M 644 inline bzrlib/commit.py data 10835 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # FIXME: "bzr commit doc/format" commits doc/format.txt! def commit(branch, message, timestamp=None, timezone=None, committer=None, verbose=True, specific_files=None, rev_id=None): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. specific_files If true, commit only those files. rev_id If set, use this as the new revision id. Useful for test or import commands that need to tightly control what revisions are assigned. If you duplicate a revision id that exists elsewhere it is your own fault. If null (default), a time/random revision id is generated. """ import time, tempfile from bzrlib.osutils import local_time_offset, username from bzrlib.branch import gen_file_id from bzrlib.errors import BzrError from bzrlib.revision import Revision, RevisionReference from bzrlib.trace import mutter, note from bzrlib.xml import pack_xml branch.lock_write() try: # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_tree = branch.working_tree() work_inv = work_tree.inventory basis = branch.basis_tree() basis_inv = basis.inventory if verbose: note('looking for changes...') pending_merges = branch.pending_merges() missing_ids, new_inv = _gather_commit(branch, work_tree, work_inv, basis_inv, specific_files, verbose) for file_id in missing_ids: # Any files that have been deleted are now removed from the # working inventory. Files that were not selected for commit # are left as they were in the working inventory and ommitted # from the revision inventory. # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itbranch. if work_inv.has_id(file_id): del work_inv[file_id] if rev_id is None: rev_id = _gen_revision_id(time.time()) inv_id = rev_id inv_tmp = tempfile.TemporaryFile() pack_xml(new_inv, inv_tmp) inv_tmp.seek(0) branch.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) # We could also just sha hash the inv_tmp file # however, in the case that branch.inventory_store.add() # ever actually does anything special inv_sha1 = branch.get_inventory_sha1(inv_id) branch._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, message = message, inventory_id=inv_id, inventory_sha1=inv_sha1, revision_id=rev_id) rev.parents = [] precursor_id = branch.last_patch() if precursor_id: precursor_sha1 = branch.get_revision_sha1(precursor_id) rev.parents.append(RevisionReference(precursor_id, precursor_sha1)) for merge_rev in pending_merges: rev.parents.append(RevisionReference(merge_rev)) rev_tmp = tempfile.TemporaryFile() pack_xml(rev, rev_tmp) rev_tmp.seek(0) branch.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (branch.revno() + 1)) branch.append_revision(rev_id) branch.set_pending_merges([]) if verbose: note("commited r%d" % branch.revno()) finally: branch.unlock() def _gen_revision_id(when): """Return new revision-id.""" from binascii import hexlify from osutils import rand_bytes, compact_date, user_email s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def _gather_commit(branch, work_tree, work_inv, basis_inv, specific_files, verbose): """Build inventory preparatory to commit. This adds any changed files into the text store, and sets their test-id, sha and size in the returned inventory appropriately. missing_ids Modified to hold a list of files that have been deleted from the working directory; these should be removed from the working inventory. """ from bzrlib.inventory import Inventory from osutils import isdir, isfile, sha_string, quotefn, \ local_time_offset, username, kind_marker, is_inside_any from branch import gen_file_id from errors import BzrError from revision import Revision from bzrlib.trace import mutter, note inv = Inventory() missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). p = branch.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if specific_files and not is_inside_any(specific_files, path): mutter(' skipping file excluded from commit') if basis_inv.has_id(file_id): # carry over with previous state inv.add(basis_inv[file_id].copy()) else: # omit this from committed inventory pass continue if not work_tree.has_id(file_id): if verbose: print('deleted %s%s' % (path, kind_marker(entry.kind))) mutter(" file is missing, removing from inventory") missing_ids.append(file_id) continue # this is present in the new inventory; may be new, modified or # unchanged. old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] entry = entry.copy() inv.add(entry) if old_ie: old_kind = old_ie.kind if old_kind != entry.kind: raise BzrError("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): raise BzrError("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): raise BzrError("%s is entered as file but is not a file" % quotefn(p)) new_sha1 = work_tree.get_file_sha1(file_id) if (old_ie and old_ie.text_sha1 == new_sha1): ## assert content == basis.get_file(file_id).read() entry.text_id = old_ie.text_id entry.text_sha1 = new_sha1 entry.text_size = old_ie.text_size mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: content = file(p, 'rb').read() # calculate the sha again, just in case the file contents # changed since we updated the cache entry.text_sha1 = sha_string(content) entry.text_size = len(content) entry.text_id = gen_file_id(entry.name) branch.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: marked = path + kind_marker(entry.kind) if not old_ie: print 'added', marked elif old_ie == entry: pass # unchanged elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): print 'modified', marked else: print 'renamed', marked return missing_ids, inv commit refs/heads/master mark :977 committer Martin Pool 1121051789 +1000 data 25 - faster weave extraction from :976 M 644 inline bzrlib/weave.py data 20398 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? # TODO: Separate out some code to read and write weaves. # TODO: End marker for each version so we can stop reading? # TODO: Check that no insertion occurs inside a deletion that was # active in the version of the insertion. # TODO: Perhaps a special slower check() method that verifies more # nesting constraints and the MD5 of each version? try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. To clients the "lines" of the file are represented as a list of strings. These strings will typically have terminal newline characters, but this is not required. In particular files commonly do not have a newline at the end of the file. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the set (included_versions), which lists the previous versions also considered active; the versions included in those versions are included transitively. So new versions created from nothing list []; most versions have a single entry; some have more. _sha1s List of hex SHA-1 of each version, or None if not recorded. """ def __init__(self): self._l = [] self._v = [] self._sha1s = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. This must normally include the parents and the parent's parents, or wierd things might happen. text Sequence of lines to be added in the new version.""" ## self._check_versions(parents) ## self._check_lines(text) idx = len(self._v) import sha s = sha.new() for l in text: s.update(l) sha1 = s.hexdigest() del s if parents: delta = self._delta(self.inclusions(parents), text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._addversion(parents) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._addversion(None) self._sha1s.append(sha1) return idx def inclusions(self, versions): """Expand out everything included by versions.""" i = set(versions) for v in versions: try: i.update(self._v[v]) except IndexError: raise ValueError("version %d not present in weave" % v) return i def _addversion(self, parents): if parents: self._v.append(frozenset(parents)) else: self._v.append(frozenset()) def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, version): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" included = self.inclusions([version]) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] dset = set() lineno = 0 # line of weave, 0-based isactive = False WFE = WeaveFormatError for l in self._l: if isinstance(l, tuple): c, v = l if v in included: # only active blocks are interesting if c == '{': assert v not in istack istack.append(v) isactive = not dset elif c == '}': oldv = istack.pop() assert oldv == v isactive = istack and not dset elif c == '[': assert v not in dset dset.add(v) isactive = False else: assert c == ']' assert v in dset dset.remove(v) isactive = istack and not dset else: assert isinstance(l, basestring) if isactive: yield origin, lineno, l lineno += 1 if istack: raise WFE("unclosed insertion blocks at end of weave", istack) if dset: raise WFE("unclosed deletion blocks at end of weave", dset) def get_iter(self, version): """Yield lines for the specified version.""" for origin, lineno, line in self._extract(self.inclusions([version])): yield line def get(self, index): return list(self.get_iter(index)) def mash_iter(self, included): """Return composed version of multiple included versions.""" included = frozenset(included) for origin, lineno, text in self._extract(included): yield text def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def numversions(self): l = len(self._v) assert l == len(self._sha1s) return l def check(self): # check no circular inclusions for version in range(self.numversions()): inclusions = list(self._v[version]) if inclusions: inclusions.sort() if inclusions[-1] >= version: raise WeaveFormatError("invalid included version %d for index %d" % (inclusions[-1], version)) # try extracting all versions; this is a bit slow and parallel # extraction could be used import sha for version in range(self.numversions()): s = sha.new() for l in self.get_iter(version): s.update(l) hd = s.hexdigest() expected = self._sha1s[version] if hd != expected: raise WeaveError("mismatched sha1 for version %d; " "got %s, expected %s" % (version, hd, expected)) # TODO: check insertions are properly nested, that there are # no lines outside of insertion blocks, that deletions are # properly paired, etc. def merge(self, merge_versions): """Automerge and mark conflicts between versions. This returns a sequence, each entry describing alternatives for a chunk of the file. Each of the alternatives is given as a list of lines. If there is a chunk of the file where there's no diagreement, only one alternative is given. """ # approach: find the included versions common to all the # merged versions raise NotImplementedError() def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ # basis a list of (origin, lineno, line) basis_lineno = [] basis_lines = [] for origin, lineno, line in self._extract(included): basis_lineno.append(lineno) basis_lines.append(line) # add a sentinal, because we can also match against the final line basis_lineno.append(len(self._l)) # XXX: which line of the weave should we really consider # matches the end of the file? the current code says it's the # last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis_lineno[i1] real_i2 = basis_lineno[i2] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def weave_info(filename, out): """Show some text information about the weave.""" from weavefile import read_weave wf = file(filename, 'rb') w = read_weave(wf) # FIXME: doesn't work on pipes weave_size = wf.tell() print >>out, "weave file size %d bytes" % weave_size print >>out, "weave contains %d versions" % len(w._v) total = 0 print '%6s %6s %8s %40s %20s' % ('ver', 'lines', 'bytes', 'sha1', 'parents') for i in (6, 6, 8, 40, 20): print '-' * i, print for i in range(len(w._v)): text = w.get(i) lines = len(text) bytes = sum((len(a) for a in text)) sha1 = w._sha1s[i] print '%6d %6d %8d %40s' % (i, lines, bytes, sha1), print ', '.join(map(str, w._v[i])) total += bytes print >>out, "versions total %d bytes" % total print >>out, "compression ratio %.3f" % (float(total)/float(weave_size)) def usage(): print """bzr weave tool Experimental tool for weave algorithm. usage: weave init WEAVEFILE Create an empty weave file weave get WEAVEFILE VERSION Write out specified version. weave check WEAVEFILE Check consistency of all versions. weave info WEAVEFILE Display table of contents. weave add WEAVEFILE [BASE...] < NEWTEXT Add NEWTEXT, with specified parent versions. weave annotate WEAVEFILE VERSION Display origin of each line. weave mash WEAVEFILE VERSION... Display composite of all selected versions. weave merge WEAVEFILE VERSION1 VERSION2 > OUT Auto-merge two versions and display conflicts. example: % weave init foo.weave % vi foo.txt % weave add foo.weave < foo.txt added version 0 (create updated version) % vi foo.txt % weave get foo.weave 0 | diff -u - foo.txt % weave add foo.weave 0 < foo.txt added version 1 % weave get foo.weave 0 > foo.txt (create forked version) % vi foo.txt % weave add foo.weave 0 < foo.txt added version 2 % weave merge foo.weave 1 2 > foo.txt (merge them) % vi foo.txt (resolve conflicts) % weave add foo.weave 1 2 < foo.txt (commit merged version) """ def main(argv): import sys import os from weavefile import write_weave, read_weave cmd = argv[1] def readit(): return read_weave(file(argv[2], 'rb')) if cmd == 'help': usage() elif cmd == 'add': w = readit() # at the moment, based on everything in the file parents = map(int, argv[3:]) lines = sys.stdin.readlines() ver = w.add(parents, lines) write_weave(w, file(argv[2], 'wb')) print 'added version %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() write_weave(w, file(fn, 'wb')) elif cmd == 'get': # get one version w = readit() sys.stdout.writelines(w.get_iter(int(argv[3]))) elif cmd == 'mash': # get composite w = readit() sys.stdout.writelines(w.mash_iter(map(int, argv[3:]))) elif cmd == 'annotate': w = readit() # newline is added to all lines regardless; too hard to get # reasonable formatting otherwise lasto = None for origin, text in w.annotate(int(argv[3])): text = text.rstrip('\r\n') if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin elif cmd == 'info': weave_info(argv[2], sys.stdout) elif cmd == 'check': w = readit() w.check() elif cmd == 'merge': if len(argv) != 5: usage() return 1 w = readit() v1, v2 = map(int, argv[3:5]) basis = w.inclusions([v1]).intersection(w.inclusions([v2])) base_lines = list(w.mash_iter(basis)) a_lines = list(w.get(v1)) b_lines = list(w.get(v2)) from bzrlib.merge3 import Merge3 m3 = Merge3(base_lines, a_lines, b_lines) name_a = 'version %d' % v1 name_b = 'version %d' % v2 sys.stdout.writelines(m3.merge_lines(name_a=name_a, name_b=name_b)) else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) commit refs/heads/master mark :978 committer Martin Pool 1121052463 +1000 data 72 - Optionally raise EmptyCommit if there are no changes. Test for this. from :977 M 644 inline bzrlib/commit.py data 11283 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # FIXME: "bzr commit doc/format" commits doc/format.txt! def commit(branch, message, timestamp=None, timezone=None, committer=None, verbose=True, specific_files=None, rev_id=None, allow_empty=True): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. This raises EmptyCommit if there are no changes, no new merges, and allow_empty is false. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. specific_files If true, commit only those files. rev_id If set, use this as the new revision id. Useful for test or import commands that need to tightly control what revisions are assigned. If you duplicate a revision id that exists elsewhere it is your own fault. If null (default), a time/random revision id is generated. """ import time, tempfile from bzrlib.osutils import local_time_offset, username from bzrlib.branch import gen_file_id from bzrlib.errors import BzrError, EmptyCommit from bzrlib.revision import Revision, RevisionReference from bzrlib.trace import mutter, note from bzrlib.xml import pack_xml branch.lock_write() try: # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_tree = branch.working_tree() work_inv = work_tree.inventory basis = branch.basis_tree() basis_inv = basis.inventory if verbose: note('looking for changes...') pending_merges = branch.pending_merges() missing_ids, new_inv, any_changes = \ _gather_commit(branch, work_tree, work_inv, basis_inv, specific_files, verbose) if not (any_changes or allow_empty or pending_merges): raise EmptyCommit() for file_id in missing_ids: # Any files that have been deleted are now removed from the # working inventory. Files that were not selected for commit # are left as they were in the working inventory and ommitted # from the revision inventory. # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itbranch. if work_inv.has_id(file_id): del work_inv[file_id] if rev_id is None: rev_id = _gen_revision_id(time.time()) inv_id = rev_id inv_tmp = tempfile.TemporaryFile() pack_xml(new_inv, inv_tmp) inv_tmp.seek(0) branch.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) # We could also just sha hash the inv_tmp file # however, in the case that branch.inventory_store.add() # ever actually does anything special inv_sha1 = branch.get_inventory_sha1(inv_id) branch._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, message = message, inventory_id=inv_id, inventory_sha1=inv_sha1, revision_id=rev_id) rev.parents = [] precursor_id = branch.last_patch() if precursor_id: precursor_sha1 = branch.get_revision_sha1(precursor_id) rev.parents.append(RevisionReference(precursor_id, precursor_sha1)) for merge_rev in pending_merges: rev.parents.append(RevisionReference(merge_rev)) rev_tmp = tempfile.TemporaryFile() pack_xml(rev, rev_tmp) rev_tmp.seek(0) branch.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (branch.revno() + 1)) branch.append_revision(rev_id) branch.set_pending_merges([]) if verbose: note("commited r%d" % branch.revno()) finally: branch.unlock() def _gen_revision_id(when): """Return new revision-id.""" from binascii import hexlify from osutils import rand_bytes, compact_date, user_email s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def _gather_commit(branch, work_tree, work_inv, basis_inv, specific_files, verbose): """Build inventory preparatory to commit. Returns missing_ids, new_inv, any_changes. This adds any changed files into the text store, and sets their test-id, sha and size in the returned inventory appropriately. missing_ids Modified to hold a list of files that have been deleted from the working directory; these should be removed from the working inventory. """ from bzrlib.inventory import Inventory from osutils import isdir, isfile, sha_string, quotefn, \ local_time_offset, username, kind_marker, is_inside_any from branch import gen_file_id from errors import BzrError from revision import Revision from bzrlib.trace import mutter, note any_changes = False inv = Inventory() missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). p = branch.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if specific_files and not is_inside_any(specific_files, path): mutter(' skipping file excluded from commit') if basis_inv.has_id(file_id): # carry over with previous state inv.add(basis_inv[file_id].copy()) else: # omit this from committed inventory pass continue if not work_tree.has_id(file_id): if verbose: print('deleted %s%s' % (path, kind_marker(entry.kind))) any_changes = True mutter(" file is missing, removing from inventory") missing_ids.append(file_id) continue # this is present in the new inventory; may be new, modified or # unchanged. old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] entry = entry.copy() inv.add(entry) if old_ie: old_kind = old_ie.kind if old_kind != entry.kind: raise BzrError("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): raise BzrError("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): raise BzrError("%s is entered as file but is not a file" % quotefn(p)) new_sha1 = work_tree.get_file_sha1(file_id) if (old_ie and old_ie.text_sha1 == new_sha1): ## assert content == basis.get_file(file_id).read() entry.text_id = old_ie.text_id entry.text_sha1 = new_sha1 entry.text_size = old_ie.text_size mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: content = file(p, 'rb').read() # calculate the sha again, just in case the file contents # changed since we updated the cache entry.text_sha1 = sha_string(content) entry.text_size = len(content) entry.text_id = gen_file_id(entry.name) branch.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: marked = path + kind_marker(entry.kind) if not old_ie: print 'added', marked any_changes = True elif old_ie == entry: pass # unchanged elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): print 'modified', marked any_changes = True else: print 'renamed', marked any_changes = True return missing_ids, inv, any_changes M 644 inline bzrlib/errors.py data 2015 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " ###################################################################### # exceptions class BzrError(StandardError): pass class BzrCheckError(BzrError): pass class BzrCommandError(BzrError): # Error from malformed user command pass class NotBranchError(BzrError): """Specified path is not in a branch""" pass class NotVersionedError(BzrError): """Specified object is not versioned.""" class BadFileKindError(BzrError): """Specified file is of a kind that cannot be added. (For example a symlink or device file.)""" pass class ForbiddenFileError(BzrError): """Cannot operate on a file because it is a control file.""" pass class LockError(Exception): """All exceptions from the lock/unlock functions should be from this exception class. They will be translated as necessary. The original exception is available as e.original_error """ def __init__(self, e=None): self.original_error = e if e: Exception.__init__(self, e) else: Exception.__init__(self) class EmptyCommit(Exception): """Commit failed because nothing was changed.""" M 644 inline bzrlib/selftest/whitebox.py data 7012 #! /usr/bin/python import os import unittest from bzrlib.selftest import InTempDir, TestBase from bzrlib.branch import ScratchBranch, Branch from bzrlib.errors import NotBranchError, NotVersionedError class Unknowns(InTempDir): def runTest(self): b = Branch('.', init=True) self.build_tree(['hello.txt', 'hello.txt~']) self.assertEquals(list(b.unknowns()), ['hello.txt']) class NoChanges(InTempDir): def runTest(self): from bzrlib.errors import EmptyCommit b = Branch('.', init=True) self.build_tree(['hello.txt']) self.assertRaises(EmptyCommit, b.commit, 'commit without adding', allow_empty=False) class ValidateRevisionId(TestBase): def runTest(self): from bzrlib.revision import validate_revision_id validate_revision_id('mbp@sourcefrog.net-20050311061123-96a255005c7c9dbe') self.assertRaises(ValueError, validate_revision_id, ' asdkjas') self.assertRaises(ValueError, validate_revision_id, 'mbp@sourcefrog.net-20050311061123-96a255005c7c9dbe\n') self.assertRaises(ValueError, validate_revision_id, ' mbp@sourcefrog.net-20050311061123-96a255005c7c9dbe') self.assertRaises(ValueError, validate_revision_id, 'Martin Pool -20050311061123-96a255005c7c9dbe') class PendingMerges(InTempDir): """Tracking pending-merged revisions.""" def runTest(self): b = Branch('.', init=True) self.assertEquals(b.pending_merges(), []) b.add_pending_merge('foo@azkhazan-123123-abcabc') self.assertEquals(b.pending_merges(), ['foo@azkhazan-123123-abcabc']) b.add_pending_merge('foo@azkhazan-123123-abcabc') self.assertEquals(b.pending_merges(), ['foo@azkhazan-123123-abcabc']) b.add_pending_merge('wibble@fofof--20050401--1928390812') self.assertEquals(b.pending_merges(), ['foo@azkhazan-123123-abcabc', 'wibble@fofof--20050401--1928390812']) b.commit("commit from base with two merges") rev = b.get_revision(b.revision_history()[0]) self.assertEquals(len(rev.parents), 2) self.assertEquals(rev.parents[0].revision_id, 'foo@azkhazan-123123-abcabc') self.assertEquals(rev.parents[1].revision_id, 'wibble@fofof--20050401--1928390812') # list should be cleared when we do a commit self.assertEquals(b.pending_merges(), []) class Revert(InTempDir): """Test selected-file revert""" def runTest(self): b = Branch('.', init=True) self.build_tree(['hello.txt']) file('hello.txt', 'w').write('initial hello') self.assertRaises(NotVersionedError, b.revert, ['hello.txt']) b.add(['hello.txt']) b.commit('create initial hello.txt') self.check_file_contents('hello.txt', 'initial hello') file('hello.txt', 'w').write('new hello') self.check_file_contents('hello.txt', 'new hello') # revert file modified since last revision b.revert(['hello.txt']) self.check_file_contents('hello.txt', 'initial hello') self.check_file_contents('hello.txt~', 'new hello') # reverting again clobbers the backup b.revert(['hello.txt']) self.check_file_contents('hello.txt', 'initial hello') self.check_file_contents('hello.txt~', 'initial hello') class RenameDirs(InTempDir): """Test renaming directories and the files within them.""" def runTest(self): b = Branch('.', init=True) self.build_tree(['dir/', 'dir/sub/', 'dir/sub/file']) b.add(['dir', 'dir/sub', 'dir/sub/file']) b.commit('create initial state') # TODO: lift out to a test helper that checks the shape of # an inventory revid = b.revision_history()[0] self.log('first revision_id is {%s}' % revid) inv = b.get_revision_inventory(revid) self.log('contents of inventory: %r' % inv.entries()) self.check_inventory_shape(inv, ['dir', 'dir/sub', 'dir/sub/file']) b.rename_one('dir', 'newdir') self.check_inventory_shape(b.inventory, ['newdir', 'newdir/sub', 'newdir/sub/file']) b.rename_one('newdir/sub', 'newdir/newsub') self.check_inventory_shape(b.inventory, ['newdir', 'newdir/newsub', 'newdir/newsub/file']) class BranchPathTestCase(TestBase): """test for branch path lookups Branch.relpath and bzrlib.branch._relpath do a simple but subtle job: given a path (either relative to cwd or absolute), work out if it is inside a branch and return the path relative to the base. """ def runTest(self): from bzrlib.branch import _relpath import tempfile, shutil savedir = os.getcwdu() dtmp = tempfile.mkdtemp() def rp(p): return _relpath(dtmp, p) try: # check paths inside dtmp while standing outside it self.assertEqual(rp(os.path.join(dtmp, 'foo')), 'foo') # root = nothing self.assertEqual(rp(dtmp), '') self.assertRaises(NotBranchError, rp, '/etc') # now some near-miss operations -- note that # os.path.commonprefix gets these wrong! self.assertRaises(NotBranchError, rp, dtmp.rstrip('\\/') + '2') self.assertRaises(NotBranchError, rp, dtmp.rstrip('\\/') + '2/foo') # now operations based on relpath of files in current # directory, or nearby os.chdir(dtmp) self.assertEqual(rp('foo/bar/quux'), 'foo/bar/quux') self.assertEqual(rp('foo'), 'foo') self.assertEqual(rp('./foo'), 'foo') self.assertEqual(rp(os.path.abspath('foo')), 'foo') self.assertRaises(NotBranchError, rp, '../foo') finally: os.chdir(savedir) shutil.rmtree(dtmp) TEST_CLASSES = [Unknowns, ValidateRevisionId, PendingMerges, Revert, RenameDirs, BranchPathTestCase, ] commit refs/heads/master mark :979 committer Martin Pool 1121052590 +1000 data 40 - more tests for detecting empty commits from :978 M 644 inline bzrlib/selftest/whitebox.py data 7649 #! /usr/bin/python import os import unittest from bzrlib.selftest import InTempDir, TestBase from bzrlib.branch import ScratchBranch, Branch from bzrlib.errors import NotBranchError, NotVersionedError class Unknowns(InTempDir): def runTest(self): b = Branch('.', init=True) self.build_tree(['hello.txt', 'hello.txt~']) self.assertEquals(list(b.unknowns()), ['hello.txt']) class NoChanges(InTempDir): def runTest(self): from bzrlib.errors import EmptyCommit b = Branch('.', init=True) self.build_tree(['hello.txt']) self.assertRaises(EmptyCommit, b.commit, 'commit without adding', allow_empty=False) b.commit('commit empty tree', allow_empty=True) b.add('hello.txt') b.commit('commit first added file', allow_empty=False) self.assertRaises(EmptyCommit, b.commit, 'commit after adding file', allow_empty=False) b.commit('commit pointless revision with one file', allow_empty=True) b.add_pending_merge('mbp@892739123-2005-123123') b.commit('commit new merge with no text changes', allow_empty=False) class ValidateRevisionId(TestBase): def runTest(self): from bzrlib.revision import validate_revision_id validate_revision_id('mbp@sourcefrog.net-20050311061123-96a255005c7c9dbe') self.assertRaises(ValueError, validate_revision_id, ' asdkjas') self.assertRaises(ValueError, validate_revision_id, 'mbp@sourcefrog.net-20050311061123-96a255005c7c9dbe\n') self.assertRaises(ValueError, validate_revision_id, ' mbp@sourcefrog.net-20050311061123-96a255005c7c9dbe') self.assertRaises(ValueError, validate_revision_id, 'Martin Pool -20050311061123-96a255005c7c9dbe') class PendingMerges(InTempDir): """Tracking pending-merged revisions.""" def runTest(self): b = Branch('.', init=True) self.assertEquals(b.pending_merges(), []) b.add_pending_merge('foo@azkhazan-123123-abcabc') self.assertEquals(b.pending_merges(), ['foo@azkhazan-123123-abcabc']) b.add_pending_merge('foo@azkhazan-123123-abcabc') self.assertEquals(b.pending_merges(), ['foo@azkhazan-123123-abcabc']) b.add_pending_merge('wibble@fofof--20050401--1928390812') self.assertEquals(b.pending_merges(), ['foo@azkhazan-123123-abcabc', 'wibble@fofof--20050401--1928390812']) b.commit("commit from base with two merges") rev = b.get_revision(b.revision_history()[0]) self.assertEquals(len(rev.parents), 2) self.assertEquals(rev.parents[0].revision_id, 'foo@azkhazan-123123-abcabc') self.assertEquals(rev.parents[1].revision_id, 'wibble@fofof--20050401--1928390812') # list should be cleared when we do a commit self.assertEquals(b.pending_merges(), []) class Revert(InTempDir): """Test selected-file revert""" def runTest(self): b = Branch('.', init=True) self.build_tree(['hello.txt']) file('hello.txt', 'w').write('initial hello') self.assertRaises(NotVersionedError, b.revert, ['hello.txt']) b.add(['hello.txt']) b.commit('create initial hello.txt') self.check_file_contents('hello.txt', 'initial hello') file('hello.txt', 'w').write('new hello') self.check_file_contents('hello.txt', 'new hello') # revert file modified since last revision b.revert(['hello.txt']) self.check_file_contents('hello.txt', 'initial hello') self.check_file_contents('hello.txt~', 'new hello') # reverting again clobbers the backup b.revert(['hello.txt']) self.check_file_contents('hello.txt', 'initial hello') self.check_file_contents('hello.txt~', 'initial hello') class RenameDirs(InTempDir): """Test renaming directories and the files within them.""" def runTest(self): b = Branch('.', init=True) self.build_tree(['dir/', 'dir/sub/', 'dir/sub/file']) b.add(['dir', 'dir/sub', 'dir/sub/file']) b.commit('create initial state') # TODO: lift out to a test helper that checks the shape of # an inventory revid = b.revision_history()[0] self.log('first revision_id is {%s}' % revid) inv = b.get_revision_inventory(revid) self.log('contents of inventory: %r' % inv.entries()) self.check_inventory_shape(inv, ['dir', 'dir/sub', 'dir/sub/file']) b.rename_one('dir', 'newdir') self.check_inventory_shape(b.inventory, ['newdir', 'newdir/sub', 'newdir/sub/file']) b.rename_one('newdir/sub', 'newdir/newsub') self.check_inventory_shape(b.inventory, ['newdir', 'newdir/newsub', 'newdir/newsub/file']) class BranchPathTestCase(TestBase): """test for branch path lookups Branch.relpath and bzrlib.branch._relpath do a simple but subtle job: given a path (either relative to cwd or absolute), work out if it is inside a branch and return the path relative to the base. """ def runTest(self): from bzrlib.branch import _relpath import tempfile, shutil savedir = os.getcwdu() dtmp = tempfile.mkdtemp() def rp(p): return _relpath(dtmp, p) try: # check paths inside dtmp while standing outside it self.assertEqual(rp(os.path.join(dtmp, 'foo')), 'foo') # root = nothing self.assertEqual(rp(dtmp), '') self.assertRaises(NotBranchError, rp, '/etc') # now some near-miss operations -- note that # os.path.commonprefix gets these wrong! self.assertRaises(NotBranchError, rp, dtmp.rstrip('\\/') + '2') self.assertRaises(NotBranchError, rp, dtmp.rstrip('\\/') + '2/foo') # now operations based on relpath of files in current # directory, or nearby os.chdir(dtmp) self.assertEqual(rp('foo/bar/quux'), 'foo/bar/quux') self.assertEqual(rp('foo'), 'foo') self.assertEqual(rp('./foo'), 'foo') self.assertEqual(rp(os.path.abspath('foo')), 'foo') self.assertRaises(NotBranchError, rp, '../foo') finally: os.chdir(savedir) shutil.rmtree(dtmp) TEST_CLASSES = [Unknowns, ValidateRevisionId, PendingMerges, Revert, RenameDirs, BranchPathTestCase, ] commit refs/heads/master mark :980 committer Martin Pool 1121052745 +1000 data 31 - reenable blackbox test module from :979 M 644 inline bzrlib/selftest/__init__.py data 2909 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from testsweet import TestBase, run_suite, InTempDir MODULES_TO_TEST = [] MODULES_TO_DOCTEST = [] def selftest(): from unittest import TestLoader, TestSuite import bzrlib, bzrlib.store, bzrlib.inventory, bzrlib.branch import bzrlib.osutils, bzrlib.commands, bzrlib.merge3 global MODULES_TO_TEST, MODULES_TO_DOCTEST import bzrlib.selftest.whitebox import bzrlib.selftest.blackbox import bzrlib.selftest.versioning import bzrlib.selftest.testmerge3 import bzrlib.selftest.testhashcache import bzrlib.merge_core from doctest import DocTestSuite import os import shutil import time import sys import unittest for m in (bzrlib.store, bzrlib.inventory, bzrlib.branch, bzrlib.osutils, bzrlib.commands, bzrlib.merge3): if m not in MODULES_TO_DOCTEST: MODULES_TO_DOCTEST.append(m) for m in (bzrlib.selftest.whitebox, bzrlib.selftest.versioning, bzrlib.selftest.testmerge3, bzrlib.selftest.testhashcache, bzrlib.selftest.blackbox): if m not in MODULES_TO_TEST: MODULES_TO_TEST.append(m) TestBase.BZRPATH = os.path.join(os.path.realpath(os.path.dirname(bzrlib.__path__[0])), 'bzr') print '%-30s %s' % ('bzr binary', TestBase.BZRPATH) print suite = TestSuite() # should also test bzrlib.merge_core, but they seem to be out of date with # the code. # XXX: python2.3's TestLoader() doesn't seem to find all the # tests; don't know why for m in MODULES_TO_TEST: suite.addTest(TestLoader().loadTestsFromModule(m)) for m in (MODULES_TO_DOCTEST): suite.addTest(DocTestSuite(m)) # for cl in (bzrlib.selftest.whitebox.TEST_CLASSES # + bzrlib.selftest.versioning.TEST_CLASSES # + bzrlib.selftest.testmerge3.TEST_CLASSES # + bzrlib.selftest.testhashcache.TEST_CLASSES # + bzrlib.selftest.blackbox.TEST_CLASSES): # suite.addTest(cl()) suite.addTest(unittest.makeSuite(bzrlib.merge_core.MergeTest, 'test_')) return run_suite(suite, 'testbzr') commit refs/heads/master mark :981 committer Martin Pool 1121053202 +1000 data 77 - commit command refuses unless something is changed or --unchanged is given from :980 M 644 inline NEWS data 9410 DEVELOPMENT HEAD NEW FEATURES: * Python plugins, automatically loaded from the directories on BZR_PLUGIN_PATH or ~/.bzr.conf/plugins by default. * New 'bzr mkdir' command. * Commit mesage is fetched from an editor if not given on the command line; patch from Torsten Marek. CHANGES: * When exporting to a tarball with ``bzr export --format tgz``, put everything under a top directory rather than dumping it into the current directory. This can be overridden with the ``--root`` option. Patch from William Dodé and John Meinel. * New ``bzr upgrade`` command to upgrade the format of a branch, replacing ``bzr check --update``. * Files within store directories are no longer marked readonly on disk. * Changed ``bzr log`` output to a more compact form suggested by John A Meinel. Old format is available with the ``--long`` or ``-l`` option, patched by William Dodé. * By default the commit command refuses to record a revision with no changes unless the ``--unchanged`` option is given. bzr-0.0.5 2005-06-15 CHANGES: * ``bzr`` with no command now shows help rather than giving an error. Suggested by Michael Ellerman. * ``bzr status`` output format changed, because svn-style output doesn't really match the model of bzr. Now files are grouped by status and can be shown with their IDs. ``bzr status --all`` shows all versioned files and unknown files but not ignored files. * ``bzr log`` runs from most-recent to least-recent, the reverse of the previous order. The previous behaviour can be obtained with the ``--forward`` option. * ``bzr inventory`` by default shows only filenames, and also ids if ``--show-ids`` is given, in which case the id is the second field. ENHANCEMENTS: * New 'bzr whoami --email' option shows only the email component of the user identification, from Jo Vermeulen. * New ``bzr ignore PATTERN`` command. * Nicer error message for broken pipe, interrupt and similar conditions that don't indicate an internal error. * Add ``.*.sw[nop] .git .*.tmp *,v`` to default ignore patterns. * Per-branch locks keyed on ``.bzr/branch-lock``, available in either read or write mode. * New option ``bzr log --show-ids`` shows revision and file ids. * New usage ``bzr log FILENAME`` shows only revisions that affected that file. * Changed format for describing changes in ``bzr log -v``. * New option ``bzr commit --file`` to take a message from a file, suggested by LarstiQ. * New syntax ``bzr status [FILE...]`` contributed by Bartosz Oler. File may be in a branch other than the working directory. * ``bzr log`` and ``bzr root`` can be given an http URL instead of a filename. * Commands can now be defined by external programs or scripts in a directory on $BZRPATH. * New "stat cache" avoids reading the contents of files if they haven't changed since the previous time. * If the Python interpreter is too old, try to find a better one or give an error. Based on a patch from Fredrik Lundh. * New optional parameter ``bzr info [BRANCH]``. * New form ``bzr commit SELECTED`` to commit only selected files. * New form ``bzr log -r FROM:TO`` shows changes in selected range; contributed by John A Meinel. * New option ``bzr diff --diff-options 'OPTS'`` allows passing options through to an external GNU diff. * New option ``bzr add --no-recurse`` to add a directory but not their contents. * ``bzr --version`` now shows more information if bzr is being run from a branch. BUG FIXES: * Fixed diff format so that added and removed files will be handled properly by patch. Fix from Lalo Martins. * Various fixes for files whose names contain spaces or other metacharacters. TESTING: * Converted black-box test suites from Bourne shell into Python; now run using ``./testbzr``. Various structural improvements to the tests. * testbzr by default runs the version of bzr found in the same directory as the tests, or the one given as the first parameter. * testbzr also runs the internal tests, so the only command required to check is just ``./testbzr``. * testbzr requires python2.4, but can be used to test bzr running under a different version. * Tests added for many other changes in this release. INTERNAL: * Included ElementTree library upgraded to 1.2.6 by Fredrik Lundh. * Refactor command functions into Command objects based on HCT by Scott James Remnant. * Better help messages for many commands. * Expose bzrlib.open_tracefile() to start the tracefile; until this is called trace messages are just discarded. * New internal function find_touching_revisions() and hidden command touching-revisions trace the changes to a given file. * Simpler and faster compare_inventories() function. * bzrlib.open_tracefile() takes a tracefilename parameter. * New AtomicFile class. * New developer commands ``added``, ``modified``. PORTABILITY: * Cope on Windows on python2.3 by using the weaker random seed. 2.4 is now only recommended. bzr-0.0.4 2005-04-22 ENHANCEMENTS: * 'bzr diff' optionally takes a list of files to diff. Still a bit basic. Patch from QuantumG. * More default ignore patterns. * New 'bzr log --verbose' shows a list of files changed in the changeset. Patch from Sebastian Cote. * Roll over ~/.bzr.log if it gets too large. * Command abbreviations 'ci', 'st', 'stat', '?' based on a patch by Jason Diamon. * New 'bzr help commands' based on a patch from Denys Duchier. CHANGES: * User email is determined by looking at $BZREMAIL or ~/.bzr.email or $EMAIL. All are decoded by the locale preferred encoding. If none of these are present user@hostname is used. The host's fully-qualified name is not used because that tends to fail when there are DNS problems. * New 'bzr whoami' command instead of username user-email. BUG FIXES: * Make commit safe for hardlinked bzr trees. * Some Unicode/locale fixes. * Partial workaround for difflib.unified_diff not handling trailing newlines properly. INTERNAL: * Allow docstrings for help to be in PEP0257 format. Patch from Matt Brubeck. * More tests in test.sh. * Write profile data to a temporary file not into working directory and delete it when done. * Smaller .bzr.log with process ids. PORTABILITY: * Fix opening of ~/.bzr.log on Windows. Patch from Andrew Bennetts. * Some improvements in handling paths on Windows, based on a patch from QuantumG. bzr-0.0.3 2005-04-06 ENHANCEMENTS: * New "directories" internal command lists versioned directories in the tree. * Can now say "bzr commit --help". * New "rename" command to rename one file to a different name and/or directory. * New "move" command to move one or more files into a different directory. * New "renames" command lists files renamed since base revision. * New cat command contributed by janmar. CHANGES: * .bzr.log is placed in $HOME (not pwd) and is always written in UTF-8. (Probably not a completely good long-term solution, but will do for now.) PORTABILITY: * Workaround for difflib bug in Python 2.3 that causes an exception when comparing empty files. Reported by Erik Toubro Nielsen. INTERNAL: * Refactored inventory storage to insert a root entry at the top. TESTING: * Start of shell-based black-box testing in test.sh. bzr-0.0.2.1 PORTABILITY: * Win32 fixes from Steve Brown. bzr-0.0.2 "black cube" 2005-03-31 ENHANCEMENTS: * Default ignore list extended (see bzrlib/__init__.py). * Patterns in .bzrignore are now added to the default ignore list, rather than replacing it. * Ignore list isn't reread for every file. * More help topics. * Reinstate the 'bzr check' command to check invariants of the branch. * New 'ignored' command lists which files are ignored and why; 'deleted' lists files deleted in the current working tree. * Performance improvements. * New global --profile option. * Ignore patterns like './config.h' now correctly match files in the root directory only. bzr-0.0.1 2005-03-26 ENHANCEMENTS: * More information from info command. * Can now say "bzr help COMMAND" for more detailed help. * Less file flushing and faster performance when writing logs and committing to stores. * More useful verbose output from some commands. BUG FIXES: * Fix inverted display of 'R' and 'M' during 'commit -v'. PORTABILITY: * Include a subset of ElementTree-1.2.20040618 to make installation easier. * Fix time.localtime call to work with Python 2.3 (the minimum supported). bzr-0.0.0.69 2005-03-22 ENHANCEMENTS: * First public release. * Storage of local versions: init, add, remove, rm, info, log, diff, status, etc. M 644 inline bzrlib/commands.py data 53644 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import BzrError, BzrCheckError, BzrCommandError from bzrlib.branch import find_branch from bzrlib import BZRDIR plugin_cmds = {} def register_command(cmd): "Utility function to help register a command" global plugin_cmds k = cmd.__name__ if k.startswith("cmd_"): k_unsquished = _unsquish_command_name(k) else: k_unsquished = k if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = cmd else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r' % sys.modules[cmd.__module__]) def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _get_cmd_dict(plugins_override=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v # If we didn't load plugins, the plugin_cmds dict will be empty if plugins_override: d.update(plugin_cmds) else: d2 = plugin_cmds.copy() d2.update(d) d = d2 return d def get_all_cmds(plugins_override=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(plugins_override=plugins_override).iteritems(): yield k,v def get_cmd_class(cmd, plugins_override=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(plugins_override=plugins_override) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() for opt in self.takes_options: if not opt in OPTIONS: raise BzrError("Unknown option '%s' returned by external command %s" % (opt, path)) # TODO: Is there any way to check takes_args is valid here? self.takes_args = pipe.readline().split() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-usage'" % path) pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-help'" % path) def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: optname = name.replace('_','-') value = kargs[name] if OPTIONS.has_key(optname): # it's an option opts.append('--%s' % optname) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = find_branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): from bzrlib.xml import pack_xml pack_xml(find_branch('.').get_revision(revision_id), sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print find_branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): from bzrlib.add import smart_add smart_add(file_list, verbose, not no_recurse) class cmd_mkdir(Command): """Create a new versioned directory. This is equivalent to creating the directory and then adding it. """ takes_args = ['dir+'] def run(self, dir_list): b = None for d in dir_list: os.mkdir(d) if not b: b = find_branch(d) b.add([d], verbose=True) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print find_branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = find_branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = find_branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = find_branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import tempfile from shutil import rmtree import errno br_to = find_branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if e.errno != errno.ENOENT: raise if location is None: if stored_loc is None: raise BzrCommandError("No pull location known or specified.") else: print "Using last location: %s" % stored_loc location = stored_loc cache_root = tempfile.mkdtemp() from bzrlib.branch import DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: from branch import find_cached_branch, DivergedBranches br_from = find_cached_branch(location, cache_root) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged." " Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") finally: rmtree(cache_root) class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. To retrieve the branch as of a particular revision, supply the --revision parameter, as in "branch foo/bar -r 5". """ takes_args = ['from_location', 'to_location?'] takes_options = ['revision'] def run(self, from_location, to_location=None, revision=None): import errno from bzrlib.merge import merge from bzrlib.branch import DivergedBranches, NoSuchRevision, \ find_cached_branch, Branch from shutil import rmtree from meta_store import CachedStore import tempfile cache_root = tempfile.mkdtemp() try: try: br_from = find_cached_branch(from_location, cache_root) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not' ' exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location.rstrip("/\\")) try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already' ' exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) try: br_to.update_revisions(br_from, stop_revision=revision) except NoSuchRevision: rmtree(to_location) msg = "The branch %s has no revision %d." % (from_location, revision) raise BzrCommandError(msg) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False, ignore_zero=True) from_location = pull_loc(br_from) br_to.controlfile("x-pull", "wb").write(from_location + "\n") finally: rmtree(cache_root) def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = find_branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = find_branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = find_branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: raise BzrError("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = find_branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: raise BzrError("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in find_branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in find_branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): from bzrlib.branch import Branch Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = find_branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = find_branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): from bzrlib.diff import compare_trees b = find_branch('.') td = compare_trees(b.basis_tree(), b.working_tree()) for path, id, kind in td.modified: print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = find_branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision','long'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None, long=False): from bzrlib.branch import find_branch from bzrlib.log import log_formatter, show_log import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') if long: log_format = 'long' else: log_format = 'short' lf = log_formatter(log_format, show_ids=show_ids, to_file=outf, show_timezone=timezone) show_log(b, lf, file_id, verbose=verbose, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = find_branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = find_branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): from bzrlib.osutils import quotefn for f in find_branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = find_branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = find_branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print find_branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, try to find the format with the extension. If no extension is found exports to a directory (equivalent to --format=dir). Root may be the top directory for tar, tgz and tbz2 formats. If none is given, the top directory will be the root name of the file.""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format', 'root'] def run(self, dest, revision=None, format=None, root=None): import os.path b = find_branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) root, ext = os.path.splitext(dest) if not format: if ext in (".tar",): format = "tar" elif ext in (".gz", ".tgz"): format = "tgz" elif ext in (".bz2", ".tbz2"): format = "tbz2" else: format = "dir" t.export(dest, format, root) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = find_branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose', 'unchanged'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None, unchanged=False): from bzrlib.errors import PointlessCommit from bzrlib.osutils import get_text_message ## Warning: shadows builtin file() if not message and not file: import cStringIO stdout = sys.stdout catcher = cStringIO.StringIO() sys.stdout = catcher cmd_status({"file_list":selected_list}, {}) info = catcher.getvalue() sys.stdout = stdout message = get_text_message(info) if message is None: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = find_branch('.') try: b.commit(message, verbose=verbose, specific_files=selected_list, allow_pointless=unchanged) except PointlessCommit: raise BzrCommandError("no changes to commit", ["use --unchanged to commit anyhow"]) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.check import check check(find_branch(dir)) class cmd_upgrade(Command): """Upgrade branch storage to current format. This should normally be used only after the check command tells you to run it. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.upgrade import upgrade upgrade(find_branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest return int(not selftest()) class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Restore selected files from a previous revision. """ takes_args = ['file+'] def run(self, file_list): from bzrlib.branch import find_branch if not file_list: file_list = ['.'] b = find_branch(file_list[0]) b.revert([b.relpath(f) for f in file_list]) class cmd_merge_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_plugins(Command): """List plugins""" hidden = True def run(self): import bzrlib.plugin from pprint import pprint pprint(bzrlib.plugin.all_plugins) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'unchanged': None, 'update': None, 'long': None, 'root': str, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', 'l': 'long', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) >>> parse_args('log -r 500'.split()) (['log'], {'revision': 500}) >>> parse_args('log -r500:600'.split()) (['log'], {'revision': [500, 600]}) >>> parse_args('log -vr500:600'.split()) (['log'], {'verbose': True, 'revision': [500, 600]}) >>> parse_args('log -rv500:600'.split()) #the r takes an argument Traceback (most recent call last): ... ValueError: invalid literal for int(): v500 """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: raise BzrError('unknown long option %r' % a) else: shortopt = a[1:] if shortopt in SHORT_OPTIONS: # Multi-character options must have a space to delimit # their value optname = SHORT_OPTIONS[shortopt] else: # Single character short options, can be chained, # and have their value appended to their name shortopt = a[1:2] if shortopt not in SHORT_OPTIONS: # We didn't find the multi-character name, and we # didn't find the single char name raise BzrError('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if a[2:]: # There are extra things on this option # see if it is the value, or if it is another # short option optargfn = OPTIONS[optname] if optargfn is None: # This option does not take an argument, so the # next entry is another short option, pack it back # into the list argv.insert(0, '-' + a[2:]) else: # This option takes an argument, so pack it # into the array optarg = a[2:] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? raise BzrError('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: raise BzrError('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: raise BzrError('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def _parse_master_args(argv): """Parse the arguments that always go with the original command. These are things like bzr --no-plugins, etc. There are now 2 types of option flags. Ones that come *before* the command, and ones that come *after* the command. Ones coming *before* the command are applied against all possible commands. And are generally applied before plugins are loaded. The current list are: --builtin Allow plugins to load, but don't let them override builtin commands, they will still be allowed if they do not override a builtin. --no-plugins Don't load any plugins. This lets you get back to official source behavior. --profile Enable the hotspot profile before running the command. For backwards compatibility, this is also a non-master option. --version Spit out the version of bzr that is running and exit. This is also a non-master option. --help Run help and exit, also a non-master option (I think that should stay, though) >>> argv, opts = _parse_master_args(['bzr', '--test']) Traceback (most recent call last): ... BzrCommandError: Invalid master option: 'test' >>> argv, opts = _parse_master_args(['bzr', '--version', 'command']) >>> print argv ['command'] >>> print opts['version'] True >>> argv, opts = _parse_master_args(['bzr', '--profile', 'command', '--more-options']) >>> print argv ['command', '--more-options'] >>> print opts['profile'] True >>> argv, opts = _parse_master_args(['bzr', '--no-plugins', 'command']) >>> print argv ['command'] >>> print opts['no-plugins'] True >>> print opts['profile'] False >>> argv, opts = _parse_master_args(['bzr', 'command', '--profile']) >>> print argv ['command', '--profile'] >>> print opts['profile'] False """ master_opts = {'builtin':False, 'no-plugins':False, 'version':False, 'profile':False, 'help':False } # This is the point where we could hook into argv[0] to determine # what front-end is supposed to be run # For now, we are just ignoring it. cmd_name = argv.pop(0) for arg in argv[:]: if arg[:2] != '--': # at the first non-option, we return the rest break arg = arg[2:] # Remove '--' if arg not in master_opts: # We could say that this is not an error, that we should # just let it be handled by the main section instead raise BzrCommandError('Invalid master option: %r' % arg) argv.pop(0) # We are consuming this entry master_opts[arg] = True return argv, master_opts def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: # some options like --builtin and --no-plugins have special effects argv, master_opts = _parse_master_args(argv) if not master_opts['no-plugins']: from bzrlib.plugin import load_plugins load_plugins() args, opts = parse_args(argv) if master_opts['help']: from bzrlib.help import help if argv: help(argv[0]) else: help() return 0 if 'help' in opts: from bzrlib.help import help if args: help(args[0]) else: help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 plugins_override = not (master_opts['builtin']) canonical_cmd, cmd_class = get_cmd_class(cmd, plugins_override=plugins_override) profile = master_opts['profile'] # For backwards compatibility, I would rather stick with --profile being a # master/global option if 'profile' in opts: profile = True del opts['profile'] # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): bzrlib.trace.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: import errno quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) M 644 inline bzrlib/commit.py data 11308 # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # FIXME: "bzr commit doc/format" commits doc/format.txt! def commit(branch, message, timestamp=None, timezone=None, committer=None, verbose=True, specific_files=None, rev_id=None, allow_pointless=True): """Commit working copy as a new revision. The basic approach is to add all the file texts into the store, then the inventory, then make a new revision pointing to that inventory and store that. This is not quite safe if the working copy changes during the commit; for the moment that is simply not allowed. A better approach is to make a temporary copy of the files before computing their hashes, and then add those hashes in turn to the inventory. This should mean at least that there are no broken hash pointers. There is no way we can get a snapshot of the whole directory at an instant. This would also have to be robust against files disappearing, moving, etc. So the whole thing is a bit hard. This raises PointlessCommit if there are no changes, no new merges, and allow_pointless is false. timestamp -- if not None, seconds-since-epoch for a postdated/predated commit. specific_files If true, commit only those files. rev_id If set, use this as the new revision id. Useful for test or import commands that need to tightly control what revisions are assigned. If you duplicate a revision id that exists elsewhere it is your own fault. If null (default), a time/random revision id is generated. """ import time, tempfile from bzrlib.osutils import local_time_offset, username from bzrlib.branch import gen_file_id from bzrlib.errors import BzrError, PointlessCommit from bzrlib.revision import Revision, RevisionReference from bzrlib.trace import mutter, note from bzrlib.xml import pack_xml branch.lock_write() try: # First walk over the working inventory; and both update that # and also build a new revision inventory. The revision # inventory needs to hold the text-id, sha1 and size of the # actual file versions committed in the revision. (These are # not present in the working inventory.) We also need to # detect missing/deleted files, and remove them from the # working inventory. work_tree = branch.working_tree() work_inv = work_tree.inventory basis = branch.basis_tree() basis_inv = basis.inventory if verbose: note('looking for changes...') pending_merges = branch.pending_merges() missing_ids, new_inv, any_changes = \ _gather_commit(branch, work_tree, work_inv, basis_inv, specific_files, verbose) if not (any_changes or allow_pointless or pending_merges): raise PointlessCommit() for file_id in missing_ids: # Any files that have been deleted are now removed from the # working inventory. Files that were not selected for commit # are left as they were in the working inventory and ommitted # from the revision inventory. # have to do this later so we don't mess up the iterator. # since parents may be removed before their children we # have to test. # FIXME: There's probably a better way to do this; perhaps # the workingtree should know how to filter itbranch. if work_inv.has_id(file_id): del work_inv[file_id] if rev_id is None: rev_id = _gen_revision_id(time.time()) inv_id = rev_id inv_tmp = tempfile.TemporaryFile() pack_xml(new_inv, inv_tmp) inv_tmp.seek(0) branch.inventory_store.add(inv_tmp, inv_id) mutter('new inventory_id is {%s}' % inv_id) # We could also just sha hash the inv_tmp file # however, in the case that branch.inventory_store.add() # ever actually does anything special inv_sha1 = branch.get_inventory_sha1(inv_id) branch._write_inventory(work_inv) if timestamp == None: timestamp = time.time() if committer == None: committer = username() if timezone == None: timezone = local_time_offset() mutter("building commit log message") rev = Revision(timestamp=timestamp, timezone=timezone, committer=committer, message = message, inventory_id=inv_id, inventory_sha1=inv_sha1, revision_id=rev_id) rev.parents = [] precursor_id = branch.last_patch() if precursor_id: precursor_sha1 = branch.get_revision_sha1(precursor_id) rev.parents.append(RevisionReference(precursor_id, precursor_sha1)) for merge_rev in pending_merges: rev.parents.append(RevisionReference(merge_rev)) rev_tmp = tempfile.TemporaryFile() pack_xml(rev, rev_tmp) rev_tmp.seek(0) branch.revision_store.add(rev_tmp, rev_id) mutter("new revision_id is {%s}" % rev_id) ## XXX: Everything up to here can simply be orphaned if we abort ## the commit; it will leave junk files behind but that doesn't ## matter. ## TODO: Read back the just-generated changeset, and make sure it ## applies and recreates the right state. ## TODO: Also calculate and store the inventory SHA1 mutter("committing patch r%d" % (branch.revno() + 1)) branch.append_revision(rev_id) branch.set_pending_merges([]) if verbose: note("commited r%d" % branch.revno()) finally: branch.unlock() def _gen_revision_id(when): """Return new revision-id.""" from binascii import hexlify from osutils import rand_bytes, compact_date, user_email s = '%s-%s-' % (user_email(), compact_date(when)) s += hexlify(rand_bytes(8)) return s def _gather_commit(branch, work_tree, work_inv, basis_inv, specific_files, verbose): """Build inventory preparatory to commit. Returns missing_ids, new_inv, any_changes. This adds any changed files into the text store, and sets their test-id, sha and size in the returned inventory appropriately. missing_ids Modified to hold a list of files that have been deleted from the working directory; these should be removed from the working inventory. """ from bzrlib.inventory import Inventory from osutils import isdir, isfile, sha_string, quotefn, \ local_time_offset, username, kind_marker, is_inside_any from branch import gen_file_id from errors import BzrError from revision import Revision from bzrlib.trace import mutter, note any_changes = False inv = Inventory() missing_ids = [] for path, entry in work_inv.iter_entries(): ## TODO: Check that the file kind has not changed from the previous ## revision of this file (if any). p = branch.abspath(path) file_id = entry.file_id mutter('commit prep file %s, id %r ' % (p, file_id)) if specific_files and not is_inside_any(specific_files, path): mutter(' skipping file excluded from commit') if basis_inv.has_id(file_id): # carry over with previous state inv.add(basis_inv[file_id].copy()) else: # omit this from committed inventory pass continue if not work_tree.has_id(file_id): if verbose: print('deleted %s%s' % (path, kind_marker(entry.kind))) any_changes = True mutter(" file is missing, removing from inventory") missing_ids.append(file_id) continue # this is present in the new inventory; may be new, modified or # unchanged. old_ie = basis_inv.has_id(file_id) and basis_inv[file_id] entry = entry.copy() inv.add(entry) if old_ie: old_kind = old_ie.kind if old_kind != entry.kind: raise BzrError("entry %r changed kind from %r to %r" % (file_id, old_kind, entry.kind)) if entry.kind == 'directory': if not isdir(p): raise BzrError("%s is entered as directory but not a directory" % quotefn(p)) elif entry.kind == 'file': if not isfile(p): raise BzrError("%s is entered as file but is not a file" % quotefn(p)) new_sha1 = work_tree.get_file_sha1(file_id) if (old_ie and old_ie.text_sha1 == new_sha1): ## assert content == basis.get_file(file_id).read() entry.text_id = old_ie.text_id entry.text_sha1 = new_sha1 entry.text_size = old_ie.text_size mutter(' unchanged from previous text_id {%s}' % entry.text_id) else: content = file(p, 'rb').read() # calculate the sha again, just in case the file contents # changed since we updated the cache entry.text_sha1 = sha_string(content) entry.text_size = len(content) entry.text_id = gen_file_id(entry.name) branch.text_store.add(content, entry.text_id) mutter(' stored with text_id {%s}' % entry.text_id) if verbose: marked = path + kind_marker(entry.kind) if not old_ie: print 'added', marked any_changes = True elif old_ie == entry: pass # unchanged elif (old_ie.name == entry.name and old_ie.parent_id == entry.parent_id): print 'modified', marked any_changes = True else: print 'renamed', marked any_changes = True return missing_ids, inv, any_changes M 644 inline bzrlib/errors.py data 2019 #! /usr/bin/env python # -*- coding: UTF-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __copyright__ = "Copyright (C) 2005 Canonical Ltd." __author__ = "Martin Pool " ###################################################################### # exceptions class BzrError(StandardError): pass class BzrCheckError(BzrError): pass class BzrCommandError(BzrError): # Error from malformed user command pass class NotBranchError(BzrError): """Specified path is not in a branch""" pass class NotVersionedError(BzrError): """Specified object is not versioned.""" class BadFileKindError(BzrError): """Specified file is of a kind that cannot be added. (For example a symlink or device file.)""" pass class ForbiddenFileError(BzrError): """Cannot operate on a file because it is a control file.""" pass class LockError(Exception): """All exceptions from the lock/unlock functions should be from this exception class. They will be translated as necessary. The original exception is available as e.original_error """ def __init__(self, e=None): self.original_error = e if e: Exception.__init__(self, e) else: Exception.__init__(self) class PointlessCommit(Exception): """Commit failed because nothing was changed.""" M 644 inline bzrlib/selftest/blackbox.py data 11856 # Copyright (C) 2005 by Canonical Ltd # -*- coding: utf-8 -*- # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Black-box tests for bzr. These check that it behaves properly when it's invoked through the regular command-line interface. This always reinvokes bzr through a new Python interpreter, which is a bit inefficient but arguably tests in a way more representative of how it's normally invoked. """ # this code was previously in testbzr from unittest import TestCase from bzrlib.selftest import TestBase, InTempDir class TestVersion(TestBase): def runTest(self): # output is intentionally passed through to stdout so that we # can see the version being tested self.runcmd(['bzr', 'version']) class HelpCommands(TestBase): def runTest(self): self.runcmd('bzr --help') self.runcmd('bzr help') self.runcmd('bzr help commands') self.runcmd('bzr help help') self.runcmd('bzr commit -h') class InitBranch(InTempDir): def runTest(self): import os self.runcmd(['bzr', 'init']) class UserIdentity(InTempDir): def runTest(self): # this should always identify something, if only "john@localhost" self.runcmd("bzr whoami") self.runcmd("bzr whoami --email") self.assertEquals(self.backtick("bzr whoami --email").count('@'), 1) class InvalidCommands(InTempDir): def runTest(self): self.runcmd("bzr pants", retcode=1) self.runcmd("bzr --pants off", retcode=1) self.runcmd("bzr diff --message foo", retcode=1) class EmptyCommit(InTempDir): def runTest(self): self.runcmd("bzr init") self.build_tree(['hello.txt']) self.runcmd("bzr commit -m empty", retcode=1) self.runcmd("bzr add hello.txt") self.runcmd("bzr commit -m added") class OldTests(InTempDir): # old tests moved from ./testbzr def runTest(self): from os import chdir, mkdir from os.path import exists import os runcmd = self.runcmd backtick = self.backtick progress = self.log progress("basic branch creation") runcmd(['mkdir', 'branch1']) chdir('branch1') runcmd('bzr init') self.assertEquals(backtick('bzr root').rstrip(), os.path.join(self.test_dir, 'branch1')) progress("status of new file") f = file('test.txt', 'wt') f.write('hello world!\n') f.close() out = backtick("bzr unknowns") self.assertEquals(out, 'test.txt\n') out = backtick("bzr status") assert out == 'unknown:\n test.txt\n' out = backtick("bzr status --all") assert out == "unknown:\n test.txt\n" out = backtick("bzr status test.txt --all") assert out == "unknown:\n test.txt\n" f = file('test2.txt', 'wt') f.write('goodbye cruel world...\n') f.close() out = backtick("bzr status test.txt") assert out == "unknown:\n test.txt\n" out = backtick("bzr status") assert out == ("unknown:\n" " test.txt\n" " test2.txt\n") os.unlink('test2.txt') progress("command aliases") out = backtick("bzr st --all") assert out == ("unknown:\n" " test.txt\n") out = backtick("bzr stat") assert out == ("unknown:\n" " test.txt\n") progress("command help") runcmd("bzr help st") runcmd("bzr help") runcmd("bzr help commands") runcmd("bzr help slartibartfast", 1) out = backtick("bzr help ci") out.index('aliases: ') progress("can't rename unversioned file") runcmd("bzr rename test.txt new-test.txt", 1) progress("adding a file") runcmd("bzr add test.txt") assert backtick("bzr unknowns") == '' assert backtick("bzr status --all") == ("added:\n" " test.txt\n") progress("rename newly-added file") runcmd("bzr rename test.txt hello.txt") assert os.path.exists("hello.txt") assert not os.path.exists("test.txt") assert backtick("bzr revno") == '0\n' progress("add first revision") runcmd(["bzr", "commit", "-m", 'add first revision']) progress("more complex renames") os.mkdir("sub1") runcmd("bzr rename hello.txt sub1", 1) runcmd("bzr rename hello.txt sub1/hello.txt", 1) runcmd("bzr move hello.txt sub1", 1) runcmd("bzr add sub1") runcmd("bzr rename sub1 sub2") runcmd("bzr move hello.txt sub2") assert backtick("bzr relpath sub2/hello.txt") == os.path.join("sub2", "hello.txt\n") assert exists("sub2") assert exists("sub2/hello.txt") assert not exists("sub1") assert not exists("hello.txt") runcmd(['bzr', 'commit', '-m', 'commit with some things moved to subdirs']) mkdir("sub1") runcmd('bzr add sub1') runcmd('bzr move sub2/hello.txt sub1') assert not exists('sub2/hello.txt') assert exists('sub1/hello.txt') runcmd('bzr move sub2 sub1') assert not exists('sub2') assert exists('sub1/sub2') runcmd(['bzr', 'commit', '-m', 'rename nested subdirectories']) chdir('sub1/sub2') self.assertEquals(backtick('bzr root')[:-1], os.path.join(self.test_dir, 'branch1')) runcmd('bzr move ../hello.txt .') assert exists('./hello.txt') assert backtick('bzr relpath hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') assert backtick('bzr relpath ../../sub1/sub2/hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') runcmd(['bzr', 'commit', '-m', 'move to parent directory']) chdir('..') assert backtick('bzr relpath sub2/hello.txt') == os.path.join('sub1', 'sub2', 'hello.txt\n') runcmd('bzr move sub2/hello.txt .') assert exists('hello.txt') f = file('hello.txt', 'wt') f.write('some nice new content\n') f.close() f = file('msg.tmp', 'wt') f.write('this is my new commit\n') f.close() runcmd('bzr commit -F msg.tmp') assert backtick('bzr revno') == '5\n' runcmd('bzr export -r 5 export-5.tmp') runcmd('bzr export export.tmp') runcmd('bzr log') runcmd('bzr log -v') progress("file with spaces in name") mkdir('sub directory') file('sub directory/file with spaces ', 'wt').write('see how this works\n') runcmd('bzr add .') runcmd('bzr diff') runcmd('bzr commit -m add-spaces') runcmd('bzr check') runcmd('bzr log') runcmd('bzr log --forward') runcmd('bzr info') chdir('..') chdir('..') progress('branch') # Can't create a branch if it already exists runcmd('bzr branch branch1', retcode=1) # Can't create a branch if its parent doesn't exist runcmd('bzr branch /unlikely/to/exist', retcode=1) runcmd('bzr branch branch1 branch2') progress("pull") chdir('branch1') runcmd('bzr pull', retcode=1) runcmd('bzr pull ../branch2') chdir('.bzr') runcmd('bzr pull') runcmd('bzr commit --unchanged -m empty') runcmd('bzr pull') chdir('../../branch2') runcmd('bzr pull') runcmd('bzr commit --unchanged -m empty') chdir('../branch1') runcmd('bzr commit --unchanged -m empty') runcmd('bzr pull', retcode=1) chdir ('..') progress('status after remove') mkdir('status-after-remove') # see mail from William Dodé, 2005-05-25 # $ bzr init; touch a; bzr add a; bzr commit -m "add a" # * looking for changes... # added a # * commited r1 # $ bzr remove a # $ bzr status # bzr: local variable 'kind' referenced before assignment # at /vrac/python/bazaar-ng/bzrlib/diff.py:286 in compare_trees() # see ~/.bzr.log for debug information chdir('status-after-remove') runcmd('bzr init') file('a', 'w').write('foo') runcmd('bzr add a') runcmd(['bzr', 'commit', '-m', 'add a']) runcmd('bzr remove a') runcmd('bzr status') chdir('..') progress('ignore patterns') mkdir('ignorebranch') chdir('ignorebranch') runcmd('bzr init') assert backtick('bzr unknowns') == '' file('foo.tmp', 'wt').write('tmp files are ignored') assert backtick('bzr unknowns') == '' file('foo.c', 'wt').write('int main() {}') assert backtick('bzr unknowns') == 'foo.c\n' runcmd('bzr add foo.c') assert backtick('bzr unknowns') == '' # 'ignore' works when creating the .bzignore file file('foo.blah', 'wt').write('blah') assert backtick('bzr unknowns') == 'foo.blah\n' runcmd('bzr ignore *.blah') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\n' # 'ignore' works when then .bzrignore file already exists file('garh', 'wt').write('garh') assert backtick('bzr unknowns') == 'garh\n' runcmd('bzr ignore garh') assert backtick('bzr unknowns') == '' assert file('.bzrignore', 'rb').read() == '*.blah\ngarh\n' chdir('..') progress("recursive and non-recursive add") mkdir('no-recurse') chdir('no-recurse') runcmd('bzr init') mkdir('foo') fp = os.path.join('foo', 'test.txt') f = file(fp, 'w') f.write('hello!\n') f.close() runcmd('bzr add --no-recurse foo') runcmd('bzr file-id foo') runcmd('bzr file-id ' + fp, 1) # not versioned yet runcmd('bzr commit -m add-dir-only') runcmd('bzr file-id ' + fp, 1) # still not versioned runcmd('bzr add foo') runcmd('bzr file-id ' + fp) runcmd('bzr commit -m add-sub-file') chdir('..') class RevertCommand(InTempDir): def runTest(self): self.runcmd('bzr init') file('hello', 'wt').write('foo') self.runcmd('bzr add hello') self.runcmd('bzr commit -m setup hello') file('hello', 'wt').write('bar') self.runcmd('bzr revert hello') self.check_file_contents('hello', 'foo') # lists all tests from this module in the best order to run them. we # do it this way rather than just discovering them all because it # allows us to test more basic functions first where failures will be # easiest to understand. TEST_CLASSES = [TestVersion, InitBranch, HelpCommands, UserIdentity, InvalidCommands, RevertCommand, OldTests, EmptyCommit, ] M 644 inline bzrlib/selftest/whitebox.py data 7689 #! /usr/bin/python import os import unittest from bzrlib.selftest import InTempDir, TestBase from bzrlib.branch import ScratchBranch, Branch from bzrlib.errors import NotBranchError, NotVersionedError class Unknowns(InTempDir): def runTest(self): b = Branch('.', init=True) self.build_tree(['hello.txt', 'hello.txt~']) self.assertEquals(list(b.unknowns()), ['hello.txt']) class NoChanges(InTempDir): def runTest(self): from bzrlib.errors import PointlessCommit b = Branch('.', init=True) self.build_tree(['hello.txt']) self.assertRaises(PointlessCommit, b.commit, 'commit without adding', allow_pointless=False) b.commit('commit pointless tree', allow_pointless=True) b.add('hello.txt') b.commit('commit first added file', allow_pointless=False) self.assertRaises(PointlessCommit, b.commit, 'commit after adding file', allow_pointless=False) b.commit('commit pointless revision with one file', allow_pointless=True) b.add_pending_merge('mbp@892739123-2005-123123') b.commit('commit new merge with no text changes', allow_pointless=False) class ValidateRevisionId(TestBase): def runTest(self): from bzrlib.revision import validate_revision_id validate_revision_id('mbp@sourcefrog.net-20050311061123-96a255005c7c9dbe') self.assertRaises(ValueError, validate_revision_id, ' asdkjas') self.assertRaises(ValueError, validate_revision_id, 'mbp@sourcefrog.net-20050311061123-96a255005c7c9dbe\n') self.assertRaises(ValueError, validate_revision_id, ' mbp@sourcefrog.net-20050311061123-96a255005c7c9dbe') self.assertRaises(ValueError, validate_revision_id, 'Martin Pool -20050311061123-96a255005c7c9dbe') class PendingMerges(InTempDir): """Tracking pending-merged revisions.""" def runTest(self): b = Branch('.', init=True) self.assertEquals(b.pending_merges(), []) b.add_pending_merge('foo@azkhazan-123123-abcabc') self.assertEquals(b.pending_merges(), ['foo@azkhazan-123123-abcabc']) b.add_pending_merge('foo@azkhazan-123123-abcabc') self.assertEquals(b.pending_merges(), ['foo@azkhazan-123123-abcabc']) b.add_pending_merge('wibble@fofof--20050401--1928390812') self.assertEquals(b.pending_merges(), ['foo@azkhazan-123123-abcabc', 'wibble@fofof--20050401--1928390812']) b.commit("commit from base with two merges") rev = b.get_revision(b.revision_history()[0]) self.assertEquals(len(rev.parents), 2) self.assertEquals(rev.parents[0].revision_id, 'foo@azkhazan-123123-abcabc') self.assertEquals(rev.parents[1].revision_id, 'wibble@fofof--20050401--1928390812') # list should be cleared when we do a commit self.assertEquals(b.pending_merges(), []) class Revert(InTempDir): """Test selected-file revert""" def runTest(self): b = Branch('.', init=True) self.build_tree(['hello.txt']) file('hello.txt', 'w').write('initial hello') self.assertRaises(NotVersionedError, b.revert, ['hello.txt']) b.add(['hello.txt']) b.commit('create initial hello.txt') self.check_file_contents('hello.txt', 'initial hello') file('hello.txt', 'w').write('new hello') self.check_file_contents('hello.txt', 'new hello') # revert file modified since last revision b.revert(['hello.txt']) self.check_file_contents('hello.txt', 'initial hello') self.check_file_contents('hello.txt~', 'new hello') # reverting again clobbers the backup b.revert(['hello.txt']) self.check_file_contents('hello.txt', 'initial hello') self.check_file_contents('hello.txt~', 'initial hello') class RenameDirs(InTempDir): """Test renaming directories and the files within them.""" def runTest(self): b = Branch('.', init=True) self.build_tree(['dir/', 'dir/sub/', 'dir/sub/file']) b.add(['dir', 'dir/sub', 'dir/sub/file']) b.commit('create initial state') # TODO: lift out to a test helper that checks the shape of # an inventory revid = b.revision_history()[0] self.log('first revision_id is {%s}' % revid) inv = b.get_revision_inventory(revid) self.log('contents of inventory: %r' % inv.entries()) self.check_inventory_shape(inv, ['dir', 'dir/sub', 'dir/sub/file']) b.rename_one('dir', 'newdir') self.check_inventory_shape(b.inventory, ['newdir', 'newdir/sub', 'newdir/sub/file']) b.rename_one('newdir/sub', 'newdir/newsub') self.check_inventory_shape(b.inventory, ['newdir', 'newdir/newsub', 'newdir/newsub/file']) class BranchPathTestCase(TestBase): """test for branch path lookups Branch.relpath and bzrlib.branch._relpath do a simple but subtle job: given a path (either relative to cwd or absolute), work out if it is inside a branch and return the path relative to the base. """ def runTest(self): from bzrlib.branch import _relpath import tempfile, shutil savedir = os.getcwdu() dtmp = tempfile.mkdtemp() def rp(p): return _relpath(dtmp, p) try: # check paths inside dtmp while standing outside it self.assertEqual(rp(os.path.join(dtmp, 'foo')), 'foo') # root = nothing self.assertEqual(rp(dtmp), '') self.assertRaises(NotBranchError, rp, '/etc') # now some near-miss operations -- note that # os.path.commonprefix gets these wrong! self.assertRaises(NotBranchError, rp, dtmp.rstrip('\\/') + '2') self.assertRaises(NotBranchError, rp, dtmp.rstrip('\\/') + '2/foo') # now operations based on relpath of files in current # directory, or nearby os.chdir(dtmp) self.assertEqual(rp('foo/bar/quux'), 'foo/bar/quux') self.assertEqual(rp('foo'), 'foo') self.assertEqual(rp('./foo'), 'foo') self.assertEqual(rp(os.path.abspath('foo')), 'foo') self.assertRaises(NotBranchError, rp, '../foo') finally: os.chdir(savedir) shutil.rmtree(dtmp) TEST_CLASSES = [Unknowns, ValidateRevisionId, PendingMerges, Revert, RenameDirs, BranchPathTestCase, ] commit refs/heads/master mark :982 committer Martin Pool 1121053373 +1000 data 3 doc from :981 M 644 inline bzrlib/commands.py data 53799 # Copyright (C) 2004, 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os import bzrlib from bzrlib.trace import mutter, note, log_error from bzrlib.errors import BzrError, BzrCheckError, BzrCommandError from bzrlib.branch import find_branch from bzrlib import BZRDIR plugin_cmds = {} def register_command(cmd): "Utility function to help register a command" global plugin_cmds k = cmd.__name__ if k.startswith("cmd_"): k_unsquished = _unsquish_command_name(k) else: k_unsquished = k if not plugin_cmds.has_key(k_unsquished): plugin_cmds[k_unsquished] = cmd else: log_error('Two plugins defined the same command: %r' % k) log_error('Not loading the one in %r' % sys.modules[cmd.__module__]) def _squish_command_name(cmd): return 'cmd_' + cmd.replace('-', '_') def _unsquish_command_name(cmd): assert cmd.startswith("cmd_") return cmd[4:].replace('_','-') def _parse_revision_str(revstr): """This handles a revision string -> revno. There are several possibilities: '234' -> 234 '234:345' -> [234, 345] ':234' -> [None, 234] '234:' -> [234, None] In the future we will also support: 'uuid:blah-blah-blah' -> ? 'hash:blahblahblah' -> ? potentially: 'tag:mytag' -> ? """ if revstr.find(':') != -1: revs = revstr.split(':') if len(revs) > 2: raise ValueError('More than 2 pieces not supported for --revision: %r' % revstr) if not revs[0]: revs[0] = None else: revs[0] = int(revs[0]) if not revs[1]: revs[1] = None else: revs[1] = int(revs[1]) else: revs = int(revstr) return revs def _get_cmd_dict(plugins_override=True): d = {} for k, v in globals().iteritems(): if k.startswith("cmd_"): d[_unsquish_command_name(k)] = v # If we didn't load plugins, the plugin_cmds dict will be empty if plugins_override: d.update(plugin_cmds) else: d2 = plugin_cmds.copy() d2.update(d) d = d2 return d def get_all_cmds(plugins_override=True): """Return canonical name and class for all registered commands.""" for k, v in _get_cmd_dict(plugins_override=plugins_override).iteritems(): yield k,v def get_cmd_class(cmd, plugins_override=True): """Return the canonical name and command class for a command. """ cmd = str(cmd) # not unicode # first look up this command under the specified name cmds = _get_cmd_dict(plugins_override=plugins_override) try: return cmd, cmds[cmd] except KeyError: pass # look for any command which claims this as an alias for cmdname, cmdclass in cmds.iteritems(): if cmd in cmdclass.aliases: return cmdname, cmdclass cmdclass = ExternalCommand.find_command(cmd) if cmdclass: return cmd, cmdclass raise BzrCommandError("unknown command %r" % cmd) class Command(object): """Base class for commands. The docstring for an actual command should give a single-line summary, then a complete description of the command. A grammar description will be inserted. takes_args List of argument forms, marked with whether they are optional, repeated, etc. takes_options List of options that may be given for this command. hidden If true, this command isn't advertised. """ aliases = [] takes_args = [] takes_options = [] hidden = False def __init__(self, options, arguments): """Construct and run the command. Sets self.status to the return value of run().""" assert isinstance(options, dict) assert isinstance(arguments, dict) cmdargs = options.copy() cmdargs.update(arguments) assert self.__doc__ != Command.__doc__, \ ("No help message set for %r" % self) self.status = self.run(**cmdargs) def run(self): """Override this in sub-classes. This is invoked with the options and arguments bound to keyword parameters. Return 0 or None if the command was successful, or a shell error code if not. """ return 0 class ExternalCommand(Command): """Class to wrap external commands. We cheat a little here, when get_cmd_class() calls us we actually give it back an object we construct that has the appropriate path, help, options etc for the specified command. When run_bzr() tries to instantiate that 'class' it gets caught by the __call__ method, which we override to call the Command.__init__ method. That then calls our run method which is pretty straight forward. The only wrinkle is that we have to map bzr's dictionary of options and arguments back into command line options and arguments for the script. """ def find_command(cls, cmd): import os.path bzrpath = os.environ.get('BZRPATH', '') for dir in bzrpath.split(os.pathsep): path = os.path.join(dir, cmd) if os.path.isfile(path): return ExternalCommand(path) return None find_command = classmethod(find_command) def __init__(self, path): self.path = path pipe = os.popen('%s --bzr-usage' % path, 'r') self.takes_options = pipe.readline().split() for opt in self.takes_options: if not opt in OPTIONS: raise BzrError("Unknown option '%s' returned by external command %s" % (opt, path)) # TODO: Is there any way to check takes_args is valid here? self.takes_args = pipe.readline().split() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-usage'" % path) pipe = os.popen('%s --bzr-help' % path, 'r') self.__doc__ = pipe.read() if pipe.close() is not None: raise BzrError("Failed funning '%s --bzr-help'" % path) def __call__(self, options, arguments): Command.__init__(self, options, arguments) return self def run(self, **kargs): opts = [] args = [] keys = kargs.keys() keys.sort() for name in keys: optname = name.replace('_','-') value = kargs[name] if OPTIONS.has_key(optname): # it's an option opts.append('--%s' % optname) if value is not None and value is not True: opts.append(str(value)) else: # it's an arg, or arg list if type(value) is not list: value = [value] for v in value: if v is not None: args.append(str(v)) self.status = os.spawnv(os.P_WAIT, self.path, [self.path] + opts + args) return self.status class cmd_status(Command): """Display status summary. This reports on versioned and unknown files, reporting them grouped by state. Possible states are: added Versioned in the working copy but not in the previous revision. removed Versioned in the previous revision but removed or deleted in the working copy. renamed Path of this file changed from the previous revision; the text may also have changed. This includes files whose parent directory was renamed. modified Text has changed since the previous revision. unchanged Nothing about this file has changed since the previous revision. Only shown with --all. unknown Not versioned and not matching an ignore pattern. To see ignored files use 'bzr ignored'. For details in the changes to file texts, use 'bzr diff'. If no arguments are specified, the status of the entire working directory is shown. Otherwise, only the status of the specified files or directories is reported. If a directory is given, status is reported for everything inside that directory. """ takes_args = ['file*'] takes_options = ['all', 'show-ids'] aliases = ['st', 'stat'] def run(self, all=False, show_ids=False, file_list=None): if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(x) for x in file_list] # special case: only one path was given and it's the root # of the branch if file_list == ['']: file_list = None else: b = find_branch('.') import status status.show_status(b, show_unchanged=all, show_ids=show_ids, specific_files=file_list) class cmd_cat_revision(Command): """Write out metadata for a revision.""" hidden = True takes_args = ['revision_id'] def run(self, revision_id): from bzrlib.xml import pack_xml pack_xml(find_branch('.').get_revision(revision_id), sys.stdout) class cmd_revno(Command): """Show current revision number. This is equal to the number of revisions on this branch.""" def run(self): print find_branch('.').revno() class cmd_add(Command): """Add specified files or directories. In non-recursive mode, all the named items are added, regardless of whether they were previously ignored. A warning is given if any of the named files are already versioned. In recursive mode (the default), files are treated the same way but the behaviour for directories is different. Directories that are already versioned do not give a warning. All directories, whether already versioned or not, are searched for files or subdirectories that are neither versioned or ignored, and these are added. This search proceeds recursively into versioned directories. Therefore simply saying 'bzr add .' will version all files that are currently unknown. TODO: Perhaps adding a file whose directly is not versioned should recursively add that parent, rather than giving an error? """ takes_args = ['file+'] takes_options = ['verbose', 'no-recurse'] def run(self, file_list, verbose=False, no_recurse=False): from bzrlib.add import smart_add smart_add(file_list, verbose, not no_recurse) class cmd_mkdir(Command): """Create a new versioned directory. This is equivalent to creating the directory and then adding it. """ takes_args = ['dir+'] def run(self, dir_list): b = None for d in dir_list: os.mkdir(d) if not b: b = find_branch(d) b.add([d], verbose=True) class cmd_relpath(Command): """Show path of a file relative to root""" takes_args = ['filename'] hidden = True def run(self, filename): print find_branch(filename).relpath(filename) class cmd_inventory(Command): """Show inventory of the current working copy or a revision.""" takes_options = ['revision', 'show-ids'] def run(self, revision=None, show_ids=False): b = find_branch('.') if revision == None: inv = b.read_working_inventory() else: inv = b.get_revision_inventory(b.lookup_revision(revision)) for path, entry in inv.entries(): if show_ids: print '%-50s %s' % (path, entry.file_id) else: print path class cmd_move(Command): """Move files to a different directory. examples: bzr move *.txt doc The destination must be a versioned directory in the same branch. """ takes_args = ['source$', 'dest'] def run(self, source_list, dest): b = find_branch('.') b.move([b.relpath(s) for s in source_list], b.relpath(dest)) class cmd_rename(Command): """Change the name of an entry. examples: bzr rename frob.c frobber.c bzr rename src/frob.c lib/frob.c It is an error if the destination name exists. See also the 'move' command, which moves files into a different directory without changing their name. TODO: Some way to rename multiple files without invoking bzr for each one?""" takes_args = ['from_name', 'to_name'] def run(self, from_name, to_name): b = find_branch('.') b.rename_one(b.relpath(from_name), b.relpath(to_name)) class cmd_pull(Command): """Pull any changes from another branch into the current one. If the location is omitted, the last-used location will be used. Both the revision history and the working directory will be updated. This command only works on branches that have not diverged. Branches are considered diverged if both branches have had commits without first pulling from the other. If branches have diverged, you can use 'bzr merge' to pull the text changes from one into the other. """ takes_args = ['location?'] def run(self, location=None): from bzrlib.merge import merge import tempfile from shutil import rmtree import errno br_to = find_branch('.') stored_loc = None try: stored_loc = br_to.controlfile("x-pull", "rb").read().rstrip('\n') except IOError, e: if e.errno != errno.ENOENT: raise if location is None: if stored_loc is None: raise BzrCommandError("No pull location known or specified.") else: print "Using last location: %s" % stored_loc location = stored_loc cache_root = tempfile.mkdtemp() from bzrlib.branch import DivergedBranches br_from = find_branch(location) location = pull_loc(br_from) old_revno = br_to.revno() try: from branch import find_cached_branch, DivergedBranches br_from = find_cached_branch(location, cache_root) location = pull_loc(br_from) old_revno = br_to.revno() try: br_to.update_revisions(br_from) except DivergedBranches: raise BzrCommandError("These branches have diverged." " Try merge.") merge(('.', -1), ('.', old_revno), check_clean=False) if location != stored_loc: br_to.controlfile("x-pull", "wb").write(location + "\n") finally: rmtree(cache_root) class cmd_branch(Command): """Create a new copy of a branch. If the TO_LOCATION is omitted, the last component of the FROM_LOCATION will be used. In other words, "branch ../foo/bar" will attempt to create ./bar. To retrieve the branch as of a particular revision, supply the --revision parameter, as in "branch foo/bar -r 5". """ takes_args = ['from_location', 'to_location?'] takes_options = ['revision'] def run(self, from_location, to_location=None, revision=None): import errno from bzrlib.merge import merge from bzrlib.branch import DivergedBranches, NoSuchRevision, \ find_cached_branch, Branch from shutil import rmtree from meta_store import CachedStore import tempfile cache_root = tempfile.mkdtemp() try: try: br_from = find_cached_branch(from_location, cache_root) except OSError, e: if e.errno == errno.ENOENT: raise BzrCommandError('Source location "%s" does not' ' exist.' % to_location) else: raise if to_location is None: to_location = os.path.basename(from_location.rstrip("/\\")) try: os.mkdir(to_location) except OSError, e: if e.errno == errno.EEXIST: raise BzrCommandError('Target directory "%s" already' ' exists.' % to_location) if e.errno == errno.ENOENT: raise BzrCommandError('Parent of "%s" does not exist.' % to_location) else: raise br_to = Branch(to_location, init=True) try: br_to.update_revisions(br_from, stop_revision=revision) except NoSuchRevision: rmtree(to_location) msg = "The branch %s has no revision %d." % (from_location, revision) raise BzrCommandError(msg) merge((to_location, -1), (to_location, 0), this_dir=to_location, check_clean=False, ignore_zero=True) from_location = pull_loc(br_from) br_to.controlfile("x-pull", "wb").write(from_location + "\n") finally: rmtree(cache_root) def pull_loc(branch): # TODO: Should perhaps just make attribute be 'base' in # RemoteBranch and Branch? if hasattr(branch, "baseurl"): return branch.baseurl else: return branch.base class cmd_renames(Command): """Show list of renamed files. TODO: Option to show renames between two historical versions. TODO: Only show renames under dir, rather than in the whole branch. """ takes_args = ['dir?'] def run(self, dir='.'): b = find_branch(dir) old_inv = b.basis_tree().inventory new_inv = b.read_working_inventory() renames = list(bzrlib.tree.find_renames(old_inv, new_inv)) renames.sort() for old_name, new_name in renames: print "%s => %s" % (old_name, new_name) class cmd_info(Command): """Show statistical information about a branch.""" takes_args = ['branch?'] def run(self, branch=None): import info b = find_branch(branch) info.show_info(b) class cmd_remove(Command): """Make a file unversioned. This makes bzr stop tracking changes to a versioned file. It does not delete the working copy. """ takes_args = ['file+'] takes_options = ['verbose'] def run(self, file_list, verbose=False): b = find_branch(file_list[0]) b.remove([b.relpath(f) for f in file_list], verbose=verbose) class cmd_file_id(Command): """Print file_id of a particular file or directory. The file_id is assigned when the file is first added and remains the same through all revisions where the file exists, even when it is moved or renamed. """ hidden = True takes_args = ['filename'] def run(self, filename): b = find_branch(filename) i = b.inventory.path2id(b.relpath(filename)) if i == None: raise BzrError("%r is not a versioned file" % filename) else: print i class cmd_file_path(Command): """Print path of file_ids to a file or directory. This prints one line for each directory down to the target, starting at the branch root.""" hidden = True takes_args = ['filename'] def run(self, filename): b = find_branch(filename) inv = b.inventory fid = inv.path2id(b.relpath(filename)) if fid == None: raise BzrError("%r is not a versioned file" % filename) for fip in inv.get_idpath(fid): print fip class cmd_revision_history(Command): """Display list of revision ids on this branch.""" hidden = True def run(self): for patchid in find_branch('.').revision_history(): print patchid class cmd_directories(Command): """Display list of versioned directories in this branch.""" def run(self): for name, ie in find_branch('.').read_working_inventory().directories(): if name == '': print '.' else: print name class cmd_init(Command): """Make a directory into a versioned branch. Use this to create an empty branch, or before importing an existing project. Recipe for importing a tree of files: cd ~/project bzr init bzr add -v . bzr status bzr commit -m 'imported project' """ def run(self): from bzrlib.branch import Branch Branch('.', init=True) class cmd_diff(Command): """Show differences in working tree. If files are listed, only the changes in those files are listed. Otherwise, all changes for the tree are listed. TODO: Given two revision arguments, show the difference between them. TODO: Allow diff across branches. TODO: Option to use external diff command; could be GNU diff, wdiff, or a graphical diff. TODO: Python difflib is not exactly the same as unidiff; should either fix it up or prefer to use an external diff. TODO: If a directory is given, diff everything under that. TODO: Selected-file diff is inefficient and doesn't show you deleted files. TODO: This probably handles non-Unix newlines poorly. """ takes_args = ['file*'] takes_options = ['revision', 'diff-options'] aliases = ['di', 'dif'] def run(self, revision=None, file_list=None, diff_options=None): from bzrlib.diff import show_diff if file_list: b = find_branch(file_list[0]) file_list = [b.relpath(f) for f in file_list] if file_list == ['']: # just pointing to top-of-tree file_list = None else: b = find_branch('.') show_diff(b, revision, specific_files=file_list, external_diff_options=diff_options) class cmd_deleted(Command): """List files deleted in the working tree. TODO: Show files deleted since a previous revision, or between two revisions. """ def run(self, show_ids=False): b = find_branch('.') old = b.basis_tree() new = b.working_tree() ## TODO: Much more efficient way to do this: read in new ## directories with readdir, rather than stating each one. Same ## level of effort but possibly much less IO. (Or possibly not, ## if the directories are very large...) for path, ie in old.inventory.iter_entries(): if not new.has_id(ie.file_id): if show_ids: print '%-50s %s' % (path, ie.file_id) else: print path class cmd_modified(Command): """List files modified in working tree.""" hidden = True def run(self): from bzrlib.diff import compare_trees b = find_branch('.') td = compare_trees(b.basis_tree(), b.working_tree()) for path, id, kind in td.modified: print path class cmd_added(Command): """List files added in working tree.""" hidden = True def run(self): b = find_branch('.') wt = b.working_tree() basis_inv = b.basis_tree().inventory inv = wt.inventory for file_id in inv: if file_id in basis_inv: continue path = inv.id2path(file_id) if not os.access(b.abspath(path), os.F_OK): continue print path class cmd_root(Command): """Show the tree root directory. The root is the nearest enclosing directory with a .bzr control directory.""" takes_args = ['filename?'] def run(self, filename=None): """Print the branch root.""" b = find_branch(filename) print getattr(b, 'base', None) or getattr(b, 'baseurl') class cmd_log(Command): """Show log of this branch. To request a range of logs, you can use the command -r begin:end -r revision requests a specific revision, -r :end or -r begin: are also valid. TODO: Make --revision support uuid: and hash: [future tag:] notation. """ takes_args = ['filename?'] takes_options = ['forward', 'timezone', 'verbose', 'show-ids', 'revision','long'] def run(self, filename=None, timezone='original', verbose=False, show_ids=False, forward=False, revision=None, long=False): from bzrlib.branch import find_branch from bzrlib.log import log_formatter, show_log import codecs direction = (forward and 'forward') or 'reverse' if filename: b = find_branch(filename) fp = b.relpath(filename) if fp: file_id = b.read_working_inventory().path2id(fp) else: file_id = None # points to branch root else: b = find_branch('.') file_id = None if revision == None: revision = [None, None] elif isinstance(revision, int): revision = [revision, revision] else: # pair of revisions? pass assert len(revision) == 2 mutter('encoding log as %r' % bzrlib.user_encoding) # use 'replace' so that we don't abort if trying to write out # in e.g. the default C locale. outf = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace') if long: log_format = 'long' else: log_format = 'short' lf = log_formatter(log_format, show_ids=show_ids, to_file=outf, show_timezone=timezone) show_log(b, lf, file_id, verbose=verbose, direction=direction, start_revision=revision[0], end_revision=revision[1]) class cmd_touching_revisions(Command): """Return revision-ids which affected a particular file. A more user-friendly interface is "bzr log FILE".""" hidden = True takes_args = ["filename"] def run(self, filename): b = find_branch(filename) inv = b.read_working_inventory() file_id = inv.path2id(b.relpath(filename)) for revno, revision_id, what in bzrlib.log.find_touching_revisions(b, file_id): print "%6d %s" % (revno, what) class cmd_ls(Command): """List files in a tree. TODO: Take a revision or remote path and list that tree instead. """ hidden = True def run(self, revision=None, verbose=False): b = find_branch('.') if revision == None: tree = b.working_tree() else: tree = b.revision_tree(b.lookup_revision(revision)) for fp, fc, kind, fid in tree.list_files(): if verbose: if kind == 'directory': kindch = '/' elif kind == 'file': kindch = '' else: kindch = '???' print '%-8s %s%s' % (fc, fp, kindch) else: print fp class cmd_unknowns(Command): """List unknown files.""" def run(self): from bzrlib.osutils import quotefn for f in find_branch('.').unknowns(): print quotefn(f) class cmd_ignore(Command): """Ignore a command or pattern. To remove patterns from the ignore list, edit the .bzrignore file. If the pattern contains a slash, it is compared to the whole path from the branch root. Otherwise, it is comapred to only the last component of the path. Ignore patterns are case-insensitive on case-insensitive systems. Note: wildcards must be quoted from the shell on Unix. examples: bzr ignore ./Makefile bzr ignore '*.class' """ takes_args = ['name_pattern'] def run(self, name_pattern): from bzrlib.atomicfile import AtomicFile import os.path b = find_branch('.') ifn = b.abspath('.bzrignore') if os.path.exists(ifn): f = open(ifn, 'rt') try: igns = f.read().decode('utf-8') finally: f.close() else: igns = '' # TODO: If the file already uses crlf-style termination, maybe # we should use that for the newly added lines? if igns and igns[-1] != '\n': igns += '\n' igns += name_pattern + '\n' try: f = AtomicFile(ifn, 'wt') f.write(igns.encode('utf-8')) f.commit() finally: f.close() inv = b.working_tree().inventory if inv.path2id('.bzrignore'): mutter('.bzrignore is already versioned') else: mutter('need to make new .bzrignore file versioned') b.add(['.bzrignore']) class cmd_ignored(Command): """List ignored files and the patterns that matched them. See also: bzr ignore""" def run(self): tree = find_branch('.').working_tree() for path, file_class, kind, file_id in tree.list_files(): if file_class != 'I': continue ## XXX: Slightly inefficient since this was already calculated pat = tree.is_ignored(path) print '%-50s %s' % (path, pat) class cmd_lookup_revision(Command): """Lookup the revision-id from a revision-number example: bzr lookup-revision 33 """ hidden = True takes_args = ['revno'] def run(self, revno): try: revno = int(revno) except ValueError: raise BzrCommandError("not a valid revision-number: %r" % revno) print find_branch('.').lookup_revision(revno) class cmd_export(Command): """Export past revision to destination directory. If no revision is specified this exports the last committed revision. Format may be an "exporter" name, such as tar, tgz, tbz2. If none is given, try to find the format with the extension. If no extension is found exports to a directory (equivalent to --format=dir). Root may be the top directory for tar, tgz and tbz2 formats. If none is given, the top directory will be the root name of the file.""" # TODO: list known exporters takes_args = ['dest'] takes_options = ['revision', 'format', 'root'] def run(self, dest, revision=None, format=None, root=None): import os.path b = find_branch('.') if revision == None: rh = b.revision_history()[-1] else: rh = b.lookup_revision(int(revision)) t = b.revision_tree(rh) root, ext = os.path.splitext(dest) if not format: if ext in (".tar",): format = "tar" elif ext in (".gz", ".tgz"): format = "tgz" elif ext in (".bz2", ".tbz2"): format = "tbz2" else: format = "dir" t.export(dest, format, root) class cmd_cat(Command): """Write a file's text from a previous revision.""" takes_options = ['revision'] takes_args = ['filename'] def run(self, filename, revision=None): if revision == None: raise BzrCommandError("bzr cat requires a revision number") b = find_branch('.') b.print_file(b.relpath(filename), int(revision)) class cmd_local_time_offset(Command): """Show the offset in seconds from GMT to local time.""" hidden = True def run(self): print bzrlib.osutils.local_time_offset() class cmd_commit(Command): """Commit changes into a new revision. If selected files are specified, only changes to those files are committed. If a directory is specified then its contents are also committed. A selected-file commit may fail in some cases where the committed tree would be invalid, such as trying to commit a file in a newly-added directory that is not itself committed. TODO: Run hooks on tree to-be-committed, and after commit. TODO: Strict commit that fails if there are unknown or deleted files. """ takes_args = ['selected*'] takes_options = ['message', 'file', 'verbose', 'unchanged'] aliases = ['ci', 'checkin'] def run(self, message=None, file=None, verbose=True, selected_list=None, unchanged=False): from bzrlib.errors import PointlessCommit from bzrlib.osutils import get_text_message ## Warning: shadows builtin file() if not message and not file: import cStringIO stdout = sys.stdout catcher = cStringIO.StringIO() sys.stdout = catcher cmd_status({"file_list":selected_list}, {}) info = catcher.getvalue() sys.stdout = stdout message = get_text_message(info) if message is None: raise BzrCommandError("please specify a commit message", ["use either --message or --file"]) elif message and file: raise BzrCommandError("please specify either --message or --file") if file: import codecs message = codecs.open(file, 'rt', bzrlib.user_encoding).read() b = find_branch('.') try: b.commit(message, verbose=verbose, specific_files=selected_list, allow_pointless=unchanged) except PointlessCommit: # FIXME: This should really happen before the file is read in; # perhaps prepare the commit; get the message; then actually commit raise BzrCommandError("no changes to commit", ["use --unchanged to commit anyhow"]) class cmd_check(Command): """Validate consistency of branch history. This command checks various invariants about the branch storage to detect data corruption or bzr bugs. If given the --update flag, it will update some optional fields to help ensure data consistency. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.check import check check(find_branch(dir)) class cmd_upgrade(Command): """Upgrade branch storage to current format. This should normally be used only after the check command tells you to run it. """ takes_args = ['dir?'] def run(self, dir='.'): from bzrlib.upgrade import upgrade upgrade(find_branch(dir)) class cmd_whoami(Command): """Show bzr user id.""" takes_options = ['email'] def run(self, email=False): if email: print bzrlib.osutils.user_email() else: print bzrlib.osutils.username() class cmd_selftest(Command): """Run internal test suite""" hidden = True def run(self): from bzrlib.selftest import selftest return int(not selftest()) class cmd_version(Command): """Show version of bzr.""" def run(self): show_version() def show_version(): print "bzr (bazaar-ng) %s" % bzrlib.__version__ # is bzrlib itself in a branch? bzrrev = bzrlib.get_bzr_revision() if bzrrev: print " (bzr checkout, revision %d {%s})" % bzrrev print bzrlib.__copyright__ print "http://bazaar-ng.org/" print print "bzr comes with ABSOLUTELY NO WARRANTY. bzr is free software, and" print "you may use, modify and redistribute it under the terms of the GNU" print "General Public License version 2 or later." class cmd_rocks(Command): """Statement of optimism.""" hidden = True def run(self): print "it sure does!" def parse_spec(spec): """ >>> parse_spec(None) [None, None] >>> parse_spec("./") ['./', None] >>> parse_spec("../@") ['..', -1] >>> parse_spec("../f/@35") ['../f', 35] """ if spec is None: return [None, None] if '/@' in spec: parsed = spec.split('/@') assert len(parsed) == 2 if parsed[1] == "": parsed[1] = -1 else: parsed[1] = int(parsed[1]) assert parsed[1] >=0 else: parsed = [spec, None] return parsed class cmd_merge(Command): """Perform a three-way merge of trees. The SPEC parameters are working tree or revision specifiers. Working trees are specified using standard paths or urls. No component of a directory path may begin with '@'. Working tree examples: '.', '..', 'foo@', but NOT 'foo/@bar' Revisions are specified using a dirname/@revno pair, where dirname is the branch directory and revno is the revision within that branch. If no revno is specified, the latest revision is used. Revision examples: './@127', 'foo/@', '../@1' The OTHER_SPEC parameter is required. If the BASE_SPEC parameter is not supplied, the common ancestor of OTHER_SPEC the current branch is used as the BASE. merge refuses to run if there are any uncommitted changes, unless --force is given. """ takes_args = ['other_spec', 'base_spec?'] takes_options = ['force'] def run(self, other_spec, base_spec=None, force=False): from bzrlib.merge import merge merge(parse_spec(other_spec), parse_spec(base_spec), check_clean=(not force)) class cmd_revert(Command): """Restore selected files from a previous revision. """ takes_args = ['file+'] def run(self, file_list): from bzrlib.branch import find_branch if not file_list: file_list = ['.'] b = find_branch(file_list[0]) b.revert([b.relpath(f) for f in file_list]) class cmd_merge_revert(Command): """Reverse all changes since the last commit. Only versioned files are affected. TODO: Store backups of any files that will be reverted, so that the revert can be undone. """ takes_options = ['revision'] def run(self, revision=-1): from bzrlib.merge import merge merge(('.', revision), parse_spec('.'), check_clean=False, ignore_zero=True) class cmd_assert_fail(Command): """Test reporting of assertion failures""" hidden = True def run(self): assert False, "always fails" class cmd_help(Command): """Show help on a command or other topic. For a list of all available commands, say 'bzr help commands'.""" takes_args = ['topic?'] aliases = ['?'] def run(self, topic=None): import help help.help(topic) class cmd_plugins(Command): """List plugins""" hidden = True def run(self): import bzrlib.plugin from pprint import pprint pprint(bzrlib.plugin.all_plugins) # list of all available options; the rhs can be either None for an # option that takes no argument, or a constructor function that checks # the type. OPTIONS = { 'all': None, 'diff-options': str, 'help': None, 'file': unicode, 'force': None, 'format': unicode, 'forward': None, 'message': unicode, 'no-recurse': None, 'profile': None, 'revision': _parse_revision_str, 'show-ids': None, 'timezone': str, 'verbose': None, 'version': None, 'email': None, 'unchanged': None, 'update': None, 'long': None, 'root': str, } SHORT_OPTIONS = { 'F': 'file', 'h': 'help', 'm': 'message', 'r': 'revision', 'v': 'verbose', 'l': 'long', } def parse_args(argv): """Parse command line. Arguments and options are parsed at this level before being passed down to specific command handlers. This routine knows, from a lookup table, something about the available options, what optargs they take, and which commands will accept them. >>> parse_args('--help'.split()) ([], {'help': True}) >>> parse_args('--version'.split()) ([], {'version': True}) >>> parse_args('status --all'.split()) (['status'], {'all': True}) >>> parse_args('commit --message=biter'.split()) (['commit'], {'message': u'biter'}) >>> parse_args('log -r 500'.split()) (['log'], {'revision': 500}) >>> parse_args('log -r500:600'.split()) (['log'], {'revision': [500, 600]}) >>> parse_args('log -vr500:600'.split()) (['log'], {'verbose': True, 'revision': [500, 600]}) >>> parse_args('log -rv500:600'.split()) #the r takes an argument Traceback (most recent call last): ... ValueError: invalid literal for int(): v500 """ args = [] opts = {} # TODO: Maybe handle '--' to end options? while argv: a = argv.pop(0) if a[0] == '-': # option names must not be unicode a = str(a) optarg = None if a[1] == '-': mutter(" got option %r" % a) if '=' in a: optname, optarg = a[2:].split('=', 1) else: optname = a[2:] if optname not in OPTIONS: raise BzrError('unknown long option %r' % a) else: shortopt = a[1:] if shortopt in SHORT_OPTIONS: # Multi-character options must have a space to delimit # their value optname = SHORT_OPTIONS[shortopt] else: # Single character short options, can be chained, # and have their value appended to their name shortopt = a[1:2] if shortopt not in SHORT_OPTIONS: # We didn't find the multi-character name, and we # didn't find the single char name raise BzrError('unknown short option %r' % a) optname = SHORT_OPTIONS[shortopt] if a[2:]: # There are extra things on this option # see if it is the value, or if it is another # short option optargfn = OPTIONS[optname] if optargfn is None: # This option does not take an argument, so the # next entry is another short option, pack it back # into the list argv.insert(0, '-' + a[2:]) else: # This option takes an argument, so pack it # into the array optarg = a[2:] if optname in opts: # XXX: Do we ever want to support this, e.g. for -r? raise BzrError('repeated option %r' % a) optargfn = OPTIONS[optname] if optargfn: if optarg == None: if not argv: raise BzrError('option %r needs an argument' % a) else: optarg = argv.pop(0) opts[optname] = optargfn(optarg) else: if optarg != None: raise BzrError('option %r takes no argument' % optname) opts[optname] = True else: args.append(a) return args, opts def _match_argform(cmd, takes_args, args): argdict = {} # step through args and takes_args, allowing appropriate 0-many matches for ap in takes_args: argname = ap[:-1] if ap[-1] == '?': if args: argdict[argname] = args.pop(0) elif ap[-1] == '*': # all remaining arguments if args: argdict[argname + '_list'] = args[:] args = [] else: argdict[argname + '_list'] = None elif ap[-1] == '+': if not args: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) else: argdict[argname + '_list'] = args[:] args = [] elif ap[-1] == '$': # all but one if len(args) < 2: raise BzrCommandError("command %r needs one or more %s" % (cmd, argname.upper())) argdict[argname + '_list'] = args[:-1] args[:-1] = [] else: # just a plain arg argname = ap if not args: raise BzrCommandError("command %r requires argument %s" % (cmd, argname.upper())) else: argdict[argname] = args.pop(0) if args: raise BzrCommandError("extra argument to command %s: %s" % (cmd, args[0])) return argdict def _parse_master_args(argv): """Parse the arguments that always go with the original command. These are things like bzr --no-plugins, etc. There are now 2 types of option flags. Ones that come *before* the command, and ones that come *after* the command. Ones coming *before* the command are applied against all possible commands. And are generally applied before plugins are loaded. The current list are: --builtin Allow plugins to load, but don't let them override builtin commands, they will still be allowed if they do not override a builtin. --no-plugins Don't load any plugins. This lets you get back to official source behavior. --profile Enable the hotspot profile before running the command. For backwards compatibility, this is also a non-master option. --version Spit out the version of bzr that is running and exit. This is also a non-master option. --help Run help and exit, also a non-master option (I think that should stay, though) >>> argv, opts = _parse_master_args(['bzr', '--test']) Traceback (most recent call last): ... BzrCommandError: Invalid master option: 'test' >>> argv, opts = _parse_master_args(['bzr', '--version', 'command']) >>> print argv ['command'] >>> print opts['version'] True >>> argv, opts = _parse_master_args(['bzr', '--profile', 'command', '--more-options']) >>> print argv ['command', '--more-options'] >>> print opts['profile'] True >>> argv, opts = _parse_master_args(['bzr', '--no-plugins', 'command']) >>> print argv ['command'] >>> print opts['no-plugins'] True >>> print opts['profile'] False >>> argv, opts = _parse_master_args(['bzr', 'command', '--profile']) >>> print argv ['command', '--profile'] >>> print opts['profile'] False """ master_opts = {'builtin':False, 'no-plugins':False, 'version':False, 'profile':False, 'help':False } # This is the point where we could hook into argv[0] to determine # what front-end is supposed to be run # For now, we are just ignoring it. cmd_name = argv.pop(0) for arg in argv[:]: if arg[:2] != '--': # at the first non-option, we return the rest break arg = arg[2:] # Remove '--' if arg not in master_opts: # We could say that this is not an error, that we should # just let it be handled by the main section instead raise BzrCommandError('Invalid master option: %r' % arg) argv.pop(0) # We are consuming this entry master_opts[arg] = True return argv, master_opts def run_bzr(argv): """Execute a command. This is similar to main(), but without all the trappings for logging and error handling. """ argv = [a.decode(bzrlib.user_encoding) for a in argv] try: # some options like --builtin and --no-plugins have special effects argv, master_opts = _parse_master_args(argv) if not master_opts['no-plugins']: from bzrlib.plugin import load_plugins load_plugins() args, opts = parse_args(argv) if master_opts['help']: from bzrlib.help import help if argv: help(argv[0]) else: help() return 0 if 'help' in opts: from bzrlib.help import help if args: help(args[0]) else: help() return 0 elif 'version' in opts: show_version() return 0 elif args and args[0] == 'builtin': include_plugins=False args = args[1:] cmd = str(args.pop(0)) except IndexError: import help help.help() return 1 plugins_override = not (master_opts['builtin']) canonical_cmd, cmd_class = get_cmd_class(cmd, plugins_override=plugins_override) profile = master_opts['profile'] # For backwards compatibility, I would rather stick with --profile being a # master/global option if 'profile' in opts: profile = True del opts['profile'] # check options are reasonable allowed = cmd_class.takes_options for oname in opts: if oname not in allowed: raise BzrCommandError("option '--%s' is not allowed for command %r" % (oname, cmd)) # mix arguments and options into one dictionary cmdargs = _match_argform(cmd, cmd_class.takes_args, args) cmdopts = {} for k, v in opts.items(): cmdopts[k.replace('-', '_')] = v if profile: import hotshot, tempfile pffileno, pfname = tempfile.mkstemp() try: prof = hotshot.Profile(pfname) ret = prof.runcall(cmd_class, cmdopts, cmdargs) or 0 prof.close() import hotshot.stats stats = hotshot.stats.load(pfname) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) return ret.status finally: os.close(pffileno) os.remove(pfname) else: return cmd_class(cmdopts, cmdargs).status def _report_exception(summary, quiet=False): import traceback log_error('bzr: ' + summary) bzrlib.trace.log_exception() if not quiet: tb = sys.exc_info()[2] exinfo = traceback.extract_tb(tb) if exinfo: sys.stderr.write(' at %s:%d in %s()\n' % exinfo[-1][:3]) sys.stderr.write(' see ~/.bzr.log for debug information\n') def main(argv): bzrlib.trace.open_tracefile(argv) try: try: try: return run_bzr(argv) finally: # do this here inside the exception wrappers to catch EPIPE sys.stdout.flush() except BzrError, e: quiet = isinstance(e, (BzrCommandError)) _report_exception('error: ' + e.args[0], quiet=quiet) if len(e.args) > 1: for h in e.args[1]: # some explanation or hints log_error(' ' + h) return 1 except AssertionError, e: msg = 'assertion failed' if str(e): msg += ': ' + str(e) _report_exception(msg) return 2 except KeyboardInterrupt, e: _report_exception('interrupted', quiet=True) return 2 except Exception, e: import errno quiet = False if (isinstance(e, IOError) and hasattr(e, 'errno') and e.errno == errno.EPIPE): quiet = True msg = 'broken pipe' else: msg = str(e).rstrip('\n') _report_exception(msg, quiet) return 2 finally: bzrlib.trace.close_trace() if __name__ == '__main__': sys.exit(main(sys.argv)) commit refs/heads/master mark :983 committer Martin Pool 1121053521 +1000 data 45 - fix imports now weave has moved into bzrlib from :982 M 644 inline tools/convertinv.py data 1739 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Experiment in converting existing bzr branches to weaves.""" import bzrlib.branch from bzrlib.weave import Weave from bzrlib.weavefile import write_weave import tempfile import hotshot def convert(): WEAVE_NAME = "inventory.weave" wf = Weave() b = bzrlib.branch.find_branch('.') print 'converting...' parents = set() revno = 1 for rev_id in b.revision_history(): print revno inv_xml = b.inventory_store[rev_id].readlines() weave_id = wf.add(parents, inv_xml) parents.add(weave_id) revno += 1 write_weave(wf, file(WEAVE_NAME, 'wb')) prof_f = tempfile.NamedTemporaryFile() prof = hotshot.Profile(prof_f.name) prof.runcall(convert) prof.close() import hotshot.stats stats = hotshot.stats.load(prof_f.name) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) commit refs/heads/master mark :984 committer Martin Pool 1121053537 +1000 data 26 - fix refactoring breakage from :983 M 644 inline bzrlib/weave.py data 20402 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? # TODO: Separate out some code to read and write weaves. # TODO: End marker for each version so we can stop reading? # TODO: Check that no insertion occurs inside a deletion that was # active in the version of the insertion. # TODO: Perhaps a special slower check() method that verifies more # nesting constraints and the MD5 of each version? try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. To clients the "lines" of the file are represented as a list of strings. These strings will typically have terminal newline characters, but this is not required. In particular files commonly do not have a newline at the end of the file. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the set (included_versions), which lists the previous versions also considered active; the versions included in those versions are included transitively. So new versions created from nothing list []; most versions have a single entry; some have more. _sha1s List of hex SHA-1 of each version, or None if not recorded. """ def __init__(self): self._l = [] self._v = [] self._sha1s = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. This must normally include the parents and the parent's parents, or wierd things might happen. text Sequence of lines to be added in the new version.""" ## self._check_versions(parents) ## self._check_lines(text) idx = len(self._v) import sha s = sha.new() for l in text: s.update(l) sha1 = s.hexdigest() del s if parents: delta = self._delta(self.inclusions(parents), text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._addversion(parents) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._addversion(None) self._sha1s.append(sha1) return idx def inclusions(self, versions): """Expand out everything included by versions.""" i = set(versions) for v in versions: try: i.update(self._v[v]) except IndexError: raise ValueError("version %d not present in weave" % v) return i def _addversion(self, parents): if parents: self._v.append(frozenset(parents)) else: self._v.append(frozenset()) def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, version): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" included = self.inclusions([version]) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] dset = set() lineno = 0 # line of weave, 0-based isactive = False WFE = WeaveFormatError for l in self._l: if isinstance(l, tuple): c, v = l if v in included: # only active blocks are interesting if c == '{': assert v not in istack istack.append(v) isactive = not dset elif c == '}': oldv = istack.pop() assert oldv == v isactive = istack and not dset elif c == '[': assert v not in dset dset.add(v) isactive = False else: assert c == ']' assert v in dset dset.remove(v) isactive = istack and not dset else: assert isinstance(l, basestring) if isactive: yield istack[-1], lineno, l lineno += 1 if istack: raise WFE("unclosed insertion blocks at end of weave", istack) if dset: raise WFE("unclosed deletion blocks at end of weave", dset) def get_iter(self, version): """Yield lines for the specified version.""" for origin, lineno, line in self._extract(self.inclusions([version])): yield line def get(self, index): return list(self.get_iter(index)) def mash_iter(self, included): """Return composed version of multiple included versions.""" included = frozenset(included) for origin, lineno, text in self._extract(included): yield text def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def numversions(self): l = len(self._v) assert l == len(self._sha1s) return l def check(self): # check no circular inclusions for version in range(self.numversions()): inclusions = list(self._v[version]) if inclusions: inclusions.sort() if inclusions[-1] >= version: raise WeaveFormatError("invalid included version %d for index %d" % (inclusions[-1], version)) # try extracting all versions; this is a bit slow and parallel # extraction could be used import sha for version in range(self.numversions()): s = sha.new() for l in self.get_iter(version): s.update(l) hd = s.hexdigest() expected = self._sha1s[version] if hd != expected: raise WeaveError("mismatched sha1 for version %d; " "got %s, expected %s" % (version, hd, expected)) # TODO: check insertions are properly nested, that there are # no lines outside of insertion blocks, that deletions are # properly paired, etc. def merge(self, merge_versions): """Automerge and mark conflicts between versions. This returns a sequence, each entry describing alternatives for a chunk of the file. Each of the alternatives is given as a list of lines. If there is a chunk of the file where there's no diagreement, only one alternative is given. """ # approach: find the included versions common to all the # merged versions raise NotImplementedError() def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ # basis a list of (origin, lineno, line) basis_lineno = [] basis_lines = [] for origin, lineno, line in self._extract(included): basis_lineno.append(lineno) basis_lines.append(line) # add a sentinal, because we can also match against the final line basis_lineno.append(len(self._l)) # XXX: which line of the weave should we really consider # matches the end of the file? the current code says it's the # last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis_lineno[i1] real_i2 = basis_lineno[i2] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def weave_info(filename, out): """Show some text information about the weave.""" from weavefile import read_weave wf = file(filename, 'rb') w = read_weave(wf) # FIXME: doesn't work on pipes weave_size = wf.tell() print >>out, "weave file size %d bytes" % weave_size print >>out, "weave contains %d versions" % len(w._v) total = 0 print '%6s %6s %8s %40s %20s' % ('ver', 'lines', 'bytes', 'sha1', 'parents') for i in (6, 6, 8, 40, 20): print '-' * i, print for i in range(len(w._v)): text = w.get(i) lines = len(text) bytes = sum((len(a) for a in text)) sha1 = w._sha1s[i] print '%6d %6d %8d %40s' % (i, lines, bytes, sha1), print ', '.join(map(str, w._v[i])) total += bytes print >>out, "versions total %d bytes" % total print >>out, "compression ratio %.3f" % (float(total)/float(weave_size)) def usage(): print """bzr weave tool Experimental tool for weave algorithm. usage: weave init WEAVEFILE Create an empty weave file weave get WEAVEFILE VERSION Write out specified version. weave check WEAVEFILE Check consistency of all versions. weave info WEAVEFILE Display table of contents. weave add WEAVEFILE [BASE...] < NEWTEXT Add NEWTEXT, with specified parent versions. weave annotate WEAVEFILE VERSION Display origin of each line. weave mash WEAVEFILE VERSION... Display composite of all selected versions. weave merge WEAVEFILE VERSION1 VERSION2 > OUT Auto-merge two versions and display conflicts. example: % weave init foo.weave % vi foo.txt % weave add foo.weave < foo.txt added version 0 (create updated version) % vi foo.txt % weave get foo.weave 0 | diff -u - foo.txt % weave add foo.weave 0 < foo.txt added version 1 % weave get foo.weave 0 > foo.txt (create forked version) % vi foo.txt % weave add foo.weave 0 < foo.txt added version 2 % weave merge foo.weave 1 2 > foo.txt (merge them) % vi foo.txt (resolve conflicts) % weave add foo.weave 1 2 < foo.txt (commit merged version) """ def main(argv): import sys import os from weavefile import write_weave, read_weave cmd = argv[1] def readit(): return read_weave(file(argv[2], 'rb')) if cmd == 'help': usage() elif cmd == 'add': w = readit() # at the moment, based on everything in the file parents = map(int, argv[3:]) lines = sys.stdin.readlines() ver = w.add(parents, lines) write_weave(w, file(argv[2], 'wb')) print 'added version %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() write_weave(w, file(fn, 'wb')) elif cmd == 'get': # get one version w = readit() sys.stdout.writelines(w.get_iter(int(argv[3]))) elif cmd == 'mash': # get composite w = readit() sys.stdout.writelines(w.mash_iter(map(int, argv[3:]))) elif cmd == 'annotate': w = readit() # newline is added to all lines regardless; too hard to get # reasonable formatting otherwise lasto = None for origin, text in w.annotate(int(argv[3])): text = text.rstrip('\r\n') if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin elif cmd == 'info': weave_info(argv[2], sys.stdout) elif cmd == 'check': w = readit() w.check() elif cmd == 'merge': if len(argv) != 5: usage() return 1 w = readit() v1, v2 = map(int, argv[3:5]) basis = w.inclusions([v1]).intersection(w.inclusions([v2])) base_lines = list(w.mash_iter(basis)) a_lines = list(w.get(v1)) b_lines = list(w.get(v2)) from bzrlib.merge3 import Merge3 m3 = Merge3(base_lines, a_lines, b_lines) name_a = 'version %d' % v1 name_b = 'version %d' % v2 sys.stdout.writelines(m3.merge_lines(name_a=name_a, name_b=name_b)) else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) commit refs/heads/master mark :985 committer Martin Pool 1121053794 +1000 data 86 - show progress bar during inventory conversion to weave, and make profiling optional from :984 M 644 inline bzrlib/progress.py data 7785 # Copyright (C) 2005 Aaron Bentley # Copyright (C) 2005 Canonical # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Simple text-mode progress indicator. To display an indicator, create a ProgressBar object. Call it, passing Progress objects indicating the current state. When done, call clear(). Progress is suppressed when output is not sent to a terminal, so as not to clutter log files. """ # TODO: remove functions in favour of keeping everything in one class # TODO: should be a global option e.g. --silent that disables progress # indicators, preferably without needing to adjust all code that # potentially calls them. # TODO: Perhaps don't write updates faster than a certain rate, say # 5/second. import sys import time def _width(): """Return estimated terminal width. TODO: Do something smart on Windows? TODO: Is there anything that gets a better update when the window is resized while the program is running? """ import os try: return int(os.environ['COLUMNS']) except (IndexError, KeyError, ValueError): return 80 def _supports_progress(f): if not hasattr(f, 'isatty'): return False if not f.isatty(): return False import os if os.environ.get('TERM') == 'dumb': # e.g. emacs compile window return False return True class ProgressBar(object): """Progress bar display object. Several options are available to control the display. These can be passed as parameters to the constructor or assigned at any time: show_pct Show percentage complete. show_spinner Show rotating baton. This ticks over on every update even if the values don't change. show_eta Show predicted time-to-completion. show_bar Show bar graph. show_count Show numerical counts. The output file should be in line-buffered or unbuffered mode. """ SPIN_CHARS = r'/-\|' MIN_PAUSE = 0.1 # seconds start_time = None last_update = None def __init__(self, to_file=sys.stderr, show_pct=False, show_spinner=False, show_eta=True, show_bar=True, show_count=True): object.__init__(self) self.to_file = to_file self.suppressed = not _supports_progress(self.to_file) self.spin_pos = 0 self.last_msg = None self.last_cnt = None self.last_total = None self.show_pct = show_pct self.show_spinner = show_spinner self.show_eta = show_eta self.show_bar = show_bar self.show_count = show_count def tick(self): self.update(self.last_msg, self.last_cnt, self.last_total) def update(self, msg, current_cnt=None, total_cnt=None): """Update and redraw progress bar.""" if self.suppressed: return # save these for the tick() function self.last_msg = msg self.last_cnt = current_cnt self.last_total = total_cnt now = time.time() if self.start_time is None: self.start_time = now else: interval = now - self.last_update if interval > 0 and interval < self.MIN_PAUSE: return self.last_update = now width = _width() if total_cnt: assert current_cnt <= total_cnt if current_cnt: assert current_cnt >= 0 if self.show_eta and self.start_time and total_cnt: eta = get_eta(self.start_time, current_cnt, total_cnt) eta_str = " " + str_tdelta(eta) else: eta_str = "" if self.show_spinner: spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' ' else: spin_str = '' # always update this; it's also used for the bar self.spin_pos += 1 if self.show_pct and total_cnt and current_cnt: pct = 100.0 * current_cnt / total_cnt pct_str = ' (%5.1f%%)' % pct else: pct_str = '' if not self.show_count: count_str = '' elif current_cnt is None: count_str = '' elif total_cnt is None: count_str = ' %i' % (current_cnt) else: # make both fields the same size t = '%i' % (total_cnt) c = '%*i' % (len(t), current_cnt) count_str = ' ' + c + '/' + t if self.show_bar: # progress bar, if present, soaks up all remaining space cols = width - 1 - len(msg) - len(spin_str) - len(pct_str) \ - len(eta_str) - len(count_str) - 3 if total_cnt: # number of markers highlighted in bar markers = int(round(float(cols) * current_cnt / total_cnt)) bar_str = '[' + ('=' * markers).ljust(cols) + '] ' elif False: # don't know total, so can't show completion. # so just show an expanded spinning thingy m = self.spin_pos % cols ms = (' ' * m + '*').ljust(cols) bar_str = '[' + ms + '] ' else: bar_str = '' else: bar_str = '' m = spin_str + bar_str + msg + count_str + pct_str + eta_str assert len(m) < width self.to_file.write('\r' + m.ljust(width - 1)) #self.to_file.flush() def clear(self): if self.suppressed: return self.to_file.write('\r%s\r' % (' ' * (_width() - 1))) #self.to_file.flush() def str_tdelta(delt): if delt is None: return "-:--:--" delt = int(round(delt)) return '%d:%02d:%02d' % (delt/3600, (delt/60) % 60, delt % 60) def get_eta(start_time, current, total, enough_samples=3): if start_time is None: return None if not total: return None if current < enough_samples: return None if current > total: return None # wtf? elapsed = time.time() - start_time if elapsed < 2.0: # not enough time to estimate return None total_duration = float(elapsed) * float(total) / float(current) assert total_duration >= elapsed return total_duration - elapsed def run_tests(): import doctest result = doctest.testmod() if result[1] > 0: if result[0] == 0: print "All tests passed" else: print "No tests to run" def demo(): from time import sleep pb = ProgressBar(show_pct=True, show_bar=True, show_spinner=False) for i in range(100): pb.update('Elephanten', i, 99) sleep(0.1) sleep(2) pb.clear() sleep(1) print 'done!' if __name__ == "__main__": demo() M 644 inline tools/convertinv.py data 2059 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Experiment in converting existing bzr branches to weaves.""" import bzrlib.branch from bzrlib.weave import Weave from bzrlib.weavefile import write_weave from bzrlib.progress import ProgressBar import tempfile import hotshot import sys def convert(): WEAVE_NAME = "inventory.weave" pb = ProgressBar() wf = Weave() b = bzrlib.branch.find_branch('.') parents = set() revno = 1 rev_history = b.revision_history() for rev_id in rev_history: pb.update('converting inventory', revno, len(rev_history)) inv_xml = b.inventory_store[rev_id].readlines() weave_id = wf.add(parents, inv_xml) parents.add(weave_id) revno += 1 pb.update('write weave', None, None) write_weave(wf, file(WEAVE_NAME, 'wb')) pb.clear() def profile_convert(): prof_f = tempfile.NamedTemporaryFile() prof = hotshot.Profile(prof_f.name) prof.runcall(convert) prof.close() import hotshot.stats stats = hotshot.stats.load(prof_f.name) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) if '-p' in sys.argv[1:]: profile_convert() else: convert() commit refs/heads/master mark :986 committer Martin Pool 1121054156 +1000 data 55 - weave info should show minimal expression of parents from :985 M 644 inline bzrlib/weave.py data 20942 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? # TODO: Separate out some code to read and write weaves. # TODO: End marker for each version so we can stop reading? # TODO: Check that no insertion occurs inside a deletion that was # active in the version of the insertion. # TODO: Perhaps a special slower check() method that verifies more # nesting constraints and the MD5 of each version? try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. To clients the "lines" of the file are represented as a list of strings. These strings will typically have terminal newline characters, but this is not required. In particular files commonly do not have a newline at the end of the file. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the set (included_versions), which lists the previous versions also considered active; the versions included in those versions are included transitively. So new versions created from nothing list []; most versions have a single entry; some have more. _sha1s List of hex SHA-1 of each version, or None if not recorded. """ def __init__(self): self._l = [] self._v = [] self._sha1s = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. This must normally include the parents and the parent's parents, or wierd things might happen. text Sequence of lines to be added in the new version.""" ## self._check_versions(parents) ## self._check_lines(text) idx = len(self._v) import sha s = sha.new() for l in text: s.update(l) sha1 = s.hexdigest() del s if parents: delta = self._delta(self.inclusions(parents), text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._addversion(parents) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._addversion(None) self._sha1s.append(sha1) return idx def inclusions(self, versions): """Expand out everything included by versions.""" i = set(versions) for v in versions: try: i.update(self._v[v]) except IndexError: raise ValueError("version %d not present in weave" % v) return i def minimal_parents(self, version): """Find the minimal set of parents for the version.""" included = self._v[version] if not included: return [] li = list(included) li.sort() li.reverse() mininc = [] gotit = set() for pv in li: if pv not in gotit: mininc.append(pv) gotit.update(self._v[pv]) assert mininc[0] >= 0 assert mininc[-1] < version return mininc def _addversion(self, parents): if parents: self._v.append(frozenset(parents)) else: self._v.append(frozenset()) def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, version): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" included = self.inclusions([version]) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] dset = set() lineno = 0 # line of weave, 0-based isactive = False WFE = WeaveFormatError for l in self._l: if isinstance(l, tuple): c, v = l if v in included: # only active blocks are interesting if c == '{': assert v not in istack istack.append(v) isactive = not dset elif c == '}': oldv = istack.pop() assert oldv == v isactive = istack and not dset elif c == '[': assert v not in dset dset.add(v) isactive = False else: assert c == ']' assert v in dset dset.remove(v) isactive = istack and not dset else: assert isinstance(l, basestring) if isactive: yield istack[-1], lineno, l lineno += 1 if istack: raise WFE("unclosed insertion blocks at end of weave", istack) if dset: raise WFE("unclosed deletion blocks at end of weave", dset) def get_iter(self, version): """Yield lines for the specified version.""" for origin, lineno, line in self._extract(self.inclusions([version])): yield line def get(self, index): return list(self.get_iter(index)) def mash_iter(self, included): """Return composed version of multiple included versions.""" included = frozenset(included) for origin, lineno, text in self._extract(included): yield text def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def numversions(self): l = len(self._v) assert l == len(self._sha1s) return l def check(self): # check no circular inclusions for version in range(self.numversions()): inclusions = list(self._v[version]) if inclusions: inclusions.sort() if inclusions[-1] >= version: raise WeaveFormatError("invalid included version %d for index %d" % (inclusions[-1], version)) # try extracting all versions; this is a bit slow and parallel # extraction could be used import sha for version in range(self.numversions()): s = sha.new() for l in self.get_iter(version): s.update(l) hd = s.hexdigest() expected = self._sha1s[version] if hd != expected: raise WeaveError("mismatched sha1 for version %d; " "got %s, expected %s" % (version, hd, expected)) # TODO: check insertions are properly nested, that there are # no lines outside of insertion blocks, that deletions are # properly paired, etc. def merge(self, merge_versions): """Automerge and mark conflicts between versions. This returns a sequence, each entry describing alternatives for a chunk of the file. Each of the alternatives is given as a list of lines. If there is a chunk of the file where there's no diagreement, only one alternative is given. """ # approach: find the included versions common to all the # merged versions raise NotImplementedError() def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ # basis a list of (origin, lineno, line) basis_lineno = [] basis_lines = [] for origin, lineno, line in self._extract(included): basis_lineno.append(lineno) basis_lines.append(line) # add a sentinal, because we can also match against the final line basis_lineno.append(len(self._l)) # XXX: which line of the weave should we really consider # matches the end of the file? the current code says it's the # last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis_lineno[i1] real_i2 = basis_lineno[i2] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def weave_info(filename, out): """Show some text information about the weave.""" from weavefile import read_weave wf = file(filename, 'rb') w = read_weave(wf) # FIXME: doesn't work on pipes weave_size = wf.tell() print >>out, "weave file size %d bytes" % weave_size print >>out, "weave contains %d versions" % len(w._v) total = 0 print '%6s %6s %8s %40s %20s' % ('ver', 'lines', 'bytes', 'sha1', 'parents') for i in (6, 6, 8, 40, 20): print '-' * i, print for i in range(len(w._v)): text = w.get(i) lines = len(text) bytes = sum((len(a) for a in text)) sha1 = w._sha1s[i] print '%6d %6d %8d %40s' % (i, lines, bytes, sha1), print ', '.join(map(str, w.minimal_parents(i))) total += bytes print >>out, "versions total %d bytes" % total print >>out, "compression ratio %.3f" % (float(total)/float(weave_size)) def usage(): print """bzr weave tool Experimental tool for weave algorithm. usage: weave init WEAVEFILE Create an empty weave file weave get WEAVEFILE VERSION Write out specified version. weave check WEAVEFILE Check consistency of all versions. weave info WEAVEFILE Display table of contents. weave add WEAVEFILE [BASE...] < NEWTEXT Add NEWTEXT, with specified parent versions. weave annotate WEAVEFILE VERSION Display origin of each line. weave mash WEAVEFILE VERSION... Display composite of all selected versions. weave merge WEAVEFILE VERSION1 VERSION2 > OUT Auto-merge two versions and display conflicts. example: % weave init foo.weave % vi foo.txt % weave add foo.weave < foo.txt added version 0 (create updated version) % vi foo.txt % weave get foo.weave 0 | diff -u - foo.txt % weave add foo.weave 0 < foo.txt added version 1 % weave get foo.weave 0 > foo.txt (create forked version) % vi foo.txt % weave add foo.weave 0 < foo.txt added version 2 % weave merge foo.weave 1 2 > foo.txt (merge them) % vi foo.txt (resolve conflicts) % weave add foo.weave 1 2 < foo.txt (commit merged version) """ def main(argv): import sys import os from weavefile import write_weave, read_weave cmd = argv[1] def readit(): return read_weave(file(argv[2], 'rb')) if cmd == 'help': usage() elif cmd == 'add': w = readit() # at the moment, based on everything in the file parents = map(int, argv[3:]) lines = sys.stdin.readlines() ver = w.add(parents, lines) write_weave(w, file(argv[2], 'wb')) print 'added version %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() write_weave(w, file(fn, 'wb')) elif cmd == 'get': # get one version w = readit() sys.stdout.writelines(w.get_iter(int(argv[3]))) elif cmd == 'mash': # get composite w = readit() sys.stdout.writelines(w.mash_iter(map(int, argv[3:]))) elif cmd == 'annotate': w = readit() # newline is added to all lines regardless; too hard to get # reasonable formatting otherwise lasto = None for origin, text in w.annotate(int(argv[3])): text = text.rstrip('\r\n') if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin elif cmd == 'info': weave_info(argv[2], sys.stdout) elif cmd == 'check': w = readit() w.check() elif cmd == 'merge': if len(argv) != 5: usage() return 1 w = readit() v1, v2 = map(int, argv[3:5]) basis = w.inclusions([v1]).intersection(w.inclusions([v2])) base_lines = list(w.mash_iter(basis)) a_lines = list(w.get(v1)) b_lines = list(w.get(v2)) from bzrlib.merge3 import Merge3 m3 = Merge3(base_lines, a_lines, b_lines) name_a = 'version %d' % v1 name_b = 'version %d' % v2 sys.stdout.writelines(m3.merge_lines(name_a=name_a, name_b=name_b)) else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) M 644 inline tools/convertinv.py data 2094 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Experiment in converting existing bzr branches to weaves.""" import bzrlib.branch from bzrlib.weave import Weave from bzrlib.weavefile import write_weave from bzrlib.progress import ProgressBar import tempfile import hotshot import sys def convert(): WEAVE_NAME = "inventory.weave" pb = ProgressBar() wf = Weave() b = bzrlib.branch.find_branch('.') parents = set() revno = 1 rev_history = b.revision_history() for rev_id in rev_history: pb.update('converting inventory', revno, len(rev_history)) inv_xml = b.inventory_store[rev_id].readlines() weave_id = wf.add(parents, inv_xml) parents = set([weave_id]) # always just one parent revno += 1 pb.update('write weave', None, None) write_weave(wf, file(WEAVE_NAME, 'wb')) pb.clear() def profile_convert(): prof_f = tempfile.NamedTemporaryFile() prof = hotshot.Profile(prof_f.name) prof.runcall(convert) prof.close() import hotshot.stats stats = hotshot.stats.load(prof_f.name) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) if '-p' in sys.argv[1:]: profile_convert() else: convert() commit refs/heads/master mark :987 committer Martin Pool 1121054913 +1000 data 29 - fix up refactoring of weave from :986 M 644 inline bzrlib/weave.py data 21004 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? # TODO: Separate out some code to read and write weaves. # TODO: End marker for each version so we can stop reading? # TODO: Check that no insertion occurs inside a deletion that was # active in the version of the insertion. # TODO: Perhaps a special slower check() method that verifies more # nesting constraints and the MD5 of each version? try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. To clients the "lines" of the file are represented as a list of strings. These strings will typically have terminal newline characters, but this is not required. In particular files commonly do not have a newline at the end of the file. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of versions, indexed by index number. For each version we store the set (included_versions), which lists the previous versions also considered active; the versions included in those versions are included transitively. So new versions created from nothing list []; most versions have a single entry; some have more. _sha1s List of hex SHA-1 of each version, or None if not recorded. """ def __init__(self): self._l = [] self._v = [] self._sha1s = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of parent version numbers. This must normally include the parents and the parent's parents, or wierd things might happen. text Sequence of lines to be added in the new version.""" ## self._check_versions(parents) ## self._check_lines(text) idx = len(self._v) import sha s = sha.new() for l in text: s.update(l) sha1 = s.hexdigest() del s if parents: delta = self._delta(self.inclusions(parents), text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._addversion(parents) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._addversion(None) self._sha1s.append(sha1) return idx def inclusions(self, versions): """Expand out everything included by versions.""" i = set(versions) for v in versions: try: i.update(self._v[v]) except IndexError: raise ValueError("version %d not present in weave" % v) return i def minimal_parents(self, version): """Find the minimal set of parents for the version.""" included = self._v[version] if not included: return [] li = list(included) li.sort() li.reverse() mininc = [] gotit = set() for pv in li: if pv not in gotit: mininc.append(pv) gotit.update(self._v[pv]) assert mininc[0] >= 0 assert mininc[-1] < version return mininc def _addversion(self, parents): if parents: self._v.append(frozenset(parents)) else: self._v.append(frozenset()) def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, version): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" included = self.inclusions([version]) for origin, lineno, text in self._extract(included): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] dset = set() lineno = 0 # line of weave, 0-based isactive = False WFE = WeaveFormatError for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': assert v not in istack istack.append(v) if not dset: isactive = (v in included) elif c == '}': oldv = istack.pop() assert oldv == v isactive = (not dset) and (istack and istack[-1] in included) elif c == '[': if v in included: assert v not in dset dset.add(v) isactive = False else: assert c == ']' if v in included: assert v in dset dset.remove(v) isactive = (not dset) and (istack and istack[-1] in included) else: assert isinstance(l, basestring) if isactive: yield istack[-1], lineno, l lineno += 1 if istack: raise WFE("unclosed insertion blocks at end of weave", istack) if dset: raise WFE("unclosed deletion blocks at end of weave", dset) def get_iter(self, version): """Yield lines for the specified version.""" for origin, lineno, line in self._extract(self.inclusions([version])): yield line def get(self, index): return list(self.get_iter(index)) def mash_iter(self, included): """Return composed version of multiple included versions.""" included = frozenset(included) for origin, lineno, text in self._extract(included): yield text def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def numversions(self): l = len(self._v) assert l == len(self._sha1s) return l def check(self): # check no circular inclusions for version in range(self.numversions()): inclusions = list(self._v[version]) if inclusions: inclusions.sort() if inclusions[-1] >= version: raise WeaveFormatError("invalid included version %d for index %d" % (inclusions[-1], version)) # try extracting all versions; this is a bit slow and parallel # extraction could be used import sha for version in range(self.numversions()): s = sha.new() for l in self.get_iter(version): s.update(l) hd = s.hexdigest() expected = self._sha1s[version] if hd != expected: raise WeaveError("mismatched sha1 for version %d; " "got %s, expected %s" % (version, hd, expected)) # TODO: check insertions are properly nested, that there are # no lines outside of insertion blocks, that deletions are # properly paired, etc. def merge(self, merge_versions): """Automerge and mark conflicts between versions. This returns a sequence, each entry describing alternatives for a chunk of the file. Each of the alternatives is given as a list of lines. If there is a chunk of the file where there's no diagreement, only one alternative is given. """ # approach: find the included versions common to all the # merged versions raise NotImplementedError() def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ # basis a list of (origin, lineno, line) basis_lineno = [] basis_lines = [] for origin, lineno, line in self._extract(included): basis_lineno.append(lineno) basis_lines.append(line) # add a sentinal, because we can also match against the final line basis_lineno.append(len(self._l)) # XXX: which line of the weave should we really consider # matches the end of the file? the current code says it's the # last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis_lineno[i1] real_i2 = basis_lineno[i2] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def weave_info(filename, out): """Show some text information about the weave.""" from weavefile import read_weave wf = file(filename, 'rb') w = read_weave(wf) # FIXME: doesn't work on pipes weave_size = wf.tell() print >>out, "weave file size %d bytes" % weave_size print >>out, "weave contains %d versions" % len(w._v) total = 0 print '%6s %6s %8s %40s %20s' % ('ver', 'lines', 'bytes', 'sha1', 'parents') for i in (6, 6, 8, 40, 20): print '-' * i, print for i in range(len(w._v)): text = w.get(i) lines = len(text) bytes = sum((len(a) for a in text)) sha1 = w._sha1s[i] print '%6d %6d %8d %40s' % (i, lines, bytes, sha1), print ', '.join(map(str, w.minimal_parents(i))) total += bytes print >>out, "versions total %d bytes" % total print >>out, "compression ratio %.3f" % (float(total)/float(weave_size)) def usage(): print """bzr weave tool Experimental tool for weave algorithm. usage: weave init WEAVEFILE Create an empty weave file weave get WEAVEFILE VERSION Write out specified version. weave check WEAVEFILE Check consistency of all versions. weave info WEAVEFILE Display table of contents. weave add WEAVEFILE [BASE...] < NEWTEXT Add NEWTEXT, with specified parent versions. weave annotate WEAVEFILE VERSION Display origin of each line. weave mash WEAVEFILE VERSION... Display composite of all selected versions. weave merge WEAVEFILE VERSION1 VERSION2 > OUT Auto-merge two versions and display conflicts. example: % weave init foo.weave % vi foo.txt % weave add foo.weave < foo.txt added version 0 (create updated version) % vi foo.txt % weave get foo.weave 0 | diff -u - foo.txt % weave add foo.weave 0 < foo.txt added version 1 % weave get foo.weave 0 > foo.txt (create forked version) % vi foo.txt % weave add foo.weave 0 < foo.txt added version 2 % weave merge foo.weave 1 2 > foo.txt (merge them) % vi foo.txt (resolve conflicts) % weave add foo.weave 1 2 < foo.txt (commit merged version) """ def main(argv): import sys import os from weavefile import write_weave, read_weave cmd = argv[1] def readit(): return read_weave(file(argv[2], 'rb')) if cmd == 'help': usage() elif cmd == 'add': w = readit() # at the moment, based on everything in the file parents = map(int, argv[3:]) lines = sys.stdin.readlines() ver = w.add(parents, lines) write_weave(w, file(argv[2], 'wb')) print 'added version %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() write_weave(w, file(fn, 'wb')) elif cmd == 'get': # get one version w = readit() sys.stdout.writelines(w.get_iter(int(argv[3]))) elif cmd == 'mash': # get composite w = readit() sys.stdout.writelines(w.mash_iter(map(int, argv[3:]))) elif cmd == 'annotate': w = readit() # newline is added to all lines regardless; too hard to get # reasonable formatting otherwise lasto = None for origin, text in w.annotate(int(argv[3])): text = text.rstrip('\r\n') if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin elif cmd == 'info': weave_info(argv[2], sys.stdout) elif cmd == 'check': w = readit() w.check() elif cmd == 'merge': if len(argv) != 5: usage() return 1 w = readit() v1, v2 = map(int, argv[3:5]) basis = w.inclusions([v1]).intersection(w.inclusions([v2])) base_lines = list(w.mash_iter(basis)) a_lines = list(w.get(v1)) b_lines = list(w.get(v2)) from bzrlib.merge3 import Merge3 m3 = Merge3(base_lines, a_lines, b_lines) name_a = 'version %d' % v1 name_b = 'version %d' % v2 sys.stdout.writelines(m3.merge_lines(name_a=name_a, name_b=name_b)) else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) M 644 inline bzrlib/weavefile.py data 3956 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Store and retrieve weaves in files. There is one format marker followed by a blank line, followed by a series of version headers, followed by the weave itself. Each version marker has 'i' and the included previous versions, then '1' and the SHA-1 of the text, if known. The inclusions do not need to list versions included by a parent. The weave is bracketed by 'w' and 'W' lines, and includes the '{}[]' processing instructions. Lines of text are prefixed by '.' if the line contains a newline, or ',' if not. """ # TODO: When extracting a single version it'd be enough to just pass # an iterator returning the weave lines... FORMAT_1 = '# bzr weave file v3\n' def write_weave(weave, f, format=None): if format == None or format == 1: return write_weave_v1(weave, f) else: raise ValueError("unknown weave format %r" % format) def write_weave_v1(weave, f): """Write weave to file f.""" print >>f, FORMAT_1, for version, included in enumerate(weave._v): if included: mininc = weave.minimal_parents(version) print >>f, 'i', for i in mininc: print >>f, i, print >>f else: print >>f, 'i' print >>f, '1', weave._sha1s[version] print >>f print >>f, 'w' for l in weave._l: if isinstance(l, tuple): assert l[0] in '{}[]' print >>f, '%s %d' % l else: # text line if not l: print >>f, ', ' elif l[-1] == '\n': assert l.find('\n', 0, -1) == -1 print >>f, '.', l, else: assert l.find('\n') == -1 print >>f, ',', l print >>f, 'W' def read_weave(f): return read_weave_v1(f) def read_weave_v1(f): from weave import Weave, WeaveFormatError w = Weave() wfe = WeaveFormatError l = f.readline() if l != FORMAT_1: raise WeaveFormatError('invalid weave file header: %r' % l) ver = 0 while True: l = f.readline() if l[0] == 'i': ver += 1 if len(l) > 2: included = map(int, l[2:].split(' ')) full = set() for pv in included: full.add(pv) full.update(w._v[pv]) w._addversion(full) else: w._addversion(None) l = f.readline()[:-1] assert l.startswith('1 ') w._sha1s.append(l[2:]) l = f.readline() assert l == '\n' elif l == 'w\n': break else: raise WeaveFormatError('unexpected line %r' % l) while True: l = f.readline() if l == 'W\n': break elif l.startswith('. '): w._l.append(intern(l[2:])) # include newline elif l.startswith(', '): w._l.append(l[2:-1]) # exclude newline else: assert l[0] in '{}[]', l assert l[1] == ' ', l w._l.append((intern(l[0]), int(l[2:]))) return w M 644 inline tools/testweave.py data 18435 #! /usr/bin/python2.4 # Copyright (C) 2005 by Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """test suite for weave algorithm""" import testsweet from bzrlib.weave import Weave, WeaveFormatError from bzrlib.weavefile import write_weave, read_weave from pprint import pformat try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet # texts for use in testing TEXT_0 = ["Hello world"] TEXT_1 = ["Hello world", "A second line"] class TestBase(testsweet.TestBase): def check_read_write(self, k): """Check the weave k can be written & re-read.""" from tempfile import TemporaryFile tf = TemporaryFile() write_weave(k, tf) tf.seek(0) k2 = read_weave(tf) if k != k2: tf.seek(0) self.log('serialized weave:') self.log(tf.read()) self.fail('read/write check failed') class Easy(TestBase): def runTest(self): k = Weave() class StoreText(TestBase): """Store and retrieve a simple text.""" def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(k.get(idx), TEXT_0) self.assertEqual(idx, 0) class AnnotateOne(TestBase): def runTest(self): k = Weave() k.add([], TEXT_0) self.assertEqual(k.annotate(0), [(0, TEXT_0[0])]) class StoreTwo(TestBase): def runTest(self): k = Weave() idx = k.add([], TEXT_0) self.assertEqual(idx, 0) idx = k.add([], TEXT_1) self.assertEqual(idx, 1) self.assertEqual(k.get(0), TEXT_0) self.assertEqual(k.get(1), TEXT_1) k.dump(self.TEST_LOG) class DeltaAdd(TestBase): """Detection of changes prior to inserting new revision.""" def runTest(self): k = Weave() k.add([], ['line 1']) self.assertEqual(k._l, [('{', 0), 'line 1', ('}', 0), ]) changes = list(k._delta(set([0]), ['line 1', 'new line'])) self.log('raw changes: ' + pformat(changes)) # currently there are 3 lines in the weave, and we insert after them self.assertEquals(changes, [(3, 3, ['new line'])]) changes = k._delta(set([0]), ['top line', 'line 1']) self.assertEquals(list(changes), [(1, 1, ['top line'])]) self.check_read_write(k) class InvalidAdd(TestBase): """Try to use invalid version number during add.""" def runTest(self): k = Weave() self.assertRaises(ValueError, k.add, [69], ['new text!']) class InsertLines(TestBase): """Store a revision that adds one line to the original. Look at the annotations to make sure that the first line is matched and not stored repeatedly.""" def runTest(self): k = Weave() k.add([], ['line 1']) k.add([0], ['line 1', 'line 2']) self.assertEqual(k.annotate(0), [(0, 'line 1')]) self.assertEqual(k.get(1), ['line 1', 'line 2']) self.assertEqual(k.annotate(1), [(0, 'line 1'), (1, 'line 2')]) k.add([0], ['line 1', 'diverged line']) self.assertEqual(k.annotate(2), [(0, 'line 1'), (2, 'diverged line')]) text3 = ['line 1', 'middle line', 'line 2'] k.add([0, 1], text3) self.log("changes to text3: " + pformat(list(k._delta(set([0, 1]), text3)))) self.log("k._l=" + pformat(k._l)) self.assertEqual(k.annotate(3), [(0, 'line 1'), (3, 'middle line'), (1, 'line 2')]) # now multiple insertions at different places k.add([0, 1, 3], ['line 1', 'aaa', 'middle line', 'bbb', 'line 2', 'ccc']) self.assertEqual(k.annotate(4), [(0, 'line 1'), (4, 'aaa'), (3, 'middle line'), (4, 'bbb'), (1, 'line 2'), (4, 'ccc')]) class DeleteLines(TestBase): """Deletion of lines from existing text. Try various texts all based on a common ancestor.""" def runTest(self): k = Weave() base_text = ['one', 'two', 'three', 'four'] k.add([], base_text) texts = [['one', 'two', 'three'], ['two', 'three', 'four'], ['one', 'four'], ['one', 'two', 'three', 'four'], ] for t in texts: ver = k.add([0], t) self.log('final weave:') self.log('k._l=' + pformat(k._l)) for i in range(len(texts)): self.assertEqual(k.get(i+1), texts[i]) class SuicideDelete(TestBase): """Invalid weave which tries to add and delete simultaneously.""" def runTest(self): k = Weave() k._v = [(), ] k._l = [('{', 0), 'first line', ('[', 0), 'deleted in 0', (']', 0), ('}', 0), ] ################################### SKIPPED # Weave.get doesn't trap this anymore return self.assertRaises(WeaveFormatError, k.get, 0) class CannedDelete(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [(), frozenset([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'last line', ]) class CannedReplacement(TestBase): """Unpack canned weave with deleted lines.""" def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0]), ] k._l = [('{', 0), 'first line', ('[', 1), 'line to be deleted', (']', 1), ('{', 1), 'replacement line', ('}', 1), 'last line', ('}', 0), ] self.assertEqual(k.get(0), ['first line', 'line to be deleted', 'last line', ]) self.assertEqual(k.get(1), ['first line', 'replacement line', 'last line', ]) class BadWeave(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [frozenset(), ] k._l = ['bad line', ('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] ################################### SKIPPED # Weave.get doesn't trap this anymore return self.assertRaises(WeaveFormatError, k.get, 0) class BadInsert(TestBase): """Test that we trap an insert which should not occur.""" def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0]), frozenset([0]), frozenset([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 1), ' more in 1', ('}', 1), ('}', 1), ('}', 0)] # this is not currently enforced by get return ########################################## self.assertRaises(WeaveFormatError, k.get, 0) self.assertRaises(WeaveFormatError, k.get, 1) class InsertNested(TestBase): """Insertion with nested instructions.""" def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0]), frozenset([0]), frozenset([0,1,2]), ] k._l = [('{', 0), 'foo {', ('{', 1), ' added in version 1', ('{', 2), ' added in v2', ('}', 2), ' also from v1', ('}', 1), '}', ('}', 0)] self.assertEqual(k.get(0), ['foo {', '}']) self.assertEqual(k.get(1), ['foo {', ' added in version 1', ' also from v1', '}']) self.assertEqual(k.get(2), ['foo {', ' added in v2', '}']) self.assertEqual(k.get(3), ['foo {', ' added in version 1', ' added in v2', ' also from v1', '}']) class DeleteLines2(TestBase): """Test recording revisions that delete lines. This relies on the weave having a way to represent lines knocked out by a later revision.""" def runTest(self): k = Weave() k.add([], ["line the first", "line 2", "line 3", "fine"]) self.assertEqual(len(k.get(0)), 4) k.add([0], ["line the first", "fine"]) self.assertEqual(k.get(1), ["line the first", "fine"]) self.assertEqual(k.annotate(1), [(0, "line the first"), (0, "fine")]) class IncludeVersions(TestBase): """Check texts that are stored across multiple revisions. Here we manually create a weave with particular encoding and make sure it unpacks properly. Text 0 includes nothing; text 1 includes text 0 and adds some lines. """ def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0])] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1)] self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(0), ["first line"]) k.dump(self.TEST_LOG) class DivergedIncludes(TestBase): """Weave with two diverged texts based on version 0. """ def runTest(self): k = Weave() k._v = [frozenset(), frozenset([0]), frozenset([0]), ] k._l = [('{', 0), "first line", ('}', 0), ('{', 1), "second line", ('}', 1), ('{', 2), "alternative second line", ('}', 2), ] self.assertEqual(k.get(0), ["first line"]) self.assertEqual(k.get(1), ["first line", "second line"]) self.assertEqual(k.get(2), ["first line", "alternative second line"]) self.assertEqual(k.inclusions([2]), set([0, 2])) class ReplaceLine(TestBase): def runTest(self): k = Weave() text0 = ['cheddar', 'stilton', 'gruyere'] text1 = ['cheddar', 'blue vein', 'neufchatel', 'chevre'] k.add([], text0) k.add([0], text1) self.log('k._l=' + pformat(k._l)) self.assertEqual(k.get(0), text0) self.assertEqual(k.get(1), text1) class Merge(TestBase): """Storage of versions that merge diverged parents""" def runTest(self): k = Weave() texts = [['header'], ['header', '', 'line from 1'], ['header', '', 'line from 2', 'more from 2'], ['header', '', 'line from 1', 'fixup line', 'line from 2'], ] k.add([], texts[0]) k.add([0], texts[1]) k.add([0], texts[2]) k.add([0, 1, 2], texts[3]) for i, t in enumerate(texts): self.assertEqual(k.get(i), t) self.assertEqual(k.annotate(3), [(0, 'header'), (1, ''), (1, 'line from 1'), (3, 'fixup line'), (2, 'line from 2'), ]) self.assertEqual(k.inclusions([3]), set([0, 1, 2, 3])) self.log('k._l=' + pformat(k._l)) self.check_read_write(k) class Conflicts(TestBase): """Test detection of conflicting regions during a merge. A base version is inserted, then two descendents try to insert different lines in the same place. These should be reported as a possible conflict and forwarded to the user.""" def runTest(self): return # NOT RUN k = Weave() k.add([], ['aaa', 'bbb']) k.add([0], ['aaa', '111', 'bbb']) k.add([1], ['aaa', '222', 'bbb']) merged = k.merge([1, 2]) self.assertEquals([[['aaa']], [['111'], ['222']], [['bbb']]]) class NonConflict(TestBase): """Two descendants insert compatible changes. No conflict should be reported.""" def runTest(self): return # NOT RUN k = Weave() k.add([], ['aaa', 'bbb']) k.add([0], ['111', 'aaa', 'ccc', 'bbb']) k.add([1], ['aaa', 'ccc', 'bbb', '222']) class AutoMerge(TestBase): def runTest(self): k = Weave() texts = [['header', 'aaa', 'bbb'], ['header', 'aaa', 'line from 1', 'bbb'], ['header', 'aaa', 'bbb', 'line from 2', 'more from 2'], ] k.add([], texts[0]) k.add([0], texts[1]) k.add([0], texts[2]) self.log('k._l=' + pformat(k._l)) m = list(k.mash_iter([0, 1, 2])) self.assertEqual(m, ['header', 'aaa', 'line from 1', 'bbb', 'line from 2', 'more from 2']) class Khayyam(TestBase): """Test changes to multi-line texts, and read/write""" def runTest(self): rawtexts = [ """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, -- and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise enow!""", """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, -- and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now!""", """A Book of poems underneath the tree, A Jug of Wine, a Loaf of Bread, and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now! -- O. Khayyam""", """A Book of Verses underneath the Bough, A Jug of Wine, a Loaf of Bread, and Thou Beside me singing in the Wilderness -- Oh, Wilderness were Paradise now!""", ] texts = [[l.strip() for l in t.split('\n')] for t in rawtexts] k = Weave() parents = set() for t in texts: ver = k.add(list(parents), t) parents.add(ver) self.log("k._l=" + pformat(k._l)) for i, t in enumerate(texts): self.assertEqual(k.get(i), t) self.check_read_write(k) def testweave(): import testsweet from unittest import TestSuite, TestLoader import testweave tl = TestLoader() suite = TestSuite() suite.addTest(tl.loadTestsFromModule(testweave)) return int(not testsweet.run_suite(suite)) # for shell 0=true if __name__ == '__main__': import sys sys.exit(testweave()) commit refs/heads/master mark :988 committer Martin Pool 1121056124 +1000 data 83 - weave stores only direct parents, and calculates and memoizes expansion as needed from :987 M 644 inline bzrlib/weave.py data 21379 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? # TODO: Separate out some code to read and write weaves. # TODO: End marker for each version so we can stop reading? # TODO: Check that no insertion occurs inside a deletion that was # active in the version of the insertion. # TODO: Perhaps a special slower check() method that verifies more # nesting constraints and the MD5 of each version? try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. To clients the "lines" of the file are represented as a list of strings. These strings will typically have terminal newline characters, but this is not required. In particular files commonly do not have a newline at the end of the file. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of parents, indexed by version number. It is only necessary to store the minimal set of parents for each version; the parent's parents are implied. _i Full set of inclusions for each revision. _sha1s List of hex SHA-1 of each version, or None if not recorded. """ def __init__(self): self._l = [] self._v = [] self._sha1s = [] self._i = {} def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of direct parent version numbers. text Sequence of lines to be added in the new version.""" ## self._check_versions(parents) ## self._check_lines(text) idx = len(self._v) import sha s = sha.new() for l in text: s.update(l) sha1 = s.hexdigest() del s if parents: ancestors = self.inclusions(parents) delta = self._delta(ancestors, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._addversion(parents) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._addversion(None) self._sha1s.append(sha1) return idx def _expand(self, version): if version in self._i: return self._i[version] else: i = set([version]) for pv in self._v[version]: i.update(self._expand(pv)) self._i[version] = i return i def inclusions(self, versions): """Expand out everything included by versions.""" i = set(versions) try: for v in versions: i.update(self._expand(v)) except IndexError: raise ValueError("version %d not present in weave" % v) return i def minimal_parents(self, version): """Find the minimal set of parents for the version.""" included = self._v[version] if not included: return [] li = list(included) li.sort() li.reverse() mininc = [] gotit = set() for pv in li: if pv not in gotit: mininc.append(pv) gotit.update(self._v[pv]) assert mininc[0] >= 0 assert mininc[-1] < version return mininc def _addversion(self, parents): if parents: self._v.append(parents) else: self._v.append(frozenset()) def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, version): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" included = self.inclusions([version]) for origin, lineno, text in self._extract(self.inclusions(included)): yield origin, text def _extract(self, included): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ istack = [] dset = set() lineno = 0 # line of weave, 0-based isactive = False WFE = WeaveFormatError for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': assert v not in istack istack.append(v) if not dset: isactive = (v in included) elif c == '}': oldv = istack.pop() assert oldv == v isactive = (not dset) and (istack and istack[-1] in included) elif c == '[': if v in included: assert v not in dset dset.add(v) isactive = False else: assert c == ']' if v in included: assert v in dset dset.remove(v) isactive = (not dset) and (istack and istack[-1] in included) else: assert isinstance(l, basestring) if isactive: yield istack[-1], lineno, l lineno += 1 if istack: raise WFE("unclosed insertion blocks at end of weave", istack) if dset: raise WFE("unclosed deletion blocks at end of weave", dset) def get_iter(self, version): """Yield lines for the specified version.""" for origin, lineno, line in self._extract(self.inclusions([version])): yield line def get(self, index): return list(self.get_iter(index)) def mash_iter(self, included): """Return composed version of multiple included versions.""" included = frozenset(included) for origin, lineno, text in self._extract(self.inclusions(included)): yield text def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def numversions(self): l = len(self._v) assert l == len(self._sha1s) return l def check(self): # check no circular inclusions for version in range(self.numversions()): inclusions = list(self._v[version]) if inclusions: inclusions.sort() if inclusions[-1] >= version: raise WeaveFormatError("invalid included version %d for index %d" % (inclusions[-1], version)) # try extracting all versions; this is a bit slow and parallel # extraction could be used import sha for version in range(self.numversions()): s = sha.new() for l in self.get_iter(version): s.update(l) hd = s.hexdigest() expected = self._sha1s[version] if hd != expected: raise WeaveError("mismatched sha1 for version %d; " "got %s, expected %s" % (version, hd, expected)) # TODO: check insertions are properly nested, that there are # no lines outside of insertion blocks, that deletions are # properly paired, etc. def merge(self, merge_versions): """Automerge and mark conflicts between versions. This returns a sequence, each entry describing alternatives for a chunk of the file. Each of the alternatives is given as a list of lines. If there is a chunk of the file where there's no diagreement, only one alternative is given. """ # approach: find the included versions common to all the # merged versions raise NotImplementedError() def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ # basis a list of (origin, lineno, line) basis_lineno = [] basis_lines = [] for origin, lineno, line in self._extract(self.inclusions(included)): basis_lineno.append(lineno) basis_lines.append(line) # add a sentinal, because we can also match against the final line basis_lineno.append(len(self._l)) # XXX: which line of the weave should we really consider # matches the end of the file? the current code says it's the # last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis_lineno[i1] real_i2 = basis_lineno[i2] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def weave_info(filename, out): """Show some text information about the weave.""" from weavefile import read_weave wf = file(filename, 'rb') w = read_weave(wf) # FIXME: doesn't work on pipes weave_size = wf.tell() print >>out, "weave file size %d bytes" % weave_size print >>out, "weave contains %d versions" % len(w._v) total = 0 print '%6s %6s %8s %40s %20s' % ('ver', 'lines', 'bytes', 'sha1', 'parents') for i in (6, 6, 8, 40, 20): print '-' * i, print for i in range(len(w._v)): text = w.get(i) lines = len(text) bytes = sum((len(a) for a in text)) sha1 = w._sha1s[i] print '%6d %6d %8d %40s' % (i, lines, bytes, sha1), for pv in w._v[i]: print pv, print total += bytes print >>out, "versions total %d bytes" % total print >>out, "compression ratio %.3f" % (float(total)/float(weave_size)) def usage(): print """bzr weave tool Experimental tool for weave algorithm. usage: weave init WEAVEFILE Create an empty weave file weave get WEAVEFILE VERSION Write out specified version. weave check WEAVEFILE Check consistency of all versions. weave info WEAVEFILE Display table of contents. weave add WEAVEFILE [BASE...] < NEWTEXT Add NEWTEXT, with specified parent versions. weave annotate WEAVEFILE VERSION Display origin of each line. weave mash WEAVEFILE VERSION... Display composite of all selected versions. weave merge WEAVEFILE VERSION1 VERSION2 > OUT Auto-merge two versions and display conflicts. example: % weave init foo.weave % vi foo.txt % weave add foo.weave < foo.txt added version 0 (create updated version) % vi foo.txt % weave get foo.weave 0 | diff -u - foo.txt % weave add foo.weave 0 < foo.txt added version 1 % weave get foo.weave 0 > foo.txt (create forked version) % vi foo.txt % weave add foo.weave 0 < foo.txt added version 2 % weave merge foo.weave 1 2 > foo.txt (merge them) % vi foo.txt (resolve conflicts) % weave add foo.weave 1 2 < foo.txt (commit merged version) """ def main(argv): import sys import os from weavefile import write_weave, read_weave cmd = argv[1] def readit(): return read_weave(file(argv[2], 'rb')) if cmd == 'help': usage() elif cmd == 'add': w = readit() # at the moment, based on everything in the file parents = map(int, argv[3:]) lines = sys.stdin.readlines() ver = w.add(parents, lines) write_weave(w, file(argv[2], 'wb')) print 'added version %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() write_weave(w, file(fn, 'wb')) elif cmd == 'get': # get one version w = readit() sys.stdout.writelines(w.get_iter(int(argv[3]))) elif cmd == 'mash': # get composite w = readit() sys.stdout.writelines(w.mash_iter(map(int, argv[3:]))) elif cmd == 'annotate': w = readit() # newline is added to all lines regardless; too hard to get # reasonable formatting otherwise lasto = None for origin, text in w.annotate(int(argv[3])): text = text.rstrip('\r\n') if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin elif cmd == 'info': weave_info(argv[2], sys.stdout) elif cmd == 'check': w = readit() w.check() elif cmd == 'inclusions': w = readit() print ' '.join(map(str, w.inclusions([int(argv[3])]))) elif cmd == 'parents': w = readit() print ' '.join(map(str, w._v[int(argv[3])])) elif cmd == 'merge': if len(argv) != 5: usage() return 1 w = readit() v1, v2 = map(int, argv[3:5]) basis = w.inclusions([v1]).intersection(w.inclusions([v2])) base_lines = list(w.mash_iter(basis)) a_lines = list(w.get(v1)) b_lines = list(w.get(v2)) from bzrlib.merge3 import Merge3 m3 = Merge3(base_lines, a_lines, b_lines) name_a = 'version %d' % v1 name_b = 'version %d' % v2 sys.stdout.writelines(m3.merge_lines(name_a=name_a, name_b=name_b)) else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) M 644 inline bzrlib/weavefile.py data 3852 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Store and retrieve weaves in files. There is one format marker followed by a blank line, followed by a series of version headers, followed by the weave itself. Each version marker has 'i' and the included previous versions, then '1' and the SHA-1 of the text, if known. The inclusions do not need to list versions included by a parent. The weave is bracketed by 'w' and 'W' lines, and includes the '{}[]' processing instructions. Lines of text are prefixed by '.' if the line contains a newline, or ',' if not. """ # TODO: When extracting a single version it'd be enough to just pass # an iterator returning the weave lines... FORMAT_1 = '# bzr weave file v3\n' def write_weave(weave, f, format=None): if format == None or format == 1: return write_weave_v1(weave, f) else: raise ValueError("unknown weave format %r" % format) def write_weave_v1(weave, f): """Write weave to file f.""" print >>f, FORMAT_1, for version, included in enumerate(weave._v): if included: # mininc = weave.minimal_parents(version) mininc = included print >>f, 'i', for i in mininc: print >>f, i, print >>f else: print >>f, 'i' print >>f, '1', weave._sha1s[version] print >>f print >>f, 'w' for l in weave._l: if isinstance(l, tuple): assert l[0] in '{}[]' print >>f, '%s %d' % l else: # text line if not l: print >>f, ', ' elif l[-1] == '\n': assert l.find('\n', 0, -1) == -1 print >>f, '.', l, else: assert l.find('\n') == -1 print >>f, ',', l print >>f, 'W' def read_weave(f): return read_weave_v1(f) def read_weave_v1(f): from weave import Weave, WeaveFormatError w = Weave() wfe = WeaveFormatError l = f.readline() if l != FORMAT_1: raise WeaveFormatError('invalid weave file header: %r' % l) ver = 0 while True: l = f.readline() if l[0] == 'i': ver += 1 if len(l) > 2: included = map(int, l[2:].split(' ')) w._addversion(included) else: w._addversion(None) l = f.readline()[:-1] assert l.startswith('1 ') w._sha1s.append(l[2:]) l = f.readline() assert l == '\n' elif l == 'w\n': break else: raise WeaveFormatError('unexpected line %r' % l) while True: l = f.readline() if l == 'W\n': break elif l.startswith('. '): w._l.append(intern(l[2:])) # include newline elif l.startswith(', '): w._l.append(l[2:-1]) # exclude newline else: assert l[0] in '{}[]', l assert l[1] == ' ', l w._l.append((intern(l[0]), int(l[2:]))) return w commit refs/heads/master mark :989 committer Martin Pool 1121057587 +1000 data 43 - Refactor weave calculation of inclusions from :988 M 644 inline bzrlib/weave.py data 21073 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? # TODO: Separate out some code to read and write weaves. # TODO: End marker for each version so we can stop reading? # TODO: Check that no insertion occurs inside a deletion that was # active in the version of the insertion. # TODO: Perhaps a special slower check() method that verifies more # nesting constraints and the MD5 of each version? try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. To clients the "lines" of the file are represented as a list of strings. These strings will typically have terminal newline characters, but this is not required. In particular files commonly do not have a newline at the end of the file. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of parents, indexed by version number. It is only necessary to store the minimal set of parents for each version; the parent's parents are implied. _sha1s List of hex SHA-1 of each version, or None if not recorded. """ def __init__(self): self._l = [] self._v = [] self._sha1s = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of direct parent version numbers. text Sequence of lines to be added in the new version.""" ## self._check_versions(parents) ## self._check_lines(text) idx = len(self._v) import sha s = sha.new() for l in text: s.update(l) sha1 = s.hexdigest() del s if parents: ancestors = self.inclusions(parents) delta = self._delta(ancestors, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._addversion(parents) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._addversion(None) self._sha1s.append(sha1) return idx def inclusions(self, versions): """Return set of all ancestors of given version(s).""" i = set(versions) v = max(versions) try: while v >= 0: if v in i: # include all its parents i.update(self._v[v]) v -= 1 return i except IndexError: raise ValueError("version %d not present in weave" % v) def minimal_parents(self, version): """Find the minimal set of parents for the version.""" included = self._v[version] if not included: return [] li = list(included) li.sort(reverse=True) mininc = [] gotit = set() for pv in li: if pv not in gotit: mininc.append(pv) gotit.update(self.inclusions(pv)) assert mininc[0] >= 0 assert mininc[-1] < version return mininc def _addversion(self, parents): if parents: self._v.append(parents) else: self._v.append(frozenset()) def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, version): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" for origin, lineno, text in self._extract([version]): yield origin, text def _extract(self, versions): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ included = self.inclusions(versions) istack = [] dset = set() lineno = 0 # line of weave, 0-based isactive = False WFE = WeaveFormatError for l in self._l: if isinstance(l, tuple): c, v = l if c == '{': assert v not in istack istack.append(v) if not dset: isactive = (v in included) elif c == '}': oldv = istack.pop() assert oldv == v isactive = (not dset) and (istack and istack[-1] in included) elif c == '[': if v in included: assert v not in dset dset.add(v) isactive = False else: assert c == ']' if v in included: assert v in dset dset.remove(v) isactive = (not dset) and (istack and istack[-1] in included) else: assert isinstance(l, basestring) if isactive: yield istack[-1], lineno, l lineno += 1 if istack: raise WFE("unclosed insertion blocks at end of weave", istack) if dset: raise WFE("unclosed deletion blocks at end of weave", dset) def get_iter(self, version): """Yield lines for the specified version.""" for origin, lineno, line in self._extract([version]): yield line def get(self, index): return list(self.get_iter(index)) def mash_iter(self, included): """Return composed version of multiple included versions.""" included = frozenset(included) for origin, lineno, text in self._extract(included): yield text def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def numversions(self): l = len(self._v) assert l == len(self._sha1s) return l def check(self): # check no circular inclusions for version in range(self.numversions()): inclusions = list(self._v[version]) if inclusions: inclusions.sort() if inclusions[-1] >= version: raise WeaveFormatError("invalid included version %d for index %d" % (inclusions[-1], version)) # try extracting all versions; this is a bit slow and parallel # extraction could be used import sha for version in range(self.numversions()): s = sha.new() for l in self.get_iter(version): s.update(l) hd = s.hexdigest() expected = self._sha1s[version] if hd != expected: raise WeaveError("mismatched sha1 for version %d; " "got %s, expected %s" % (version, hd, expected)) # TODO: check insertions are properly nested, that there are # no lines outside of insertion blocks, that deletions are # properly paired, etc. def merge(self, merge_versions): """Automerge and mark conflicts between versions. This returns a sequence, each entry describing alternatives for a chunk of the file. Each of the alternatives is given as a list of lines. If there is a chunk of the file where there's no diagreement, only one alternative is given. """ # approach: find the included versions common to all the # merged versions raise NotImplementedError() def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ # basis a list of (origin, lineno, line) basis_lineno = [] basis_lines = [] for origin, lineno, line in self._extract(included): basis_lineno.append(lineno) basis_lines.append(line) # add a sentinal, because we can also match against the final line basis_lineno.append(len(self._l)) # XXX: which line of the weave should we really consider # matches the end of the file? the current code says it's the # last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis_lineno[i1] real_i2 = basis_lineno[i2] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def weave_info(filename, out): """Show some text information about the weave.""" from weavefile import read_weave wf = file(filename, 'rb') w = read_weave(wf) # FIXME: doesn't work on pipes weave_size = wf.tell() print >>out, "weave file size %d bytes" % weave_size print >>out, "weave contains %d versions" % len(w._v) total = 0 print '%6s %6s %8s %40s %20s' % ('ver', 'lines', 'bytes', 'sha1', 'parents') for i in (6, 6, 8, 40, 20): print '-' * i, print for i in range(len(w._v)): text = w.get(i) lines = len(text) bytes = sum((len(a) for a in text)) sha1 = w._sha1s[i] print '%6d %6d %8d %40s' % (i, lines, bytes, sha1), for pv in w._v[i]: print pv, print total += bytes print >>out, "versions total %d bytes" % total print >>out, "compression ratio %.3f" % (float(total)/float(weave_size)) def usage(): print """bzr weave tool Experimental tool for weave algorithm. usage: weave init WEAVEFILE Create an empty weave file weave get WEAVEFILE VERSION Write out specified version. weave check WEAVEFILE Check consistency of all versions. weave info WEAVEFILE Display table of contents. weave add WEAVEFILE [BASE...] < NEWTEXT Add NEWTEXT, with specified parent versions. weave annotate WEAVEFILE VERSION Display origin of each line. weave mash WEAVEFILE VERSION... Display composite of all selected versions. weave merge WEAVEFILE VERSION1 VERSION2 > OUT Auto-merge two versions and display conflicts. example: % weave init foo.weave % vi foo.txt % weave add foo.weave < foo.txt added version 0 (create updated version) % vi foo.txt % weave get foo.weave 0 | diff -u - foo.txt % weave add foo.weave 0 < foo.txt added version 1 % weave get foo.weave 0 > foo.txt (create forked version) % vi foo.txt % weave add foo.weave 0 < foo.txt added version 2 % weave merge foo.weave 1 2 > foo.txt (merge them) % vi foo.txt (resolve conflicts) % weave add foo.weave 1 2 < foo.txt (commit merged version) """ def main(argv): import sys import os from weavefile import write_weave, read_weave cmd = argv[1] def readit(): return read_weave(file(argv[2], 'rb')) if cmd == 'help': usage() elif cmd == 'add': w = readit() # at the moment, based on everything in the file parents = map(int, argv[3:]) lines = sys.stdin.readlines() ver = w.add(parents, lines) write_weave(w, file(argv[2], 'wb')) print 'added version %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() write_weave(w, file(fn, 'wb')) elif cmd == 'get': # get one version w = readit() sys.stdout.writelines(w.get_iter(int(argv[3]))) elif cmd == 'mash': # get composite w = readit() sys.stdout.writelines(w.mash_iter(map(int, argv[3:]))) elif cmd == 'annotate': w = readit() # newline is added to all lines regardless; too hard to get # reasonable formatting otherwise lasto = None for origin, text in w.annotate(int(argv[3])): text = text.rstrip('\r\n') if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin elif cmd == 'info': weave_info(argv[2], sys.stdout) elif cmd == 'check': w = readit() w.check() elif cmd == 'inclusions': w = readit() print ' '.join(map(str, w.inclusions([int(argv[3])]))) elif cmd == 'parents': w = readit() print ' '.join(map(str, w._v[int(argv[3])])) elif cmd == 'merge': if len(argv) != 5: usage() return 1 w = readit() v1, v2 = map(int, argv[3:5]) basis = w.inclusions([v1]).intersection(w.inclusions([v2])) base_lines = list(w.mash_iter(basis)) a_lines = list(w.get(v1)) b_lines = list(w.get(v2)) from bzrlib.merge3 import Merge3 m3 = Merge3(base_lines, a_lines, b_lines) name_a = 'version %d' % v1 name_b = 'version %d' % v2 sys.stdout.writelines(m3.merge_lines(name_a=name_a, name_b=name_b)) else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) M 644 inline tools/convertinv.py data 2084 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Experiment in converting existing bzr branches to weaves.""" import bzrlib.branch from bzrlib.weave import Weave from bzrlib.weavefile import write_weave from bzrlib.progress import ProgressBar import tempfile import hotshot, hotshot.stats import sys def convert(): WEAVE_NAME = "inventory.weave" pb = ProgressBar() wf = Weave() b = bzrlib.branch.find_branch('.') parents = set() revno = 1 rev_history = b.revision_history() for rev_id in rev_history: pb.update('converting inventory', revno, len(rev_history)) inv_xml = b.inventory_store[rev_id].readlines() weave_id = wf.add(parents, inv_xml) parents = set([weave_id]) # always just one parent revno += 1 pb.update('write weave', None, None) write_weave(wf, file(WEAVE_NAME, 'wb')) pb.clear() def profile_convert(): prof_f = tempfile.NamedTemporaryFile() prof = hotshot.Profile(prof_f.name) prof.runcall(convert) prof.close() stats = hotshot.stats.load(prof_f.name) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) if '-p' in sys.argv[1:]: profile_convert() else: convert() commit refs/heads/master mark :990 committer Martin Pool 1121060711 +1000 data 78 - small optimization for weave extract - show progressbar during weave check from :989 M 644 inline bzrlib/weave.py data 21190 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Author: Martin Pool """Weave - storage of related text file versions""" # TODO: Perhaps have copy method for Weave instances? # XXX: If we do weaves this way, will a merge still behave the same # way if it's done in a different order? That's a pretty desirable # property. # TODO: How to write these to disk? One option is cPickle, which # would be fast but less friendly to C, and perhaps not portable. Another is # TODO: Nothing here so far assumes the lines are really \n newlines, # rather than being split up in some other way. We could accomodate # binaries, perhaps by naively splitting on \n or perhaps using # something like a rolling checksum. # TODO: Perhaps track SHA-1 in the header for protection? This would # be redundant with it being stored in the inventory, but perhaps # usefully so? # TODO: Track version names as well as indexes. # TODO: Probably do transitive expansion when specifying parents? # TODO: Separate out some code to read and write weaves. # TODO: End marker for each version so we can stop reading? # TODO: Check that no insertion occurs inside a deletion that was # active in the version of the insertion. # TODO: Perhaps a special slower check() method that verifies more # nesting constraints and the MD5 of each version? try: set frozenset except NameError: from sets import Set, ImmutableSet set = Set frozenset = ImmutableSet del Set, ImmutableSet class WeaveError(Exception): """Exception in processing weave""" class WeaveFormatError(WeaveError): """Weave invariant violated""" class Weave(object): """weave - versioned text file storage. A Weave manages versions of line-based text files, keeping track of the originating version for each line. To clients the "lines" of the file are represented as a list of strings. These strings will typically have terminal newline characters, but this is not required. In particular files commonly do not have a newline at the end of the file. Texts can be identified in either of two ways: * a nonnegative index number. * a version-id string. Typically the index number will be valid only inside this weave and the version-id is used to reference it in the larger world. The weave is represented as a list mixing edit instructions and literal text. Each entry in _l can be either a string (or unicode), or a tuple. If a string, it means that the given line should be output in the currently active revisions. If a tuple, it gives a processing instruction saying in which revisions the enclosed lines are active. The tuple has the form (instruction, version). The instruction can be '{' or '}' for an insertion block, and '[' and ']' for a deletion block respectively. The version is the integer version index. There is no replace operator, only deletes and inserts. Constraints/notes: * A later version can delete lines that were introduced by any number of ancestor versions; this implies that deletion instructions can span insertion blocks without regard to the insertion block's nesting. * Similarly, deletions need not be properly nested with regard to each other, because they might have been generated by independent revisions. * Insertions are always made by inserting a new bracketed block into a single point in the previous weave. This implies they can nest but not overlap, and the nesting must always have later insertions on the inside. * It doesn't seem very useful to have an active insertion inside an inactive insertion, but it might happen. * Therefore, all instructions are always"considered"; that is passed onto and off the stack. An outer inactive block doesn't disable an inner block. * Lines are enabled if the most recent enclosing insertion is active and none of the enclosing deletions are active. * There is no point having a deletion directly inside its own insertion; you might as well just not write it. And there should be no way to get an earlier version deleting a later version. _l Text of the weave. _v List of parents, indexed by version number. It is only necessary to store the minimal set of parents for each version; the parent's parents are implied. _sha1s List of hex SHA-1 of each version, or None if not recorded. """ def __init__(self): self._l = [] self._v = [] self._sha1s = [] def __eq__(self, other): if not isinstance(other, Weave): return False return self._v == other._v \ and self._l == other._l def __ne__(self, other): return not self.__eq__(other) def add(self, parents, text): """Add a single text on top of the weave. Returns the index number of the newly added version. parents List or set of direct parent version numbers. text Sequence of lines to be added in the new version.""" ## self._check_versions(parents) ## self._check_lines(text) idx = len(self._v) import sha s = sha.new() for l in text: s.update(l) sha1 = s.hexdigest() del s if parents: ancestors = self.inclusions(parents) delta = self._delta(ancestors, text) # offset gives the number of lines that have been inserted # into the weave up to the current point; if the original edit instruction # says to change line A then we actually change (A+offset) offset = 0 for i1, i2, newlines in delta: assert 0 <= i1 assert i1 <= i2 assert i2 <= len(self._l) # the deletion and insertion are handled separately. # first delete the region. if i1 != i2: self._l.insert(i1+offset, ('[', idx)) self._l.insert(i2+offset+1, (']', idx)) offset += 2 # is this OK??? if newlines: # there may have been a deletion spanning up to # i2; we want to insert after this region to make sure # we don't destroy ourselves i = i2 + offset self._l[i:i] = [('{', idx)] \ + newlines \ + [('}', idx)] offset += 2 + len(newlines) self._addversion(parents) else: # special case; adding with no parents revision; can do this # more quickly by just appending unconditionally self._l.append(('{', idx)) self._l += text self._l.append(('}', idx)) self._addversion(None) self._sha1s.append(sha1) return idx def inclusions(self, versions): """Return set of all ancestors of given version(s).""" i = set(versions) v = max(versions) try: while v >= 0: if v in i: # include all its parents i.update(self._v[v]) v -= 1 return i except IndexError: raise ValueError("version %d not present in weave" % v) def minimal_parents(self, version): """Find the minimal set of parents for the version.""" included = self._v[version] if not included: return [] li = list(included) li.sort(reverse=True) mininc = [] gotit = set() for pv in li: if pv not in gotit: mininc.append(pv) gotit.update(self.inclusions(pv)) assert mininc[0] >= 0 assert mininc[-1] < version return mininc def _addversion(self, parents): if parents: self._v.append(parents) else: self._v.append(frozenset()) def _check_lines(self, text): if not isinstance(text, list): raise ValueError("text should be a list, not %s" % type(text)) for l in text: if not isinstance(l, basestring): raise ValueError("text line should be a string or unicode, not %s" % type(l)) def _check_versions(self, indexes): """Check everything in the sequence of indexes is valid""" for i in indexes: try: self._v[i] except IndexError: raise IndexError("invalid version number %r" % i) def annotate(self, index): return list(self.annotate_iter(index)) def annotate_iter(self, version): """Yield list of (index-id, line) pairs for the specified version. The index indicates when the line originated in the weave.""" for origin, lineno, text in self._extract([version]): yield origin, text def _extract(self, versions): """Yield annotation of lines in included set. Yields a sequence of tuples (origin, lineno, text), where origin is the origin version, lineno the index in the weave, and text the text of the line. The set typically but not necessarily corresponds to a version. """ included = self.inclusions(versions) istack = [] dset = set() lineno = 0 # line of weave, 0-based isactive = None WFE = WeaveFormatError for l in self._l: if isinstance(l, tuple): c, v = l isactive = None if c == '{': assert v not in istack istack.append(v) elif c == '}': oldv = istack.pop() assert oldv == v elif c == '[': if v in included: assert v not in dset dset.add(v) else: assert c == ']' if v in included: assert v in dset dset.remove(v) else: assert isinstance(l, basestring) if isactive is None: isactive = (not dset) and istack and (istack[-1] in included) if isactive: yield istack[-1], lineno, l lineno += 1 if istack: raise WFE("unclosed insertion blocks at end of weave", istack) if dset: raise WFE("unclosed deletion blocks at end of weave", dset) def get_iter(self, version): """Yield lines for the specified version.""" for origin, lineno, line in self._extract([version]): yield line def get(self, index): return list(self.get_iter(index)) def mash_iter(self, included): """Return composed version of multiple included versions.""" included = frozenset(included) for origin, lineno, text in self._extract(included): yield text def dump(self, to_file): from pprint import pprint print >>to_file, "Weave._l = ", pprint(self._l, to_file) print >>to_file, "Weave._v = ", pprint(self._v, to_file) def numversions(self): l = len(self._v) assert l == len(self._sha1s) return l def check(self, progress_bar=None): # check no circular inclusions for version in range(self.numversions()): inclusions = list(self._v[version]) if inclusions: inclusions.sort() if inclusions[-1] >= version: raise WeaveFormatError("invalid included version %d for index %d" % (inclusions[-1], version)) # try extracting all versions; this is a bit slow and parallel # extraction could be used import sha nv = self.numversions() for version in range(nv): if progress_bar: progress_bar.update('checking text', version, nv) s = sha.new() for l in self.get_iter(version): s.update(l) hd = s.hexdigest() expected = self._sha1s[version] if hd != expected: raise WeaveError("mismatched sha1 for version %d; " "got %s, expected %s" % (version, hd, expected)) # TODO: check insertions are properly nested, that there are # no lines outside of insertion blocks, that deletions are # properly paired, etc. def merge(self, merge_versions): """Automerge and mark conflicts between versions. This returns a sequence, each entry describing alternatives for a chunk of the file. Each of the alternatives is given as a list of lines. If there is a chunk of the file where there's no diagreement, only one alternative is given. """ # approach: find the included versions common to all the # merged versions raise NotImplementedError() def _delta(self, included, lines): """Return changes from basis to new revision. The old text for comparison is the union of included revisions. This is used in inserting a new text. Delta is returned as a sequence of (weave1, weave2, newlines). This indicates that weave1:weave2 of the old weave should be replaced by the sequence of lines in newlines. Note that these line numbers are positions in the total weave and don't correspond to the lines in any extracted version, or even the extracted union of included versions. If line1=line2, this is a pure insert; if newlines=[] this is a pure delete. (Similar to difflib.) """ # basis a list of (origin, lineno, line) basis_lineno = [] basis_lines = [] for origin, lineno, line in self._extract(included): basis_lineno.append(lineno) basis_lines.append(line) # add a sentinal, because we can also match against the final line basis_lineno.append(len(self._l)) # XXX: which line of the weave should we really consider # matches the end of the file? the current code says it's the # last line of the weave? from difflib import SequenceMatcher s = SequenceMatcher(None, basis_lines, lines) # TODO: Perhaps return line numbers from composed weave as well? for tag, i1, i2, j1, j2 in s.get_opcodes(): ##print tag, i1, i2, j1, j2 if tag == 'equal': continue # i1,i2 are given in offsets within basis_lines; we need to map them # back to offsets within the entire weave real_i1 = basis_lineno[i1] real_i2 = basis_lineno[i2] assert 0 <= j1 assert j1 <= j2 assert j2 <= len(lines) yield real_i1, real_i2, lines[j1:j2] def weave_info(filename, out): """Show some text information about the weave.""" from weavefile import read_weave wf = file(filename, 'rb') w = read_weave(wf) # FIXME: doesn't work on pipes weave_size = wf.tell() print >>out, "weave file size %d bytes" % weave_size print >>out, "weave contains %d versions" % len(w._v) total = 0 print '%6s %6s %8s %40s %20s' % ('ver', 'lines', 'bytes', 'sha1', 'parents') for i in (6, 6, 8, 40, 20): print '-' * i, print for i in range(len(w._v)): text = w.get(i) lines = len(text) bytes = sum((len(a) for a in text)) sha1 = w._sha1s[i] print '%6d %6d %8d %40s' % (i, lines, bytes, sha1), for pv in w._v[i]: print pv, print total += bytes print >>out, "versions total %d bytes" % total print >>out, "compression ratio %.3f" % (float(total)/float(weave_size)) def usage(): print """bzr weave tool Experimental tool for weave algorithm. usage: weave init WEAVEFILE Create an empty weave file weave get WEAVEFILE VERSION Write out specified version. weave check WEAVEFILE Check consistency of all versions. weave info WEAVEFILE Display table of contents. weave add WEAVEFILE [BASE...] < NEWTEXT Add NEWTEXT, with specified parent versions. weave annotate WEAVEFILE VERSION Display origin of each line. weave mash WEAVEFILE VERSION... Display composite of all selected versions. weave merge WEAVEFILE VERSION1 VERSION2 > OUT Auto-merge two versions and display conflicts. example: % weave init foo.weave % vi foo.txt % weave add foo.weave < foo.txt added version 0 (create updated version) % vi foo.txt % weave get foo.weave 0 | diff -u - foo.txt % weave add foo.weave 0 < foo.txt added version 1 % weave get foo.weave 0 > foo.txt (create forked version) % vi foo.txt % weave add foo.weave 0 < foo.txt added version 2 % weave merge foo.weave 1 2 > foo.txt (merge them) % vi foo.txt (resolve conflicts) % weave add foo.weave 1 2 < foo.txt (commit merged version) """ def main(argv): import sys import os from weavefile import write_weave, read_weave from bzrlib.progress import ProgressBar #import psyco #psyco.full() cmd = argv[1] def readit(): return read_weave(file(argv[2], 'rb')) if cmd == 'help': usage() elif cmd == 'add': w = readit() # at the moment, based on everything in the file parents = map(int, argv[3:]) lines = sys.stdin.readlines() ver = w.add(parents, lines) write_weave(w, file(argv[2], 'wb')) print 'added version %d' % ver elif cmd == 'init': fn = argv[2] if os.path.exists(fn): raise IOError("file exists") w = Weave() write_weave(w, file(fn, 'wb')) elif cmd == 'get': # get one version w = readit() sys.stdout.writelines(w.get_iter(int(argv[3]))) elif cmd == 'mash': # get composite w = readit() sys.stdout.writelines(w.mash_iter(map(int, argv[3:]))) elif cmd == 'annotate': w = readit() # newline is added to all lines regardless; too hard to get # reasonable formatting otherwise lasto = None for origin, text in w.annotate(int(argv[3])): text = text.rstrip('\r\n') if origin == lasto: print ' | %s' % (text) else: print '%5d | %s' % (origin, text) lasto = origin elif cmd == 'info': weave_info(argv[2], sys.stdout) elif cmd == 'check': w = readit() pb = ProgressBar() w.check(pb) pb.clear() elif cmd == 'inclusions': w = readit() print ' '.join(map(str, w.inclusions([int(argv[3])]))) elif cmd == 'parents': w = readit() print ' '.join(map(str, w._v[int(argv[3])])) elif cmd == 'merge': if len(argv) != 5: usage() return 1 w = readit() v1, v2 = map(int, argv[3:5]) basis = w.inclusions([v1]).intersection(w.inclusions([v2])) base_lines = list(w.mash_iter(basis)) a_lines = list(w.get(v1)) b_lines = list(w.get(v2)) from bzrlib.merge3 import Merge3 m3 = Merge3(base_lines, a_lines, b_lines) name_a = 'version %d' % v1 name_b = 'version %d' % v2 sys.stdout.writelines(m3.merge_lines(name_a=name_a, name_b=name_b)) else: raise ValueError('unknown command %r' % cmd) if __name__ == '__main__': import sys sys.exit(main(sys.argv)) commit refs/heads/master mark :991 committer Martin Pool 1121060815 +1000 data 17 - ignore tmp dir from :990 M 644 inline .bzrignore data 145 ## *.diff ./doc/*.html *.py[oc] *~ .arch-ids .bzr.profile .arch-inventory {arch} CHANGELOG bzr-test.log bzr.1 ,,* testbzr.log api test.log ./tmp commit refs/heads/master mark :992 committer Martin Pool 1121060828 +1000 data 22 - add weave benchmark from :991 M 644 inline tools/weavebench.py data 2546 #! /usr/bin/python # Copyright (C) 2005 Canonical Ltd # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Weave algorithms benchmark""" import bzrlib.branch from bzrlib.weave import Weave from bzrlib.weavefile import write_weave from bzrlib.progress import ProgressBar from random import randrange, randint import tempfile import hotshot, hotshot.stats import sys WEAVE_NAME = "bench.weave" NUM_REVS = 10000 def build(): pb = ProgressBar() wf = Weave() lines = [] parents = [] for i in xrange(NUM_REVS): pb.update('building', i, NUM_REVS) for j in range(randint(0, 4)): o = randint(0, len(lines)) lines.insert(o, "new in version %i\n" % i) for j in range(randint(0, 2)): if lines: del lines[randrange(0, len(lines))] rev_id = wf.add(parents, lines) parents = [rev_id] write_weave(wf, file(WEAVE_NAME, 'wb')) # parents = set() # revno = 1 # rev_history = b.revision_history() # for rev_id in rev_history: # pb.update('converting inventory', revno, len(rev_history)) # inv_xml = b.inventory_store[rev_id].readlines() # weave_id = wf.add(parents, inv_xml) # parents = set([weave_id]) # always just one parent # revno += 1 # pb.update('write weave', None, None) # write_weave(wf, file(WEAVE_NAME, 'wb')) pb.clear() def profileit(fn): prof_f = tempfile.NamedTemporaryFile() prof = hotshot.Profile(prof_f.name) prof.runcall(fn) prof.close() stats = hotshot.stats.load(prof_f.name) #stats.strip_dirs() stats.sort_stats('time') ## XXX: Might like to write to stderr or the trace file instead but ## print_stats seems hardcoded to stdout stats.print_stats(20) if '-p' in sys.argv[1:]: profileit(build) else: build()